Thu, 10 Jan 2019 14:18:48 +0100
Updated copyright for 2019.
# -*- coding: utf-8 -*- # Copyright (c) 2002 - 2019 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")