src/eric7/DebugClients/Python/DebugVariables.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
--- /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

eric ide

mercurial