--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/DebugClients/Python/DebugVariables.py Thu Jul 07 11:23:56 2022 +0200 @@ -0,0 +1,861 @@ +# -*- 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