Debugger/VariablesViewer.py

Wed, 08 Nov 2017 19:05:55 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 08 Nov 2017 19:05:55 +0100
changeset 5964
066e6c78a367
parent 5736
000ea446ff4b
child 6048
82ad8ec9548c
permissions
-rw-r--r--

Introduced a configuration option for the debugger variables viewers to limit the variables shown by the variables viewers depending on their size (in order to avoid overload situations on low power or low memory machines).

# -*- coding: utf-8 -*-

# Copyright (c) 2002 - 2017 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the variables viewer widget.
"""

from __future__ import unicode_literals
try:
    str = unicode
except NameError:
    pass

from PyQt5.QtCore import Qt, QRegExp, QCoreApplication
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, \
    QMenu

from E5Gui.E5Application import e5App

from .Config import ConfigVarTypeDispStrings

import Preferences
import Utilities
from Globals import qVersionTuple


class VariableItem(QTreeWidgetItem):
    """
    Class implementing the data structure for variable items.
    """
    Indicators = ("()", "[]", "{:}", "{}")      # __IGNORE_WARNING_M613__
    Type2Indicators = {
        # Python types
        'list': '[]',
        'tuple': '()',
        'dict': '{:}',                          # __IGNORE_WARNING_M613__
        'set': '{}',                            # __IGNORE_WARNING_M613__
        'frozenset': '{}',                      # __IGNORE_WARNING_M613__
    }
    
    def __init__(self, parent, dvar, dvalue, dtype):
        """
        Constructor
        
        @param parent reference to the parent item
        @param dvar variable name (string)
        @param dvalue value string (string)
        @param dtype type string (string)
        """
        dvar, self.__varID = VariableItem.extractId(dvar)
        
        self.__value = dvalue
        if len(dvalue) > 2048:     # 1024 * 2
            dvalue = QCoreApplication.translate(
                "VariableItem", "<double click to show value>")
            self.__tooltip = dvalue
        elif dvalue == "@@TOO_BIG_TO_SHOW@@":
            dvalue = QCoreApplication.translate(
                "VariableItem", "<variable value is too big>")
        else:
            if Qt.mightBeRichText(dvalue):
                self.__tooltip = Utilities.html_encode(dvalue)
            else:
                self.__tooltip = dvalue
            lines = dvalue.splitlines()
            if len(lines) > 1:
                # only show the first non-empty line;
                # indicate skipped lines by <...> at the
                # beginning and/or end
                index = 0
                while index < len(lines) - 1 and lines[index] == "":
                    index += 1
                dvalue = ""
                if index > 0:
                    dvalue += "<...>"
                dvalue += lines[index]
                if index < len(lines) - 1:
                    dvalue += "<...>"
        
        super(VariableItem, self).__init__(parent, [dvar, dvalue, dtype])
        
        self.populated = True
        
    def getValue(self):
        """
        Public method to return the value of the item.
        
        @return value of the item (string)
        """
        return self.__value
    
    def getId(self):
        """
        Public method to get the ID string.
        
        @return ID string
        @rtype str
        """
        return self.__varID
    
    @classmethod
    def extractId(cls, var):
        """
        Class method to extract the ID string from a variable text.
        
        @param var variable text
        @type str
        @return tuple containing the variable text without ID and the ID string
        @rtype tuple of two str
        """
        if " (ID:" in var:
            dvar, varID = var.rsplit(None, 1)
            if varID.endswith(VariableItem.Indicators):
                varID, indicators = VariableItem.extractIndicators(varID)
                dvar += indicators
        else:
            dvar = var
            varID = None
        
        return dvar, varID
    
    @classmethod
    def extractIndicators(cls, var):
        """
        Class method to extract the indicator string from a variable text.
        
        @param var variable text
        @type str
        @return tuple containing the variable text without indicators and the
            indicator string
        @rtype tuple of two str
        """
        for indicator in VariableItem.Indicators:
            if var.endswith(indicator):
                return var[:-len(indicator)], indicator
        
        return var, ""
    
    def _buildKey(self):
        """
        Protected method to build the access key for the variable.
        
        @return access key
        @rtype str
        """
        indicators = ""
        txt = self.text(0)
        if txt.endswith(VariableItem.Indicators):
            txt, indicators = VariableItem.extractIndicators(txt)
        if self.__varID:
            txt = "{0} {1}{2}".format(txt, self.__varID, indicators)
        else:
            txt = "{0}{1}".format(txt, indicators)
        return txt
    
    def data(self, column, role):
        """
        Public method to return the data for the requested role.
        
        This implementation changes the original behavior in a way, that the
        display data is returned as the tooltip for column 1.
        
        @param column column number (integer)
        @param role data role (Qt.ItemDataRole)
        @return requested data
        """
        if column == 1 and role == Qt.ToolTipRole:
            return self.__tooltip
        return super(VariableItem, self).data(column, role)
        
    def attachDummy(self):
        """
        Public method to attach a dummy sub item to allow for lazy population.
        """
        QTreeWidgetItem(self, ["DUMMY"])
        
    def deleteChildren(self):
        """
        Public method to delete all children (cleaning the subtree).
        """
        for itm in self.takeChildren():
            del itm
        
    def expand(self):
        """
        Public method to expand the item.
        
        Note: This is just a do nothing and should be overwritten.
        """
        return
        
    def collapse(self):
        """
        Public method to collapse the item.
        
        Note: This is just a do nothing and should be overwritten.
        """
        return


class SpecialVarItem(VariableItem):
    """
    Class implementing a VariableItem that represents a special variable node.
    
    These special variable nodes are generated for classes, lists,
    tuples and dictionaries.
    """
    def __init__(self, parent, dvar, dvalue, dtype, frmnr, globalScope):
        """
        Constructor
        
        @param parent parent of this item
        @param dvar variable name (string)
        @param dvalue value string (string)
        @param dtype type string (string)
        @param frmnr frame number (0 is the current frame) (int)
        @param globalScope flag indicating global (True) or local (False)
            variables
        """
        VariableItem.__init__(self, parent, dvar, dvalue, dtype)
        self.attachDummy()
        self.populated = False
        
        self.framenr = frmnr
        self.globalScope = globalScope

    def expand(self):
        """
        Public method to expand the item.
        """
        self.deleteChildren()
        self.populated = True
        
        pathlist = [self._buildKey()]
        par = self.parent()
        
        # step 1: get a pathlist up to the requested variable
        while par is not None:
            pathlist.insert(0, par._buildKey())
            par = par.parent()
        
        # step 2: request the variable from the debugger
        variablesFilter = e5App().getObject("DebugUI").variablesFilter(
            self.globalScope)
        e5App().getObject("DebugServer").remoteClientVariable(
            self.globalScope, variablesFilter, pathlist, self.framenr)


class ArrayElementVarItem(VariableItem):
    """
    Class implementing a VariableItem that represents an array element.
    """
    def __init__(self, parent, dvar, dvalue, dtype):
        """
        Constructor
        
        @param parent parent of this item
        @param dvar variable name (string)
        @param dvalue value string (string)
        @param dtype type string (string)
        """
        VariableItem.__init__(self, parent, dvar, dvalue, dtype)
        
        """
        Array elements have numbers as names, but the key must be
        right justified and zero filled to 6 decimal places. Then
        element 2 will have a key of '000002' and appear before
        element 10 with a key of '000010'
        """
        col0Str = self.text(0)
        self.setText(0, "{0:6d}".format(int(col0Str)))


class SpecialArrayElementVarItem(SpecialVarItem):
    """
    Class implementing a QTreeWidgetItem that represents a special array
    variable node.
    """
    def __init__(self, parent, dvar, dvalue, dtype, frmnr, globalScope):
        """
        Constructor
        
        @param parent parent of this item
        @param dvar variable name (string)
        @param dvalue value string (string)
        @param dtype type string (string)
        @param frmnr frame number (0 is the current frame) (int)
        @param globalScope flag indicating global (True) or local (False)
            variables
        """
        SpecialVarItem.__init__(self, parent, dvar, dvalue, dtype, frmnr,
                                globalScope)
        
        """
        Array elements have numbers as names, but the key must be
        right justified and zero filled to 6 decimal places. Then
        element 2 will have a key of '000002' and appear before
        element 10 with a key of '000010'
        """
        # strip off [], () or {}
        col0Str, indicators = VariableItem.extractIndicators(self.text(0))
        self.setText(0, "{0:6d}{1}".format(int(col0Str), indicators))


class VariablesViewer(QTreeWidget):
    """
    Class implementing the variables viewer widget.
    
    This widget is used to display the variables of the program being
    debugged in a tree. Compound types will be shown with
    their main entry first. Once the subtree has been expanded, the
    individual entries will be shown. Double clicking an entry will
    popup a dialog showing the variables parameters in a more readable
    form. This is especially useful for lengthy strings.
    
    This widget has two modes for displaying the global and the local
    variables.
    """
    def __init__(self, viewer, globalScope, parent=None):
        """
        Constructor
        
        @param viewer reference to the debug viewer object (DebugViewer)
        @param globalScope flag indicating global (True) or local (False)
            variables
        @param parent the parent (QWidget)
        """
        super(VariablesViewer, self).__init__(parent)
        
        self.__debugViewer = viewer
        self.__globalScope = globalScope
        
        indicatorPattern = "|".join([QRegExp.escape(indicator)
                                     for indicator in VariableItem.Indicators])
        self.rx_class = QRegExp('<.*(instance|object) at 0x.*>')
        self.rx_class2 = QRegExp('class .*')
        self.rx_class3 = QRegExp('<class .* at 0x.*>')
        self.dvar_rx_class1 = QRegExp(
            r'<.*(instance|object) at 0x.*>({0})'.format(indicatorPattern))
        self.dvar_rx_class2 = QRegExp(
            r'<class .* at 0x.*>({0})'.format(indicatorPattern))
        self.dvar_rx_array_element = QRegExp(r'^\d+$')
        self.dvar_rx_special_array_element = QRegExp(
            r'^\d+({0})$'.format(indicatorPattern))
        self.rx_nonprintable = QRegExp(r"""(\\x\d\d)+""")
        
        self.framenr = 0
        
        self.loc = Preferences.getSystem("StringEncoding")
        
        self.openItems = []
        
        self.setRootIsDecorated(True)
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        
        if self.__globalScope:
            self.setWindowTitle(self.tr("Global Variables"))
            self.setHeaderLabels([
                self.tr("Globals"),
                self.tr("Value"),
                self.tr("Type")])
            self.setWhatsThis(self.tr(
                """<b>The Global Variables Viewer Window</b>"""
                """<p>This window displays the global variables"""
                """ of the debugged program.</p>"""
            ))
        else:
            self.setWindowTitle(self.tr("Local Variables"))
            self.setHeaderLabels([
                self.tr("Locals"),
                self.tr("Value"),
                self.tr("Type")])
            self.setWhatsThis(self.tr(
                """<b>The Local Variables Viewer Window</b>"""
                """<p>This window displays the local variables"""
                """ of the debugged program.</p>"""
            ))
        
        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        if qVersionTuple() >= (5, 0, 0):
            header.setSectionsClickable(True)
        else:
            header.setClickable(True)
        header.sectionClicked.connect(self.__sectionClicked)
        header.resizeSection(0, 120)    # variable column
        header.resizeSection(1, 150)    # value column
        
        self.__createPopupMenus()
        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        
        self.itemExpanded.connect(self.__expandItemSignal)
        self.itemCollapsed.connect(self.collapseItem)
        
        self.resortEnabled = True
        
    def __createPopupMenus(self):
        """
        Private method to generate the popup menus.
        """
        self.menu = QMenu()
        self.menu.addAction(self.tr("Show Details..."), self.__showDetails)
        self.menu.addAction(self.tr("Refresh"), self.__refreshView)
        self.menu.addSeparator()
        self.menu.addAction(self.tr("Configure..."), self.__configure)
        
        self.backMenu = QMenu()
        self.backMenu.addAction(self.tr("Refresh"), self.__refreshView)
        self.backMenu.addSeparator()
        self.backMenu.addAction(self.tr("Configure..."), self.__configure)
        
    def __showContextMenu(self, coord):
        """
        Private slot to show the context menu.
        
        @param coord the position of the mouse pointer (QPoint)
        """
        gcoord = self.mapToGlobal(coord)
        if self.itemAt(coord) is not None:
            self.menu.popup(gcoord)
        else:
            self.backMenu.popup(gcoord)
        
    def __findItem(self, slist, column, node=None):
        """
        Private method to search for an item.
        
        It is used to find a specific item in column,
        that is a child of node. If node is None, a child of the
        QTreeWidget is searched.
        
        @param slist searchlist (list of strings)
        @param column index of column to search in (int)
        @param node start point of the search
        @return the found item or None
        """
        if node is None:
            count = self.topLevelItemCount()
        else:
            count = node.childCount()
        
        if column == 0:
            searchStr = VariableItem.extractId(slist[0])[0]
        else:
            searchStr = slist[0]
        
        for index in range(count):
            if node is None:
                itm = self.topLevelItem(index)
            else:
                itm = node.child(index)
            if itm.text(column) == searchStr:
                if len(slist) > 1:
                    itm = self.__findItem(slist[1:], column, itm)
                return itm
        
        return None
        
    def showVariables(self, vlist, frmnr):
        """
        Public method to show variables in a list.
        
        @param vlist the list of variables to be displayed. Each
                listentry is a tuple of three values.
                <ul>
                <li>the variable name (string)</li>
                <li>the variables type (string)</li>
                <li>the variables value (string)</li>
                </ul>
        @param frmnr frame number (0 is the current frame) (int)
        """
        self.current = self.currentItem()
        if self.current:
            self.curpathlist = self.__buildTreePath(self.current)
        self.clear()
        self.__scrollToItem = None
        self.framenr = frmnr
        
        if len(vlist):
            self.resortEnabled = False
            for (var, vtype, value) in vlist:
                self.__addItem(None, vtype, var, value)
            
            # re-expand tree
            openItems = sorted(self.openItems[:])
            self.openItems = []
            for itemPath in openItems:
                itm = self.__findItem(itemPath, 0)
                if itm is not None:
                    self.expandItem(itm)
                else:
                    self.openItems.append(itemPath)
            
            if self.current:
                citm = self.__findItem(self.curpathlist, 0)
                if citm:
                    self.setCurrentItem(citm)
                    citm.setSelected(True)
                    self.scrollToItem(citm, QAbstractItemView.PositionAtTop)
                    self.current = None
            
            self.resortEnabled = True
            self.__resort()

    def showVariable(self, vlist):
        """
        Public method to show variables in a list.
        
        @param vlist the list of subitems to be displayed.
                The first element gives the path of the
                parent variable. Each other listentry is
                a tuple of three values.
                <ul>
                <li>the variable name (string)</li>
                <li>the variables type (string)</li>
                <li>the variables value (string)</li>
                </ul>
        """
        resortEnabled = self.resortEnabled
        self.resortEnabled = False
        if self.current is None:
            self.current = self.currentItem()
            if self.current:
                self.curpathlist = self.__buildTreePath(self.current)
        
        if vlist:
            itm = self.__findItem(vlist[0], 0)
            for var, vtype, value in vlist[1:]:
                self.__addItem(itm, vtype, var, value)

        # re-expand tree
        openItems = sorted(self.openItems[:])
        self.openItems = []
        for itemPath in openItems:
            itm = self.__findItem(itemPath, 0)
            if itm is not None and not itm.isExpanded():
                if itm.populated:
                    self.blockSignals(True)
                    itm.setExpanded(True)
                    self.blockSignals(False)
                else:
                    self.expandItem(itm)
        self.openItems = openItems[:]
            
        if self.current:
            citm = self.__findItem(self.curpathlist, 0)
            if citm:
                self.setCurrentItem(citm)
                citm.setSelected(True)
                if self.__scrollToItem:
                    self.scrollToItem(self.__scrollToItem,
                                      QAbstractItemView.PositionAtTop)
                else:
                    self.scrollToItem(citm, QAbstractItemView.PositionAtTop)
                self.current = None
        elif self.__scrollToItem:
            self.scrollToItem(self.__scrollToItem,
                              QAbstractItemView.PositionAtTop)
        
        self.resortEnabled = resortEnabled
        self.__resort()

    def __generateItem(self, parent, dvar, dvalue, dtype, isSpecial=False):
        """
        Private method used to generate a VariableItem.
        
        @param parent parent of the item to be generated
        @param dvar variable name (string)
        @param dvalue value string (string)
        @param dtype type string (string)
        @param isSpecial flag indicating that a special node should be
            generated (boolean)
        @return The item that was generated (VariableItem).
        """
        if isSpecial and \
           (self.dvar_rx_class1.exactMatch(dvar) or
                self.dvar_rx_class2.exactMatch(dvar)):
            isSpecial = False
        
        if self.rx_class2.exactMatch(dtype):
            return SpecialVarItem(
                parent, dvar, dvalue, dtype[7:-1], self.framenr,
                self.__globalScope)
        elif dtype != "void *" and \
            (self.rx_class.exactMatch(dvalue) or
             self.rx_class3.exactMatch(dvalue) or
             isSpecial):
            if self.dvar_rx_special_array_element.exactMatch(dvar):
                return SpecialArrayElementVarItem(
                    parent, dvar, dvalue, dtype, self.framenr,
                    self.__globalScope)
            else:
                return SpecialVarItem(parent, dvar, dvalue, dtype,
                                      self.framenr, self.__globalScope)
        elif dtype in ["numpy.ndarray", "django.MultiValueDict",
                       "array.array"]:
            return SpecialVarItem(
                parent, dvar, self.tr("{0} items").format(dvalue), dtype,
                self.framenr, self.__globalScope)
        else:
            if self.dvar_rx_array_element.exactMatch(dvar):
                return ArrayElementVarItem(parent, dvar, dvalue, dtype)
            else:
                return VariableItem(parent, dvar, dvalue, dtype)
        
    def __addItem(self, parent, vtype, var, value):
        """
        Private method used to add an item to the list.
        
        If the item is of a type with subelements (i.e. list, dictionary,
        tuple), these subelements are added by calling this method recursively.
        
        @param parent the parent of the item to be added
            (QTreeWidgetItem or None)
        @param vtype the type of the item to be added
            (string)
        @param var the variable name (string)
        @param value the value string (string)
        @return The item that was added to the listview (QTreeWidgetItem).
        """
        if parent is None:
            parent = self
        try:
            dvar = '{0}{1}'.format(var, VariableItem.Type2Indicators[vtype])
        except KeyError:
            dvar = var
        dvtype = self.__getDispType(vtype)
        
        if vtype in ['list', 'tuple', 'dict', 'set', 'frozenset']:
            itm = self.__generateItem(parent, dvar,
                                      self.tr("{0} items").format(value),
                                      dvtype, True)
        elif vtype in ['unicode', 'str']:
            if self.rx_nonprintable.indexIn(value) != -1:
                sval = value
            else:
                try:
                    sval = eval(value)
                except Exception:
                    sval = value
            itm = self.__generateItem(parent, dvar, str(sval), dvtype)
        
        else:
            itm = self.__generateItem(parent, dvar, value, dvtype)
            
        return itm

    def __getDispType(self, vtype):
        """
        Private method used to get the display string for type vtype.
        
        @param vtype the type, the display string should be looked up for
              (string)
        @return displaystring (string)
        """
        try:
            dvtype = self.tr(ConfigVarTypeDispStrings[vtype])
        except KeyError:
            if vtype == 'classobj':
                dvtype = self.tr(ConfigVarTypeDispStrings['instance'])
            else:
                dvtype = vtype
        return dvtype
    
    def __refreshView(self):
        """
        Private slot to refresh the view.
        """
        if self.__globalScope:
            self.__debugViewer.setGlobalsFilter()
        else:
            self.__debugViewer.setLocalsFilter()
    
    def mouseDoubleClickEvent(self, mouseEvent):
        """
        Protected method of QAbstractItemView.
        
        Reimplemented to disable expanding/collapsing of items when
        double-clicking. Instead the double-clicked entry is opened.
        
        @param mouseEvent the mouse event object (QMouseEvent)
        """
        itm = self.itemAt(mouseEvent.pos())
        self.__showVariableDetails(itm)
        
    def __showDetails(self):
        """
        Private slot to show details about the selected variable.
        """
        itm = self.currentItem()
        self.__showVariableDetails(itm)
        
    def __showVariableDetails(self, itm):
        """
        Private method to show details about a variable.
        
        @param itm reference to the variable item
        """
        if itm is None:
            return
        
        val = itm.getValue()
        
        if not val:
            return  # do not display anything, if the variable has no value
            
        vtype = itm.text(2)
        name = VariableItem.extractIndicators(itm.text(0).strip())[0]
        
        par = itm.parent()
        if name.startswith("["):    # numpy.ndarray, array.array
            nlist = []
        else:
            nlist = [name]
        
        # build up the fully qualified name
        while par is not None:
            pname, indicators = VariableItem.extractIndicators(
                par.text(0).strip())
            if indicators:
                if nlist[0].endswith("."):
                    nlist[0] = '[{0}].'.format(nlist[0][:-1])
                else:
                    nlist[0] = '[{0}]'.format(nlist[0])
                if not pname.startswith("["):   # numpy.ndarray, array.array
                    nlist.insert(0, pname)
            else:
                if par.text(2) == "django.MultiValueDict":
                    nlist[0] = 'getlist({0})'.format(nlist[0])
                elif par.text(2) == "numpy.ndarray":
                    if nlist[0][0].isalpha():
                        if nlist[0] in ["min", "max", "mean"]:
                            nlist[0] = ".{0}()".format(nlist[0])
                        else:
                            nlist[0] = ".{0}".format(nlist[0])
                    nlist.insert(0, pname)
                else:
                    nlist.insert(0, '{0}.'.format(pname))
            par = par.parent()
            
        name = ''.join(nlist)
        # now show the dialog
        from .VariableDetailDialog import VariableDetailDialog
        dlg = VariableDetailDialog(name, vtype, val)
        dlg.exec_()
    
    def __buildTreePath(self, itm):
        """
        Private method to build up a path from the top to an item.
        
        @param itm item to build the path for (QTreeWidgetItem)
        @return list of names denoting the path from the top (list of strings)
        """
        name = itm.text(0)
        pathlist = [name]
        
        par = itm.parent()
        # build up a path from the top to the item
        while par is not None:
            pname = par.text(0)
            pathlist.insert(0, pname)
            par = par.parent()
        
        return pathlist[:]
    
    def __expandItemSignal(self, parentItem):
        """
        Private slot to handle the expanded signal.
        
        @param parentItem reference to the item being expanded
            (QTreeWidgetItem)
        """
        self.expandItem(parentItem)
        self.__scrollToItem = parentItem
        
    def expandItem(self, parentItem):
        """
        Public slot to handle the expanded signal.
        
        @param parentItem reference to the item being expanded
            (QTreeWidgetItem)
        """
        pathlist = self.__buildTreePath(parentItem)
        self.openItems.append(pathlist)
        if parentItem.populated:
            return
        
        try:
            parentItem.expand()
        except AttributeError:
            super(VariablesViewer, self).expandItem(parentItem)

    def collapseItem(self, parentItem):
        """
        Public slot to handle the collapsed signal.
        
        @param parentItem reference to the item being collapsed
            (QTreeWidgetItem)
        """
        pathlist = self.__buildTreePath(parentItem)
        self.openItems.remove(pathlist)
        
        try:
            parentItem.collapse()
        except AttributeError:
            super(VariablesViewer, self).collapseItem(parentItem)
    
    def __sectionClicked(self):
        """
        Private method handling a click onto a header section.
        """
        self.__resort()
    
    def __resort(self, parent=None):
        """
        Private method to resort the tree.
        
        @param parent reference to a parent item
        @type QTreeWidgetItem
        """
        if self.resortEnabled:
            if parent is not None:
                parent.sortChildren(self.sortColumn(),
                                    self.header().sortIndicatorOrder())
            else:
                self.sortItems(self.sortColumn(),
                               self.header().sortIndicatorOrder())
    
    def handleResetUI(self):
        """
        Public method to reset the VariablesViewer.
        """
        self.clear()
        self.openItems = []
    
    def __configure(self):
        """
        Private method to open the configuration dialog.
        """
        e5App().getObject("UserInterface")\
            .showPreferences("debuggerGeneralPage")

eric ide

mercurial