diff -r 7c3968742fd2 -r 9aad21c7765d eric6/UI/PythonDisViewer.py --- a/eric6/UI/PythonDisViewer.py Tue Sep 22 19:03:19 2020 +0200 +++ b/eric6/UI/PythonDisViewer.py Tue Sep 22 19:30:03 2020 +0200 @@ -17,12 +17,13 @@ from PyQt5.QtCore import pyqtSlot, Qt, QTimer from PyQt5.QtGui import QCursor, QBrush from PyQt5.QtWidgets import ( - QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget, - QVBoxLayout, QLabel, QMenu + QApplication, QTreeWidgetItem, QAbstractItemView, QWidget, QMenu ) import Preferences +from .Ui_PythonDisViewer import Ui_PythonDisViewer + class PythonDisViewerModes(enum.Enum): """ @@ -32,13 +33,14 @@ TracebackMode = 1 -class PythonDisViewer(QWidget): +class PythonDisViewer(QWidget, Ui_PythonDisViewer): """ Class implementing a widget to visualize the Python Disassembly for some Python sources. """ StartLineRole = Qt.UserRole EndLineRole = Qt.UserRole + 1 + CodeInfoRole = Qt.UserRole + 2 def __init__(self, viewmanager, mode=PythonDisViewerModes.SourceDisassemblyMode, @@ -54,19 +56,7 @@ @type QWidget """ super(PythonDisViewer, self).__init__(parent) - - self.__layout = QVBoxLayout(self) - self.setLayout(self.__layout) - self.__disWidget = QTreeWidget(self) - self.__layout.addWidget(self.__disWidget) - self.__layout.setContentsMargins(0, 0, 0, 0) - - self.__currentInfoLabel = QLabel(self.tr( - "italic: current instruction")) - self.__labeledInfoLabel = QLabel(self.tr( - "bold: labeled instruction")) - self.__layout.addWidget(self.__currentInfoLabel) - self.__layout.addWidget(self.__labeledInfoLabel) + self.setupUi(self) self.setWindowTitle(self.tr("Disassembly")) @@ -78,21 +68,30 @@ self.__editor = None self.__source = "" - self.__disWidget.setHeaderLabels( + self.disWidget.setHeaderLabels( [self.tr("Line"), self.tr("Offset"), self.tr("Operation"), self.tr("Parameters"), self.tr("Interpreted Parameters")]) - self.__disWidget.setSortingEnabled(False) - self.__disWidget.setSelectionBehavior(QAbstractItemView.SelectRows) - self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection) - self.__disWidget.setAlternatingRowColors(True) + self.codeInfoWidget.setHeaderLabels( + [self.tr("Key"), self.tr("Value")]) - self.__menu = QMenu(self.__disWidget) - self.__menu.addAction(self.tr('Expand All'), self.__expandAll) - self.__menu.addAction(self.tr('Collapse All'), self.__collapseAll) + self.__disMenu = QMenu(self.disWidget) + if self.__mode == PythonDisViewerModes.SourceDisassemblyMode: + self.__codeInfoAct = self.__disMenu.addAction( + self.tr("Show Code Info"), self.__showCodeInfo) + self.__disMenu.addSeparator() + self.__disMenu.addAction( + self.tr('Expand All'), self.__expandAllDis) + self.__disMenu.addAction( + self.tr('Collapse All'), self.__collapseAllDis) - self.__disWidget.setContextMenuPolicy(Qt.CustomContextMenu) - self.__disWidget.customContextMenuRequested.connect( - self.__contextMenuRequested) + self.__codeInfoMenu = QMenu(self.codeInfoWidget) + if self.__mode == PythonDisViewerModes.SourceDisassemblyMode: + self.__codeInfoMenu.addAction( + self.tr("Hide"), self.codeInfoWidget.hide) + self.__codeInfoMenu.addAction( + self.tr('Expand All'), self.__expandAllCodeInfo) + self.__codeInfoMenu.addAction( + self.tr('Collapse All'), self.__collapseAllCodeInfo) self.__errorColor = QBrush( Preferences.getPython("DisViewerErrorColor")) @@ -101,27 +100,44 @@ self.__jumpTargetColor = QBrush( Preferences.getPython("DisViewerLabeledColor")) - self.__disWidget.itemClicked.connect(self.__disItemClicked) - self.__disWidget.itemCollapsed.connect(self.__resizeColumns) - self.__disWidget.itemExpanded.connect(self.__resizeColumns) + self.disWidget.itemClicked.connect(self.__disItemClicked) + self.disWidget.itemCollapsed.connect(self.__resizeDisColumns) + self.disWidget.itemExpanded.connect(self.__resizeDisColumns) + self.disWidget.customContextMenuRequested.connect( + self.__disContextMenuRequested) + + self.codeInfoWidget.itemCollapsed.connect(self.__resizeCodeInfoColumns) + self.codeInfoWidget.itemExpanded.connect(self.__resizeCodeInfoColumns) + self.codeInfoWidget.customContextMenuRequested.connect( + self.__codeInfoContextMenuRequested) if self.__mode == PythonDisViewerModes.SourceDisassemblyMode: self.__vm.disViewerStateChanged.connect( self.__disViewerStateChanged) + + self.codeInfoWidget.hide() self.hide() + elif self.__mode == PythonDisViewerModes.TracebackMode: self.__styleLabels() - def __contextMenuRequested(self, coord): + def __disContextMenuRequested(self, coord): """ - Private slot to show the context menu. + Private slot to show the context menu of the disassembly widget. @param coord position of the mouse pointer @type QPoint """ - coord = self.__disWidget.mapToGlobal(coord) - self.__menu.popup(coord) + if self.__mode == PythonDisViewerModes.SourceDisassemblyMode: + itm = self.disWidget.itemAt(coord) + self.__codeInfoAct.setEnabled(bool(itm.data(0, self.CodeInfoRole))) + self.disWidget.setCurrentItem(itm) + + if self.disWidget.topLevelItemCount() > 0: + # don't show context menu on empty list + coord = self.disWidget.mapToGlobal(coord) + self.__disMenu.popup(coord) def __editorChanged(self, editor): """ @@ -245,23 +261,23 @@ self.hide() self.__editor = None - def __expandAll(self): + def __expandAllDis(self): """ - Private slot to expand all items. + Private slot to expand all items of the disassembly widget. """ - block = self.__disWidget.blockSignals(True) - self.__disWidget.expandAll() - self.__disWidget.blockSignals(block) - self.__resizeColumns() + block = self.disWidget.blockSignals(True) + self.disWidget.expandAll() + self.disWidget.blockSignals(block) + self.__resizeDisColumns() - def __collapseAll(self): + def __collapseAllDis(self): """ - Private slot to collapse all items. + Private slot to collapse all items of the disassembly widget. """ - block = self.__disWidget.blockSignals(True) - self.__disWidget.collapseAll() - self.__disWidget.blockSignals(block) - self.__resizeColumns() + block = self.disWidget.blockSignals(True) + self.disWidget.collapseAll() + self.disWidget.blockSignals(block) + self.__resizeDisColumns() def __createErrorItem(self, error): """ @@ -272,7 +288,7 @@ @return generated item @rtype QTreeWidgetItem """ - itm = QTreeWidgetItem(self.__disWidget, [error]) + itm = QTreeWidgetItem(self.disWidget, [error]) itm.setFirstColumnSpanned(True) itm.setForeground(0, self.__errorColor) return itm @@ -380,6 +396,31 @@ endLine = itm.data(0, self.StartLineRole) itm.setData(0, self.EndLineRole, endLine) + def __createCodeInfo(self, co): + """ + Private method to create a dictionary containing the code info data. + + @param co reference to the code object to generate the info for + @type code + @return dictionary containing the code info data + @rtype dict + """ + return { + "name": co.co_name, + "filename": co.co_filename, + "argcount": co.co_argcount, + "posonlyargcount": co.co_posonlyargcount, + "kwonlyargcount": co.co_kwonlyargcount, + "nlocals": co.co_nlocals, + "stacksize": co.co_stacksize, + "flags": dis.pretty_flags(co.co_flags), + "consts": co.co_consts, + "names": co.co_names, + "varnames": co.co_varnames, + "freevars": co.co_freevars, + "cellvars": co.co_cellvars, + } + def __loadDIS(self): """ Private method to generate the Disassembly from the source of the @@ -395,7 +436,7 @@ )) return - self.__disWidget.clear() + self.disWidget.clear() self.__editor.clearAllHighlights() source = self.__editor.text() @@ -427,12 +468,12 @@ if codeObject: self.setUpdatesEnabled(False) - block = self.__disWidget.blockSignals(True) + block = self.disWidget.blockSignals(True) - self.__disassembleObject(codeObject, self.__disWidget, filename) - QTimer.singleShot(0, self.__resizeColumns) + self.__disassembleObject(codeObject, self.disWidget, filename) + QTimer.singleShot(0, self.__resizeDisColumns) - self.__disWidget.blockSignals(block) + self.disWidget.blockSignals(block) self.setUpdatesEnabled(True) QApplication.restoreOverrideCursor() @@ -451,15 +492,15 @@ "instructions" in disassembly and disassembly["instructions"] ): - self.__disWidget.clear() + self.disWidget.clear() self.setUpdatesEnabled(False) - block = self.__disWidget.blockSignals(True) + block = self.disWidget.blockSignals(True) titleItem = self.__createTitleItem( self.tr("Disassembly of last traceback"), disassembly["firstlineno"], - self.__disWidget + self.disWidget ) lasti = disassembly["lasti"] @@ -486,26 +527,30 @@ if lastStartItem: self.__updateItemEndLine(lastStartItem) - QTimer.singleShot(0, self.__resizeColumns) + QTimer.singleShot(0, self.__resizeDisColumns) - self.__disWidget.blockSignals(block) + self.disWidget.blockSignals(block) self.setUpdatesEnabled(True) if lasti: - lastInstructions = self.__disWidget.findItems( + lastInstructions = self.disWidget.findItems( "{0:d}".format(lasti), Qt.MatchFixedString | Qt.MatchRecursive, 1) if lastInstructions: - self.__disWidget.scrollToItem( + self.disWidget.scrollToItem( lastInstructions[0], QAbstractItemView.PositionAtCenter) + + if "codeInfo" in disassembly: + self.__showCodeInfoData(disassembly["codeInfo"]) - def __resizeColumns(self): + def __resizeDisColumns(self): """ - Private method to resize the columns to suitable values. + Private method to resize the columns of the disassembly widget to + suitable values. """ - for col in range(self.__disWidget.columnCount()): - self.__disWidget.resizeColumnToContents(col) + for col in range(self.disWidget.columnCount()): + self.disWidget.resizeColumnToContents(col) def resizeEvent(self, evt): """ @@ -515,13 +560,14 @@ @type QResizeEvent """ # just adjust the sizes of the columns - self.__resizeColumns() + self.__resizeDisColumns() + self.__resizeCodeInfoColumns() def __clearSelection(self): """ Private method to clear all selected items. """ - for itm in self.__disWidget.selectedItems(): + for itm in self.disWidget.selectedItems(): itm.setSelected(False) def __selectChildren(self, itm, lineno): @@ -544,7 +590,7 @@ self.__selectChildren(child, lineno) if child.data(0, self.StartLineRole) == lineno: - self.__disWidget.scrollToItem( + self.disWidget.scrollToItem( child, QAbstractItemView.PositionAtCenter) def __selectItemForEditorLine(self): @@ -560,8 +606,8 @@ # make the line numbers 1-based cline += 1 - for index in range(self.__disWidget.topLevelItemCount()): - itm = self.__disWidget.topLevelItem(index) + for index in range(self.disWidget.topLevelItemCount()): + itm = self.disWidget.topLevelItem(index) if ( itm.data(0, self.StartLineRole) <= cline <= itm.data(0, self.EndLineRole) @@ -632,6 +678,10 @@ title = self.tr("Code Object '{0}'").format(name) titleItem = self.__createTitleItem(title, co.co_firstlineno, parentItem) + codeInfo = self.__createCodeInfo(co) + if codeInfo: + titleItem.setData(0, self.CodeInfoRole, codeInfo) + lastStartItem = None for instr in dis.get_instructions(co): if instr.starts_line: @@ -673,28 +723,140 @@ Private method to style the info labels iaw. selected colors. """ # current instruction - self.__currentInfoLabel.setStyleSheet( + self.currentInfoLabel.setStyleSheet( "QLabel {{ color : {0}; }}".format( self.__currentInstructionColor.color().name() ) ) - font = self.__currentInfoLabel.font() + font = self.currentInfoLabel.font() font.setItalic(True) - self.__currentInfoLabel.setFont(font) + self.currentInfoLabel.setFont(font) # labeled instruction - self.__labeledInfoLabel.setStyleSheet( + self.labeledInfoLabel.setStyleSheet( "QLabel {{ color : {0}; }}".format( self.__jumpTargetColor.color().name() ) ) - font = self.__labeledInfoLabel.font() + font = self.labeledInfoLabel.font() font.setBold(True) - self.__labeledInfoLabel.setFont(font) + self.labeledInfoLabel.setFont(font) @pyqtSlot() def clear(self): """ Public method to clear the display. """ - self.__disWidget.clear() + self.disWidget.clear() + + def __showCodeInfo(self): + """ + Private slot handling the context menu action to show code info. + """ + itm = self.disWidget.currentItem() + codeInfo = itm.data(0, self.CodeInfoRole) + if codeInfo: + self.codeInfoWidget.show() + self.__showCodeInfoData(codeInfo) + + def __showCodeInfoData(self, codeInfo): + """ + Private method to show the passed code info data. + + @param codeInfo dictionary containing the code info data + @type dict + """ + def createCodeInfoItems(title, infoList): + """ + Function to create code info items for the given list. + + @param title title string for the list + @type str + @param infoList list of info strings + @type list of str + """ + parent = QTreeWidgetItem(self.codeInfoWidget, + [title, str(len(infoList))]) + # TODO: make this a configuration item + parent.setExpanded(False) + + for index, value in enumerate(infoList): + itm = QTreeWidgetItem(parent, [str(index), str(value)]) + itm.setTextAlignment(0, Qt.AlignRight) + + self.codeInfoWidget.clear() + + if codeInfo: + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Name"), codeInfo["name"]]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Filename"), codeInfo["filename"]]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Argument Count"), str(codeInfo["argcount"])]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Positional-only Arguments"), + str(codeInfo["posonlyargcount"])]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Keyword-only arguments"), + str(codeInfo["kwonlyargcount"])]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Number of Locals"), str(codeInfo["nlocals"])]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Stack Size"), str(codeInfo["stacksize"])]) + QTreeWidgetItem(self.codeInfoWidget, [ + self.tr("Flags"), codeInfo["flags"]]) + if codeInfo["consts"]: + createCodeInfoItems(self.tr("Constants"), + codeInfo["consts"]) + if codeInfo["names"]: + createCodeInfoItems(self.tr("Names"), + codeInfo["names"]) + if codeInfo["varnames"]: + createCodeInfoItems(self.tr("Variable Names"), + codeInfo["varnames"]) + if codeInfo["freevars"]: + createCodeInfoItems(self.tr("Free Variables"), + codeInfo["freevars"]) + if codeInfo["cellvars"]: + createCodeInfoItems(self.tr("Cell Variables"), + codeInfo["cellvars"]) + + QTimer.singleShot(0, self.__resizeCodeInfoColumns) + + def __resizeCodeInfoColumns(self): + """ + Private method to resize the columns of the code info widget to + suitable values. + """ + for col in range(self.codeInfoWidget.columnCount()): + self.codeInfoWidget.resizeColumnToContents(col) + + def __expandAllCodeInfo(self): + """ + Private slot to expand all items of the code info widget. + """ + block = self.codeInfoWidget.blockSignals(True) + self.codeInfoWidget.expandAll() + self.codeInfoWidget.blockSignals(block) + self.__resizeCodeInfoColumns() + + def __collapseAllCodeInfo(self): + """ + Private slot to collapse all items of the code info widget. + """ + block = self.codeInfoWidget.blockSignals(True) + self.codeInfoWidget.collapseAll() + self.codeInfoWidget.blockSignals(block) + self.__resizeCodeInfoColumns() + + def __codeInfoContextMenuRequested(self, coord): + """ + Private slot to show the context menu of the code info widget. + + @param coord position of the mouse pointer + @type QPoint + """ + if self.disWidget.topLevelItemCount() > 0: + # don't show context menu on empty list + coord = self.codeInfoWidget.mapToGlobal(coord) + self.__codeInfoMenu.popup(coord)