diff -r d23e9854aea4 -r 18a7312cfdb3 src/eric7/DebugClients/Python/DebugVariables.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/DebugClients/Python/DebugVariables.py Sun Jul 24 11:29:56 2022 +0200 @@ -0,0 +1,860 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016 - 2022 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) + """ + yield from 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.. + with contextlib.suppress(Exception): + 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))) + + 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