from io import TextIOWrapper
from msilib.schema import File
from re import I, VERBOSE
import traceback
import sys
import os
import io
import datetime
import random
import hashlib
import psutil
import platform
import requests
from inspect import currentframe, getframeinfo, stack, trace
from math import ceil
DEBUG_FILE = None
BANNED_CLASSES = ['RegexFlags', 'EnumMeta', 'ABCMeta', 'classmethod', 'module', 'SourceFileLoader', 'ModuleSpec', 'type', 'str', 'int', 'float', 'list', 'tuple', 'dict']
MAX_RECURSION = 11
WRITE_TO_STDOUT = False
VERBOSE_STDOUT = False
ROLLER = True
ROLLER_LINES = 50000
DUMP_BYTES = True
DUMP_BYTES_LENGTH = 512
[docs]class Debug:
"""This class will dump debug information into a debug log file.
In order for this to work, first you must set the options of the logger with at least a
DEBUG_FILE parameter. You can do this by either calling :meth:`Debug.set_debug_file`, or
by calling :meth:`Debug.set_config` with a dictionary with at least a DEBUG_FILE item.
All functions are static, you should not instantiate this class.
Once this class is correctly set up, you can start logging information by using any of the
methods in this class. For instance, if you want to log an exception during a try-except, you
can do so by using :meth:`Debug.debugException` or :meth:`Debug.debugCritical` for critical
exceptions.
If you just want to log information, you can use :meth:`Debug.debugLog` at any given time.
You can also use :meth:`Debug.inspect` to inspect any object and dump it's information. This
is very useful when you want to print information related to a variable. If you want to
inspect global variables or local variables, use :meth:`Debug.inspectGlobals` or
:meth:`Debug.inspectLocals` respectively. There is also :meth:`Debug.inspectModules` if you
need to dump all loaded modules.
Last but not least, :meth:`Debug.hexdump` will dump the contents of a bytes array into the
log. If you want to dump binary data into a different file so you can inspect it later by
other means, use :meth:`Debug.bindump`.
**Example**
.. code-block:: python
from Debug import Debug
Debug.set_config({
"DEBUG_FILE": "./debug/debug.log",
"ROLLER_LINES": 25000,
"WRITE_TO_STDOUT": True
})
try:
a = 5
b = 0
c = a/b
except Exception as exc:
Debug.debugException(exc)
This prints the following debug information into debug/debug.log:
.. code-block:: text
-- START OF EXCEPTION (E 0xe7ba3afc) --
12/07/2022, 13:42:10:356113 E 0xe7ba3afc [<class 'ZeroDivisionError'>]
Traceback (most recent call last):
File "E:\PY\maira\debugTest.py", line 12, in <module>
c = a/b
ZeroDivisionError: division by zero
From: E:\PY\maira\debugTest.py 14 <module>
Context: [' Debug.debugException(exc)']
locals():
local: __doc__ (<class 'NoneType'>) -> None
local: __package__ (<class 'NoneType'>) -> None
local: __spec__ (<class 'NoneType'>) -> None
local: __cached__ (<class 'NoneType'>) -> None
local: Debug (<class 'type'>) ->
12/07/2022, 13:42:10:357113 L 0x983305b7 [instance of type]
WARNING: Objects instances of type are ignored due to deep recursion.
END of local: Debug
local: a (<class 'int'>) -> 5
local: b (<class 'int'>) -> 0
local: exc (<class 'ZeroDivisionError'>) ->
12/07/2022, 13:42:10:358114 L 0x4616027d [instance of ZeroDivisionError]
WARNING: This instance of ZeroDivisionError is empty
END of local: exc
-- END OF EXCEPTION --
**Understanding logs**
Each log has a header with the following information:
``<datetime> <log ID> [<event name>]``
``datetime`` is the date and time of the log, up to milliseconds.
``log ID`` is a unique identifier for the log in the following format:
``<log class> 0x<ID>``
Where ``log class`` is the severity of the log ('L' for log/information, 'E' for Exception, and
'C' for Critical error), and ``ID`` is a unique identifier for the log.
``event name`` is the name of the log event.
When debugging exceptions, the log is wrapped into the following headers:
.. code-block:: text
-- START OF EXCEPTION (<log ID>) --
...
-- END OF EXCEPTION --
Everything between the start and the end of the exception, belongs to the exception being reported.
The same applies for critical errors, except it says CRITICAL ERROR instead of EXCEPTION.
Exceptions and critical errors have a unique ID. Errors that generate in the same line of the code
will all have the same ID. This will help you get track of errors as well.
When inspecting objects, the name, type and value of the object will be dumped. Class instances will
also see their members dumped. The debugger will recursively dump all members of the object instance
and add indentation depending on the level of recursion, until MAX_RECURSION is reached.
**Warnings**
You might see the following warnings in your log:
``WARNING: <var num> variables not assigned: <list of not assigned>``
Caused when inspecting locals or globals and passing a list with variable names, but some of the variables
in the list are not assigned at this point.
``WARNING: Recursion limit when calling <debug function>. Skipping.``
Caused when MAX_RECURSION is reached while inspecting an object. At this point the debugger will not recurse
into any of the object's members any more.
``WARNING: <name> (<object>) is empty``
Caused when inspecting a list, dict or tuple that is empty.
``WARNING: Attempting to call <function> on something that is not a <class>.``
Caused when attempting to inspect a list, dict or tuple that is not an instance of their
respective function's intended class (i.e calling debugDict and passing a Tuple as a parameter).
The debugger will tell you what it really is in the next line.
``WARNING: Objects instances of <type> are ignored due to deep recursion.``
Caused when attempting to inspect an object instance of a class that is known to cause deep recursion
problems. Ignore these.
``WARNING: Attempting to call 'debugClass' on object of type <type> (Not a class)``
Caused when calling debugClass on something that doesn't seem to be an instance of a class. For instance,
calling debugClass and passing a dict as it's parameter.
``WARNING: This instance of <type> is empty``
Caused when calling debugClass with an object instance that has no members (it's empty). Functions are
ignored.
**Class functions**
"""
[docs] @staticmethod
def set_debug_file(path: str) -> TextIOWrapper:
"""Set the output file where the log will be stored."""
global DEBUG_FILE
if isinstance(path, str):
if DEBUG_FILE is not None:
DEBUG_FILE.close()
if not os.path.exists(path):
open(path, "w").close()
DEBUG_FILE = open(path, "r+")
DEBUG_FILE.seek(0, io.SEEK_END)
return DEBUG_FILE
else:
return None
[docs] @staticmethod
def set_max_recursion(max_rec: int) -> bool:
"""Set the maximum recursion allowed when inspecting objects.
:param max_rec: Maximum recursion. Must be between 1 and sys.getrecursionlimit() - 10.
10 recursions are reserved to avoid reaching maximum recursion. Set this to something
relatively low (You shouldn't need more than 10)."""
global MAX_RECURSION
if max_rec < 1 or max_rec > sys.getrecursionlimit() - 10:
return False
MAX_RECURSION = max_rec
return True
[docs] @staticmethod
def set_write_to_stdout(write_to_stdout: bool = True):
"""Allow/disallow output to the console."""
global WRITE_TO_STDOUT
WRITE_TO_STDOUT = write_to_stdout
return WRITE_TO_STDOUT
[docs] @staticmethod
def set_verbose_stdout(verbose_stdout: bool = True):
"""When WRITE_TO_STDOUT is enabled, this will force the debugger to print EVERY line to the console."""
global VERBOSE_STDOUT
VERBOSE_STDOUT = verbose_stdout
return VERBOSE_STDOUT
[docs] @staticmethod
def set_roller_log(roller_log: bool = True):
"""Enable/Disable rolling logs. When enabled, log files will delete the first lines if the number
of lines is bigger than ROLLER_LINES."""
global ROLLER
ROLLER = roller_log
return ROLLER
[docs] @staticmethod
def set_roller_lines(roller_lines: int):
""":param roller_lines: The number of lines. Must be between 1 and 10000000.
Set the maximum number of lines before the log starts rolling."""
global ROLLER_LINES
if roller_lines < 1 or roller_lines > 10000000:
return ROLLER_LINES
ROLLER_LINES = roller_lines
return ROLLER_LINES
[docs] @staticmethod
def set_dump_bytes(dump_bytes: bool):
"""Enable/Disable hex dumps of byte-like objects (bytes, ByteIO) during object inspection."""
global DUMP_BYTES
DUMP_BYTES = dump_bytes
return DUMP_BYTES
[docs] @staticmethod
def set_dump_bytes_length(dump_bytes_length: int):
"""When DUMP_BYTES is enabled, sets the maximum number of bytes to dump.
:param dump_bytes_length: The maximum number of bytes to dump. Must be
between 8 and 1000000."""
global DUMP_BYTES_LENGTH
if 8 < dump_bytes_length < 1000000:
DUMP_BYTES_LENGTH = dump_bytes_length
return DUMP_BYTES_LENGTH
[docs] @staticmethod
def set_config(config: dict):
""":param config: A dictionary with the Debugger configuration parameters.
:type config: dict
Set the global configuration of the debugger. The configuration is in a
dictionary object containing the paramenters and their respective values.
The following values are accepted:
| ``DEBUG_FILE`` (*str*): Path to the debug log where the information will be stored.
| ``MAX_RECURSION`` (*int*): Sets the maximum allowed recursion while inspecting. (Default: 11)
| ``WRITE_TO_STDOUT`` (*bool*): Output to the console. (Default: False)
| ``VERBOSE_STDOUT`` (*bool*): Will output EVERYTHING to the console. (Default: False)
| ``ROLLER`` (*bool*): Enable/disable the rolling log file (Default: True)
| ``ROLLER_LINES`` (*int*): Maximum lines in the log until it starts rolling (Default: 50000)
| ``DUMP_BYTES`` (*bool*): Will dump an hex representation of bytes objects (Default: True)
| ``DUMP_BYTES_LENGTH`` (*int*): Maximum length (in bytes) of the dumps (Default: 512)
**Notes**: WRITE_TO_STDOUT will write to STDERR when calling to debugException or
debugCritical.
"""
if 'DEBUG_FILE' in config.keys():
Debug.set_debug_file(config['DEBUG_FILE'])
if 'MAX_RECURSION' in config.keys():
Debug.set_max_recursion(config['MAX_RECURSION'])
if 'WRITE_TO_STDOUT' in config.keys():
Debug.set_write_to_stdout(config['WRITE_TO_STDOUT'])
if 'VERBOSE_STDOUT' in config.keys():
Debug.set_verbose_stdout(config['VERBOSE_STDOUT'])
if 'ROLLER' in config.keys():
Debug.set_roller_lines(config['ROLLER'])
if 'ROLLER_LINES' in config.keys():
Debug.set_roller_lines(config['ROLLER_LINES'])
if 'DUMP_BYTES' in config.keys():
Debug.set_dump_bytes(config['DUMP_BYTES'])
if 'DUMP_BYTES_LENGTH' in config.keys():
Debug.set_dump_bytes_length(config['DUMP_BYTES_LENGTH'])
@staticmethod
def __roll_log():
"""Private. Ignore."""
DEBUG_FILE.seek(0,0)
lines = DEBUG_FILE.readlines()
if len(lines) > ROLLER_LINES:
DEBUG_FILE.seek(0)
DEBUG_FILE.truncate(0)
DEBUG_FILE.writelines(lines[len(lines)-ROLLER_LINES:])
DEBUG_FILE.seek(0,io.SEEK_END)
@staticmethod
def __writelog(msg: str):
"""Private. Ignore."""
if DEBUG_FILE:
DEBUG_FILE.write(msg)
Debug.__roll_log()
if WRITE_TO_STDOUT and VERBOSE_STDOUT:
print(msg, file=sys.stderr)
return True
else:
return False
@staticmethod
def __genID(objs: list = None, severity: str = "L"):
"""Private. Ignore."""
if objs is None:
sh = hashlib.sha256()
sh.update(str(random.randrange(999999999,999999999999999)).encode('utf-8'))
return f"{severity} 0x{sh.hexdigest()[0:8]}"
else:
e = ""
for i in objs:
if isinstance(i, str):
e += i
elif isinstance(i, int) or isinstance(i, float):
e += str(i)
elif i is None:
e += "None"
else:
e += str(type(i))
sh = hashlib.sha256()
sh.update(e.encode('utf-8'))
return f"{severity} 0x{sh.hexdigest()[0:8]}"
@staticmethod
def __getCurrentDate():
"""Private. Ignore."""
return datetime.datetime.now().strftime('%d/%m/%Y, %H:%M:%S:%f')
@staticmethod
def __getLogHeader(event_name: str = "Log", objs: list = None, severity: str = "L"):
"""Private. Ignore."""
curd = Debug.__getCurrentDate()
id = Debug.__genID(objs,severity)
return (f"{curd} {id} [{event_name}]", curd, id)
[docs] @staticmethod
def getFrameInfo():
"""Gets the caller current frame. It executes getframeinfo(currentframe().f_back)"""
return getframeinfo(currentframe().f_back)
[docs] @staticmethod
def getLocals():
"""Gets the locals from the caller perspective."""
return currentframe().f_back.f_locals
[docs] @staticmethod
def getGlobals():
"""Gets the globals"""
return currentframe().f_back.f_globals
[docs] @staticmethod
def inspectLocals(l_: dict, vars: list = None, for_id: str = ""):
""":param l_: Dictionary with the locals.
:type l_: dict
:param vars: List of variable names to inspect as strings, defaults to None
:type vars: list
:param for_id: Ignore this parameter (It is used by the class), defaults to \"\"
:type for_id: str
Use GetFrameInfo(currentframe()) to get the local variables. If vars is set to None,
it will inspect all locals.
"""
not_assigned = []
if vars is not None:
for v in vars:
if v not in l_.keys():
not_assigned.append(v)
for var,value in l_.items():
inspectthis = False
if var.startswith('__') and var.endswith('__'):
inspectthis = False
if vars is not None:
if var in vars:
inspectthis = True
else:
inspectthis = True
if inspectthis:
if isinstance(value, dict):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.debugDict(value, f"local: {var} (dict)",1)
elif (isinstance(value, tuple) or isinstance(value, list)):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.debugListorTuple(value, f"local: {var} ({str(type(value))}) for {for_id}",1)
elif isinstance(value, str):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"local: {var} (str)({len(value)}) -> \"{value[0:20]}{'...' if len(value)>=20 else ''}\"\n")
elif isinstance(value, bytes) and DUMP_BYTES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"local: {var} (bytes)({len(value)}) ->\n{Debug.hexdump(value[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif isinstance(value, io.BytesIO) and DUMP_BYTES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"local: {var} (BytesIO)({len(value.getbuffer())}) ->\n{Debug.hexdump(value.getbuffer()[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif hasattr(value, '__dict__') and var not in BANNED_CLASSES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"local: {var} ({str(type(value))}) ->\n")
Debug.debugClass(value,1)
Debug.__writelog(f"END of local: {var}\n")
else:
Debug.__writelog(f"local: {var} ({str(type(value))}) -> {str(value)}\n")
if len(not_assigned) > 0:
Debug.__writelog(f"WARNING: {len(not_assigned)} variables not assigned: {str(not_assigned)}\n")
[docs] @staticmethod
def inspectGlobals(for_id: str = "", vars: list =None):
""":for_id: Ignore this parameter. It is used by the class.
:type for_id: str
:param vars: List of variable names to inspect as strings, defaults to None
:type vars: list
If vars is set to None, it will inspect all globals.
"""
g_ = globals()
not_assigned = []
if vars is not None:
for v in vars:
if v not in g_.keys():
not_assigned.append(v)
for var,value in g_.items():
inspectthis = False
if var.startswith('__') and var.endswith('__'):
inspectthis = False
if vars is not None:
if var in vars:
inspectthis = True
else:
inspectthis = True
if inspectthis:
if isinstance(value, dict):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.debugDict(value, f"global: {var} (dict)({len(value)})",1)
elif (isinstance(value, tuple) or isinstance(value, list)):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.debugListorTuple(value, f"global: {var} ({str(type(value))})({len(value)}) for {for_id}",1)
elif isinstance(value, str):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"global: {var} (str)({len(value)}) -> \"{value[0:20]}{'...' if len(value)>=20 else ''}\"\n")
elif isinstance(value, bytes) and DUMP_BYTES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"global: {var} (bytes)({len(value)}) ->\n{Debug.hexdump(value[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif isinstance(value, io.BytesIO) and DUMP_BYTES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"global: {var} (BytesIO)({len(value.getbuffer())}) ->\n{Debug.hexdump(value.getbuffer()[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif hasattr(value, '__dict__') and var not in BANNED_CLASSES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"global: {var} ({str(type(value))}) ->\n")
Debug.debugClass(value,1)
Debug.__writelog(f"END of local: {var}\n")
else:
Debug.__writelog(f"global: {var} ({str(type(value))}) -> {str(value)}\n")
if len(not_assigned) > 0:
Debug.__writelog(f"WARNING: {len(not_assigned)} variables not assigned: {str(not_assigned)}\n")
[docs] @staticmethod
def debugLog(msg: str, name: str = ""):
""":param msg: Message to log.
:type msg: str
:param name: Name of the event to log, defaults to \"\"
:type name: str
The parameter 'name' is just for decorative purposes only.
"""
caller = getframeinfo(stack()[1].frame)
head,c,id = Debug.__getLogHeader(name, objs=[name,msg], severity="L")
if WRITE_TO_STDOUT:
print(f"DEBUG: LOG: {id}")
Debug.__writelog(f"{head} from {caller.filename} {caller.lineno}\n")
Debug.__writelog(f'{msg}\n')
return id
[docs] @staticmethod
def debugException(exc: Exception, inspect_globals: bool = False, inspect_global_vars: list = None,
inspect_locals: bool = True, inspect_local_vars: list = None) -> int:
""":param exc: The exception to debug
:type exc: Exception
:param inspect_globals: Inspect globals or not, defaults to False
:type inspect_globals: bool
:param inspect_global_vars: List of global variable names to inspect as strings, defaults to None
:type inspect_global_vars: list
:param inspect_locals: Inspect locals or not, defaults to True
:type inspect_locals: bool
:param inspect_local_vars: List of local variable names to inspect as strings, defaults to None
:type inspect_local_vars: list
When either inspect_global_vars or inspect_local_vars is set to a list of names, it will attempt
to inspect those variables. If set to None, it will inspect all variables in the global and local
scopes respectively.
:return: ID of the exception. The ID is a unique identifier for the exception, meaning that if the exception is the same, the same ID will be returned.
:rtype: int
"""
caller = getframeinfo(stack()[1].frame)
head,c,id = Debug.__getLogHeader(f"{str(type(exc))}", objs=[str(exc),caller,getframeinfo(currentframe().f_back)], severity="E")
if WRITE_TO_STDOUT:
print(f"DEBUG: EXCEPTION: {id}", file=sys.stderr)
Debug.__writelog(f"\n -- START OF EXCEPTION ({id}) -- \n{head}\n")
Debug.__writelog(traceback.format_exc()+"\n")
Debug.__writelog(f"From: {caller.filename} {caller.lineno} {caller.function}\n")
Debug.__writelog(f"Context: {caller.code_context}\n")
if inspect_globals is True:
Debug.__writelog(f"globals():\n")
Debug.inspectGlobals(id, inspect_global_vars)
if inspect_locals:
Debug.__writelog(f"locals():\n")
Debug.inspectLocals(trace()[-1][0].f_locals, inspect_local_vars, id)
Debug.__writelog("-- END OF EXCEPTION --\n\n")
return id
[docs] @staticmethod
def debugCritical(exc: Exception) -> int:
""":param exc: The exception to debug
:type exc: Exception
Call this function only for critical errors. It's the same as debugException, but it logs at critical
level 'C' and outputs the whole global and local frames as well as the loaded modules.
:return: ID of the exception. The ID is a unique identifier for the exception, meaning that if the exception is the same, the same ID will be returned.
:rtype: int
"""
caller = getframeinfo(stack()[1].frame)
head,c,id = Debug.__getLogHeader(f"{str(type(exc))}", objs=[str(exc),caller,getframeinfo(currentframe().f_back)], severity="C")
if WRITE_TO_STDOUT:
print(f"DEBUG: !!CRITICAL: {id}", file=sys.stderr)
Debug.__writelog(f"\n -- CRITICAL ERROR ({id}) -- \n{head}\n")
Debug.__writelog(traceback.format_exc()+"\n")
Debug.__writelog(f"From: {caller.filename} {caller.lineno} {caller.function}\n")
Debug.__writelog(f"Context: {caller.code_context}\n")
Debug.__writelog("Loaded Modules:")
Debug.inspectModules()
Debug.__writelog(f"globals():\n")
Debug.inspectGlobals(id)
Debug.__writelog(f"locals():\n")
Debug.inspectLocals(trace()[-1][0].f_locals, None, id)
Debug.__writelog("-- END OF CRITICAL ERROR --\n\n")
return id
[docs] @staticmethod
def debugListorTuple(_list: list or tuple, name: str = "", _rec: int = 0):
""":param _list: List or tuple to inspcect
:type _instance: list or tuple
:param name: Name of the list or tuple, optional, defaults to \"\"
:type name: str
This function will dump all items from the given list or tuple. 'name' is optional and it's only
used to give a name to the list or tuple in the log."""
head,c,id = Debug.__getLogHeader(f"{name} ({str(type(_list))})", objs=[_list,name])
spaces = " " * _rec
if _rec >= MAX_RECURSION:
Debug.__writelog(f"WARNING: Recursion limit when calling debugListorTuple. Skipping.\n")
return id
if not isinstance(_list, list) and not isinstance(_list, tuple):
Debug.__writelog(f"{spaces}{head}\n")
Debug.__writelog(f"{spaces}WARNING: Attempting to call debugListorTuple on something that is not a list or tuple.\n")
Debug.__writelog(f"{spaces}Object: {str(type(_list))} -> {'None' if _list is None else str(_list)}\n")
return id
Debug.__writelog(f"{spaces}{head}\n")
i = 0
if len(_list) < 1:
Debug.__writelog(f"{spaces}WARNING: {name} (list) is empty\n")
return id
for item in _list:
Debug.__writelog(f"{spaces}[{i}]: ({str(type(item))}) = {str(item)}\n")
i += 1
Debug.__writelog("\n")
return id
[docs] @staticmethod
def debugDict(_dict: dict, name: str = "", _rec: int = 0):
""":param _dict: Dictionary to inspect
:type _instance: dict
:param name: Name of the dictionary, optional, defaults to \"\"
:type name: str
This function will dump all key-value pairs from the given dictionary. 'name' is optional and it's only
used to give a name to the dict in the log."""
head,c,id = Debug.__getLogHeader(f"{name}", objs=[_dict,name])
spaces = " " * _rec
if _rec >= MAX_RECURSION:
Debug.__writelog(f"{spaces}WARNING: Recursion limit when calling debugDict. Skipping.\n")
return id
if not isinstance(_dict, dict):
Debug.__writelog(f"{spaces}{head}\n")
Debug.__writelog(f"{spaces}WARNING: Attempting to call debugDict on something that is not a dict.\n")
Debug.__writelog(f"{spaces}Object: {str(type(_dict))} -> {'None' if _dict is None else str(_dict)}\n")
return id
Debug.__writelog(f"{head}\n")
if len(_dict) < 1:
Debug.__writelog(f"{spaces}WARNING: {name} ({type(_dict).__name__}) is empty\n")
return id
for key, value in _dict.items():
Debug.__writelog(f"{spaces}{key} -> {value[0:20] if isinstance(value, str) else Debug.debugDict(value, key, _rec+1) if isinstance(value, dict) else str(value)}{'...' if isinstance(value,str) and len(value) > 20 else ''} -> ({str(type(value))})\n")
Debug.__writelog("\n")
return id
[docs] @staticmethod
def debugClass(_instance: object, _rec: int = 0):
""":param _instance: Object instance of any class.
:type _instance: object
Inspects an instance object, dumping all the information from it."""
head,c,id = Debug.__getLogHeader(f"instance of {type(_instance).__name__}", objs=[_instance,type(_instance).__name__])
spaces = " " * _rec
Debug.__writelog(f"{head}\n")
if _rec >= MAX_RECURSION:
Debug.__writelog(f"{spaces}WARNING: Recursion limit when calling debugClass. Skipping.\n")
return id
if type(_instance).__name__ in BANNED_CLASSES:
Debug.__writelog(f"{spaces}WARNING: Objects instances of {type(_instance).__name__} are ignored due to deep recursion.\n")
return id
if not hasattr(_instance, '__dict__'):
Debug.__writelog(f"{spaces}WARNING: Attempting to call 'debugClass' on object of type {type(_instance).__name__} (Not a class)\n")
return id
if len(_instance.__dict__) < 1:
Debug.__writelog(f"{spaces}WARNING: This instance of {type(_instance).__name__} is empty\n")
return id
Debug.__writelog(f"{spaces}Object class {type(_instance).__name__}:\n")
for var, value in _instance.__dict__.items():
Debug.__writelog(f"{spaces}{var} ({type(value).__name__}) -> {str(value)}\n")
if isinstance(value, dict):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.debugDict(value, f"{type(_instance).__name__}.{var} (dict)({len(value)})",_rec + 1)
elif isinstance(value, tuple) or isinstance(value, list):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.debugListorTuple(value, f"{type(_instance).__name__}.{var} ({str(type(value))})({len(value)}) for {id}",_rec + 1)
elif isinstance(value, str):
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"{spaces}{type(_instance).__name__}.{var} (str)({len(value)}) -> \"{value[0:20]}{'...' if len(value)>=20 else ''}\"\n")
elif isinstance(value, bytes) and DUMP_BYTES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"{spaces}{type(_instance).__name__}.{var} (bytes)({len(value)}) ->\n{Debug.hexdump(value[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif isinstance(value, io.BytesIO) and DUMP_BYTES:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"{spaces}{type(_instance).__name__}.{var} (BytesIO)({len(value.getbuffer())}) ->\n{Debug.hexdump(value.getbuffer()[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif hasattr(value, '__dict__') and var not in ['__builtins__']:
if var.startswith('__') == False and var.endswith('__') == False:
Debug.__writelog(f"{spaces}{type(_instance).__name__}.{var} ({str(type(value))}) ->\n")
Debug.debugClass(value, _rec + 1)
Debug.__writelog(f"{spaces}END of object: {type(_instance).__name__}.{var}\n")
else:
Debug.__writelog(f"{spaces}{type(_instance).__name__}.{var} ({str(type(value))}) -> {str(value)}\n")
[docs] @staticmethod
def inspectModules():
"""This function will get all loaded modules and dump them into the log file. """
head,c,id = Debug.__getLogHeader(f"loaded modules")
Debug.__writelog(f"[{head}]\n")
for var in sys.modules:
Debug.__writelog(f"{var}\n")
return id
[docs] @staticmethod
def getSystemInfo() -> dict:
"""Returns the system information as a dict containing the following information:
| ``python``: Python build version.
| ``platform``: Platform information.
| ``cpu_name``: Name of the processor architecture.
| ``cpu_count``: String containing the physical CPU count and the logical CPU count.
| ``os``: Name of the operating system.
| ``memory`` dict with the following information:
| ``virtual``: Tuple with the virtual memory information.
| ``swap``: Swap memory information
| ``used_by_python``: Memory used by the program.
| ``network``: If the machine is connected to the internet, it's set to True, False if it's not.
All units are in bytes.
**Note**: To determine if it's connected to the internet, this function makes a request to
ipecho.net.
"""
sysinfo = {}
sysinfo["python"] = platform.python_build()
sysinfo["platform"] = platform.platform()
sysinfo["cpu_name"] = platform.processor()
sysinfo["cpu_count"] = f"{psutil.cpu_count(False)} {psutil.cpu_count(True)}"
sysinfo["os"] = f"{platform.uname()}"
sysinfo["memory"] = {
"virtual": psutil.virtual_memory(),
"swap": psutil.swap_memory(),
"used_by_python": psutil.Process(os.getpid()).memory_info().rss
}
sysinfo["network"] = "connected" if requests.get("https://ipecho.net/plain").status_code == 200 else "not connected"
return sysinfo
[docs] @staticmethod
def hexdump(obj: bytes, start_offset:int = 0, end_offset:int = None, cols: int = 16,
rows: int = None, split: int = 0, headers: bool = True, offsets: bool = True,
ascii: bool = True, rawoutput: bool = False, forceasciionly = False) -> str or None:
""":param obj: Byte array to dump.
:type obj: bytes
:param start_offset: Starting offset to dump, defaults to 0
:type start_offset: int
:param end_offset: End offset to dump, defaults to None
:type end_offset: int
:param cols: Columns to dump, defaults to 16
:type cols: int
:param rows: Rows to dump, defaults to None
:type rows: int
:param split: Split the dump into several tables at this many rows, defaults to 0
:type split: int
:param headers: Print the headers or not, defaults to True
:type headers: bool
:param offsets: Print the offsets or not, defaults to True
:type offsets: bool
:param ascii: Will print the ASCII representation of the hex information, defaults to True
:type ascii: bool
:param rawoutput: Will only print a table of hex bytes and nothing else, defaults to False
:type rawoutput: bool
:param forceasciionly: Will only print printable characters (such as letters and numbers), defaults to False
:type forceasciionly: bool
:return: String containing the hex dump, or None if the function fails
:rtype: str or None
This function will return an hex dump in the style of an hex editor as a string. The parameters determine the
format and the information dumped. When dumping information, by default the offset column will always start at
0. If you want to inspect a portion of the array, using ``start_offset`` and ``end_offset`` is recommended rather than
slicing the array so that the output is accurate.
The output is in a table format in the style of hex editors. Each row represents a string of n bytes, and each
column represents a byte. The parameters ``row`` and ``cols`` determine how many rows and how many columns can be
represented. No more than 16 columns can be represented, and no less than 1 rows or columns can be represented.
The parameter ``split`` will split the output into smaller tables. It will split the table every n rows, where n
is the value of split. So if split is set to 16, it will split the table every 16 rows.
The header is the top part of the table and the offsets are at the left of the table. They can be disables with
the ``headers`` and ``offsets`` parameters if you don't want them.
The ascii representation of the data will be printed at the right of the table, and can also be disabled with the
``ascii`` parameter.
If ``rawoutput`` is set to True, it will disable split, headers, offsets, and ascii. Set this to True if you want a
raw output.
When storing hex dumps into files, enable ``forceasciionly`` to avoid non-ascii characters to be stored that could
cause issues.
"""
result = ""
if rawoutput:
split = 0
headers = False
offsets = False
ascii = False
if not isinstance(obj, bytes):
return None
if end_offset is None:
end_offset = len(obj)
if cols <= 0:
cols = 16
elif cols > 16:
cols = 16
if start_offset < 0:
start_offset = 0
elif start_offset > len(obj):
start_offset = len(obj)-cols
if end_offset < cols:
end_offset = cols
if end_offset < start_offset:
end_offset = start_offset + cols
if end_offset > len(obj):
end_offset = len(obj)
if end_offset-start_offset >= 65535:
end_offset = start_offset + 65534
if end_offset > len(obj):
end_offset = len(obj)
header = " "
if headers:
if offsets:
header = " \t "
for i in range(0,cols+1):
header += str(f"{i:1x}").capitalize() + " " if i <= cols-1 else " "
header += ("*"*cols if ascii else '')
if len(obj) < 1:
return
if rows is None:
rows = ceil(len(obj[start_offset:end_offset])/cols)
if rows > ceil(len(obj[start_offset:end_offset])/cols):
rows = ceil(len(obj[start_offset:end_offset])/cols)
if split == 0:
if headers:
result += f"{header}\n"
for r in range(0, rows):
if split != 0:
if r % split == 0:
if (start_offset+r*cols)+(split*cols)-1 <= end_offset - start_offset:
result += f"\n{start_offset+r*cols:04x}-{(start_offset+r*cols)+(split*cols)-1:04x}\n{header if headers else ''}\n"
else:
result += f"\n{(start_offset+r*cols):04x}-{end_offset-1:04x}\n{header if headers else ''}\n"
for c in range(0,cols if (r*cols)+cols < end_offset else end_offset-(r*cols)):
if c == 0:
loffset = ''
if offsets:
loffset = f'{(cols*r)+start_offset:04x}\t'
result += f"{loffset}{obj[start_offset+(c+(r*cols))]:02x} "
elif c < cols-1 and c < end_offset-(r*cols)-1:
result += f"{obj[start_offset+(c+(r*cols))]:02x} "
else:
result += f"{obj[start_offset+(c+(r*cols))]:02x} " + f'{" " * (cols-(end_offset-(r*cols)))}{"| " if ascii else ""}'
if ascii:
for cc in range (0,cols if (r*cols)+cols < end_offset else end_offset-(r*cols)):
if not forceasciionly:
if chr(obj[start_offset+(cc+(r*cols))]).isalnum():
result += f"{chr(obj[start_offset+(cc+(r*cols))])}"
else:
result += '.'
else:
if chr(obj[start_offset+(cc+(r*cols))]).isascii() and 32 < obj[start_offset+(cc+(r*cols))] < 127:
result += f"{chr(obj[start_offset+(cc+(r*cols))])}"
else:
result += '.'
result += "\n"
return result
[docs] @staticmethod
def bindump(obj: bytes, output_file: str, as_string: bool = False):
""":param obj: Object to dump
:type obj: bytes
:param output_file: File where the binary data will be output.
:type output_file: str
:param as_string: Output a string with the binary data instead of binary data, defaults to False
:type as_string: bool
This function will dump binary data into output_file. If as_string is set to True, it will instead
dump the binary data as hex by calling hexdump.
"""
if not as_string:
with open(output_file, 'wb') as f:
return f.write(obj)
else:
with open(output_file, 'w') as f:
return f.write(Debug.hexdump(obj, rawoutput=True,forceasciionly=True))
[docs] @staticmethod
def inspect(obj: object):
""":param obj: Object to inspect
:type obj: Any
Inspect an object.
"""
if isinstance(obj, dict):
Debug.debugDict(obj, f"(dict)({len(obj)})")
elif isinstance(obj, tuple) or isinstance(obj, list):
Debug.debugListorTuple(obj, f"({str(type(obj))})({len(obj)}) for {id}")
elif isinstance(obj, str):
Debug.__writelog(f"(str)({len(obj)}) -> \"{obj[0:20]}{'...' if len(obj)>=20 else ''}\"\n")
elif isinstance(obj, bytes) and DUMP_BYTES:
Debug.__writelog(f"(bytes)({len(obj)}) ->\n{Debug.hexdump(obj[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif isinstance(obj, io.BytesIO) and DUMP_BYTES:
Debug.__writelog(f"(BytesIO)({len(obj.getbuffer())}) ->\n{Debug.hexdump(obj.getbuffer()[0:DUMP_BYTES_LENGTH],headers=False, forceasciionly=True)}\n(Only the first {DUMP_BYTES_LENGTH} bytes are shown)\n")
elif hasattr(obj, '__dict__') and obj not in ['__builtins__']:
Debug.__writelog(f"(object)({str(type(obj))}) ->\n")
Debug.debugClass(obj)
Debug.__writelog(f"END of object: {type(obj).__name__}\n")
else:
Debug.__writelog(f"{type(obj).__name__} ({str(type(obj))}) -> {str(obj)}\n")