diff -r f99d60d6b59b -r 2602857055c5 eric6/Debugger/VariablesViewer.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Debugger/VariablesViewer.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,846 @@ +# -*- 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")