Wed, 08 Nov 2017 19:05:55 +0100
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")