Sun, 29 Aug 2021 19:19:31 +0200
Redesign of the internal evaluation of the variable types.
# -*- coding: utf-8 -*- # Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing classes and functions to dump variable contents. """ import contextlib import sys from collections.abc import ItemsView, KeysView, ValuesView from DebugConfig import ConfigQtNames, ConfigKnownQtTypes, BatchSize # # This code was inspired by pydevd. # ############################################################ ## Classes implementing resolvers for various compound types ############################################################ class BaseResolver: """ Base class of the resolver class tree. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type any @param attribute name of the attribute to extract @type str @return value of the attribute @rtype any """ return getattr(var, attribute, None) def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @return list containing the variable attributes @rtype list """ d = [] for name in dir(var): with contextlib.suppress(Exception): attribute = getattr(var, name) d.append((name, attribute)) return d ############################################################ ## Default Resolver ############################################################ class DefaultResolver(BaseResolver): """ Class used to resolve the default way. """ def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] for name in dir(var): with contextlib.suppress(Exception): attribute = getattr(var, name) d.append((name, attribute)) yield -1, d while True: yield -2, [] ############################################################ ## Resolver for Dictionaries ############################################################ class DictResolver(BaseResolver): """ Class used to resolve from a dictionary. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type dict @param attribute name of the attribute to extract @type str @return value of the attribute @rtype any """ if " (ID:" not in attribute: try: return var[attribute] except Exception: return getattr(var, attribute, None) expectedID = int(attribute.split(" (ID:")[-1][:-1]) for key, value in var.items(): if id(key) == expectedID: return value return None def keyToStr(self, key): """ Public method to get a string representation for a key. @param key key to be converted @type any @return string representation of the given key @rtype str """ if isinstance(key, str): key = repr(key) # Special handling for bytes object # Raw and f-Strings are always converted to str if key[0] == 'b': key = key[1:] return key # __IGNORE_WARNING_M834__ def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] start = count = 0 allItems = list(var.items()) try: # Fast path: all items from same type allItems.sort(key=lambda x: x[0]) except TypeError: # Slow path: only sort items with same type (Py3 only) allItems.sort(key=lambda x: (str(x[0]), x[0])) for key, value in allItems: key = "{0} (ID:{1})".format(self.keyToStr(key), id(key)) d.append((key, value)) count += 1 if count >= BatchSize: yield start, d start += count count = 0 d = [] if d: yield start, d # in case it has additional fields d = super().getVariableList(var) yield -1, d while True: yield -2, [] ############################################################ ## Resolver for Lists and Tuples ############################################################ class ListResolver(BaseResolver): """ Class used to resolve from a tuple or list. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type tuple or list @param attribute name of the attribute to extract @type str @return value of the attribute @rtype any """ try: return var[int(attribute)] except Exception: return getattr(var, str(attribute), None) def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] start = count = 0 for idx, value in enumerate(var): d.append((idx, value)) count += 1 if count >= BatchSize: yield start, d start = idx + 1 count = 0 d = [] if d: yield start, d # in case it has additional fields d = super().getVariableList(var) yield -1, d while True: yield -2, [] ############################################################ ## Resolver for dict_items, dict_keys and dict_values ############################################################ class DictViewResolver(ListResolver): """ Class used to resolve from dict views. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type dict_items, dict_keys or dict_values @param attribute id of the value to extract @type str @return value of the attribute @rtype any """ return super().resolve(list(var), attribute) def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ return super().getVariableList(list(var)) ############################################################ ## Resolver for Sets and Frozensets ############################################################ class SetResolver(BaseResolver): """ Class used to resolve from a set or frozenset. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type tuple or list @param attribute id of the value to extract @type str @return value of the attribute @rtype any """ if attribute.startswith("'ID: "): attribute = attribute.split(None, 1)[1][:-1] try: attribute = int(attribute) except Exception: return getattr(var, attribute, None) for v in var: if id(v) == attribute: return v return None def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] start = count = 0 for value in var: count += 1 d.append(("'ID: {0}'".format(id(value)), value)) if count >= BatchSize: yield start, d start += count count = 0 d = [] if d: yield start, d # in case it has additional fields d = super().getVariableList(var) yield -1, d while True: yield -2, [] ############################################################ ## Resolver for Numpy Arrays ############################################################ class NdArrayResolver(BaseResolver): """ Class used to resolve from numpy ndarray including some meta data. """ def __isNumeric(self, arr): """ Private method to check, if an array is of a numeric type. @param arr array to check @type ndarray @return flag indicating a numeric array @rtype bool """ try: return arr.dtype.kind in 'biufc' except AttributeError: return False def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type ndarray @param attribute id of the value to extract @type str @return value of the attribute @rtype any """ if attribute == 'min': if self.__isNumeric(var): return var.min() else: return None if attribute == 'max': if self.__isNumeric(var): return var.max() else: return None if attribute == 'mean': if self.__isNumeric(var): return var.mean() else: return None try: return var[int(attribute)] except Exception: return getattr(var, attribute, None) return None def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] start = count = 0 try: len(var) # Check if it's an unsized object, e.g. np.ndarray(()) allItems = var.tolist() except TypeError: # TypeError: len() of unsized object allItems = [] for idx, value in enumerate(allItems): d.append((str(idx), value)) count += 1 if count >= BatchSize: yield start, d start += count count = 0 d = [] if d: yield start, d # in case it has additional fields d = super().getVariableList(var) if var.size > 1024 * 1024: d.append(('min', 'ndarray too big, calculating min would slow down debugging')) d.append(('max', 'ndarray too big, calculating max would slow down debugging')) d.append(('mean', 'ndarray too big, calculating mean would slow down debugging')) elif self.__isNumeric(var): if var.size == 0: d.append(('min', 'empty array')) d.append(('max', 'empty array')) d.append(('mean', 'empty array')) else: d.append(('min', var.min())) d.append(('max', var.max())) d.append(('mean', var.mean())) else: d.append(('min', 'not a numeric object')) d.append(('max', 'not a numeric object')) d.append(('mean', 'not a numeric object')) yield -1, d while True: yield -2, [] ############################################################ ## Resolver for Django Multi Value Dictionaries ############################################################ class MultiValueDictResolver(DictResolver): """ Class used to resolve from Django multi value dictionaries. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type MultiValueDict @param attribute name of the attribute to extract @type str @return value of the attribute @rtype any """ if " (ID:" not in attribute: try: return var[attribute] except Exception: return getattr(var, attribute, None) expectedID = int(attribute.split(" (ID:")[-1][:-1]) for key in var: if id(key) == expectedID: return var.getlist(key) return None def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] start = count = 0 allKeys = list(var.keys()) try: # Fast path: all items from same type allKeys.sort() except TypeError: # Slow path: only sort items with same type (Py3 only) allKeys.sort(key=lambda x: (str(x), x)) for key in allKeys: dkey = "{0} (ID:{1})".format(self.keyToStr(key), id(key)) d.append((dkey, var.getlist(key))) count += 1 if count >= BatchSize: yield start, d start += count count = 0 d = [] if d: yield start, d # in case it has additional fields d = super(DictResolver, self).getVariableList(var) yield -1, d while True: yield -2, [] ############################################################ ## Resolver for array.array ############################################################ class ArrayResolver(BaseResolver): """ Class used to resolve from array.array including some meta data. """ TypeCodeMap = { "b": "int (signed char)", "B": "int (unsigned char)", "u": "Unicode character (Py_UNICODE)", "h": "int (signed short)", "H": "int (unsigned short)", "i": "int (signed int)", "I": "int (unsigned int)", "l": "int (signed long)", "L": "int (unsigned long)", "q": "int (signed long long)", "Q": "int (unsigned long long)", "f": "float (float)", "d": "float (double)", } def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type array.array @param attribute id of the value to extract @type str @return value of the attribute @rtype any """ try: return var[int(attribute)] except Exception: return getattr(var, attribute, None) return None def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] start = count = 0 allItems = var.tolist() for idx, value in enumerate(allItems): d.append((str(idx), value)) count += 1 if count >= BatchSize: yield start, d start += count count = 0 d = [] if d: yield start, d # in case it has additional fields d = super().getVariableList(var) # Special data for array type: convert typecode to readable text d.append(('type', self.TypeCodeMap.get(var.typecode, 'illegal type'))) yield -1, d while True: yield -2, [] ############################################################ ## PySide / PyQt Resolver ############################################################ class QtResolver(BaseResolver): """ Class used to resolve the Qt implementations. """ def resolve(self, var, attribute): """ Public method to get an attribute from a variable. @param var variable to extract an attribute or value from @type Qt objects @param attribute name of the attribute to extract @type str @return value of the attribute @rtype any """ if attribute == 'internalPointer': return var.internalPointer() return getattr(var, attribute, None) def getVariableList(self, var): """ Public method to get the attributes of a variable as a list. @param var variable to be converted @type any @yield tuple containing the batch start index and a list containing the variable attributes @ytype tuple of (int, list) """ d = [] attributes = () # Gently handle exception which could occure as special # cases, e.g. already deleted C++ objects, str conversion.. try: qttype = type(var).__name__ if qttype in ('QLabel', 'QPushButton'): attributes = ('text', ) elif qttype == 'QByteArray': d.append(('bytes', bytes(var))) d.append(('hex', "QByteArray", "{0}".format(var.toHex()))) d.append(('base64', "QByteArray", "{0}".format(var.toBase64()))) d.append(('percent encoding', "QByteArray", "{0}".format(var.toPercentEncoding()))) elif qttype in ('QPoint', 'QPointF'): attributes = ('x', 'y') elif qttype in ('QRect', 'QRectF'): attributes = ('x', 'y', 'width', 'height') elif qttype in ('QSize', 'QSizeF'): attributes = ('width', 'height') elif qttype == 'QColor': attributes = ('name', ) r, g, b, a = var.getRgb() d.append( ('rgba', "{0:d}, {1:d}, {2:d}, {3:d}".format(r, g, b, a)) ) h, s, v, a = var.getHsv() d.append( ('hsva', "{0:d}, {1:d}, {2:d}, {3:d}".format(h, s, v, a)) ) c, m, y, k, a = var.getCmyk() d.append( ('cmyka', "{0:d}, {1:d}, {2:d}, {3:d}, {4:d}".format(c, m, y, k, a)) ) elif qttype in ('QDate', 'QTime', 'QDateTime'): d.append((qttype[1:].lower(), var.toString())) elif qttype == 'QDir': attributes = ('path', 'absolutePath', 'canonicalPath') elif qttype == 'QFile': attributes = ('fileName', ) elif qttype == 'QFont': attributes = ( 'family', 'pointSize', 'weight', 'bold', 'italic' ) elif qttype == 'QUrl': d.append(('url', var.toString())) attributes = ('scheme', 'userName', 'password', 'host', 'port', 'path') elif qttype == 'QModelIndex': valid = var.isValid() d.append(('valid', valid)) if valid: d.append(("internalPointer", var.internalPointer())) attributes = ('row', 'column', 'internalId') elif qttype in ('QRegExp', "QRegularExpression"): attributes = ('pattern', ) # GUI stuff elif qttype == 'QAction': d.append(('shortcut', var.shortcut().toString())) attributes = ('objectName', 'text', 'iconText', 'toolTip', 'whatsThis') elif qttype == 'QKeySequence': d.append(('keySequence', var.toString())) # XML stuff elif qttype == 'QDomAttr': attributes = ('name', 'var') elif qttype in ('QDomCharacterData', 'QDomComment', 'QDomText'): attributes = ('data', ) elif qttype == 'QDomDocument': d.append(('text', var.toString())) elif qttype == 'QDomElement': attributes = ('tagName', 'text') # Networking stuff elif qttype == 'QHostAddress': d.append(('address', var.toString())) # PySide specific elif qttype == 'EnumType': # Not in PyQt possible for key, value in var.values.items(): d.append((key, int(value))) except Exception: pass for attribute in attributes: d.append((attribute, getattr(var, attribute)())) # add additional fields if qttype != 'EnumType': d.extend(super().getVariableList(var)) yield -1, d while True: yield -2, [] defaultResolver = DefaultResolver() dictResolver = DictResolver() listResolver = ListResolver() dictViewResolver = DictViewResolver() setResolver = SetResolver() ndarrayResolver = NdArrayResolver() multiValueDictResolver = MultiValueDictResolver() arrayResolver = ArrayResolver() qtResolver = QtResolver() ############################################################ ## Methods to determine the type of a variable and the ## resolver class to use ############################################################ _TypeMap = _ArrayTypes = None _TryArray = _TryNumpy = _TryDjango = True _MapCount = 0 def _initTypeMap(): """ Protected function to initialize the type map. """ global _TypeMap # Type map for special handling of array types. # All other types not listed here use the default resolver. _TypeMap = [ (tuple, listResolver), (list, listResolver), (dict, dictResolver), (set, setResolver), (frozenset, setResolver), (ItemsView, dictViewResolver), # Since Python 3.0 (KeysView, dictViewResolver), (ValuesView, dictViewResolver), ] # Initialize the static type map _initTypeMap() def updateTypeMap(): """ Public function to update the type map based on module imports. """ global _TypeMap, _ArrayTypes, _TryArray, _TryNumpy, _TryDjango, _MapCount # array.array may not be imported (yet) if _TryArray and 'array' in sys.modules: import array _TypeMap.append((array.array, arrayResolver)) _TryArray = False # numpy may not be imported (yet) if _TryNumpy and 'numpy' in sys.modules: import numpy _TypeMap.append((numpy.ndarray, ndarrayResolver)) _TryNumpy = False # django may not be imported (yet) if _TryDjango and 'django' in sys.modules: from django.utils.datastructures import MultiValueDict # it should go before dict _TypeMap.insert(0, (MultiValueDict, multiValueDictResolver)) _TryDjango = False # If _TypeMap changed, rebuild the _ArrayTypes tuple if _MapCount != len(_TypeMap): _ArrayTypes = tuple(typ for typ, _resolver in _TypeMap) _MapCount = len(_TypeMap) def getResolver(obj): """ Public method to get the resolver based on the type info of an object. @param obj object to get resolver for @type any @return resolver @rtype BaseResolver """ # Between PyQt and PySide the returned type is different (class vs. type) typeStr = str(type(obj)).split(' ', 1)[-1] typeStr = typeStr[1:-2] if ( typeStr.startswith(ConfigQtNames) and typeStr.endswith(ConfigKnownQtTypes) ): return qtResolver for typeData, resolver in _TypeMap: # __IGNORE_WARNING_M507__ if isinstance(obj, typeData): return resolver return defaultResolver # # eflag: noqa = Y113