Source code for Debug

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")