Mon, 12 Apr 2021 19:05:23 +0200
Applied some more code simplifications suggested by the new Simplify checker (Y102: use single if) (batch 2).
# -*- coding: utf-8 -*- # Copyright (c) 2020 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a widget to visualize the Python Disassembly for some Python sources. """ import os import dis import enum from PyQt5.QtCore import pyqtSlot, Qt, QTimer from PyQt5.QtGui import QBrush from PyQt5.QtWidgets import ( QTreeWidgetItem, QAbstractItemView, QWidget, QMenu ) from E5Gui.E5Application import e5App from E5Gui.E5OverrideCursor import E5OverrideCursor import Preferences from .Ui_PythonDisViewer import Ui_PythonDisViewer class PythonDisViewerModes(enum.Enum): """ Class implementing the disassembly viewer operation modes. """ SourceDisassemblyMode = 0 TracebackMode = 1 class PythonDisViewer(QWidget, Ui_PythonDisViewer): """ Class implementing a widget to visualize the Python Disassembly for some Python sources. """ StartLineRole = Qt.ItemDataRole.UserRole EndLineRole = Qt.ItemDataRole.UserRole + 1 CodeInfoRole = Qt.ItemDataRole.UserRole + 2 def __init__(self, viewmanager, mode=PythonDisViewerModes.SourceDisassemblyMode, parent=None): """ Constructor @param viewmanager reference to the viewmanager object @type ViewManager @param mode operation mode of the viewer @type int @param parent reference to the parent widget @type QWidget """ super().__init__(parent) self.setupUi(self) self.setWindowTitle(self.tr("Disassembly")) self.__vm = viewmanager self.__vmConnected = False self.__mode = mode self.__editor = None self.__source = "" self.disWidget.setHeaderLabels( [self.tr("Line"), self.tr("Offset"), self.tr("Operation"), self.tr("Parameters"), self.tr("Interpreted Parameters")]) self.codeInfoWidget.setHeaderLabels( [self.tr("Key"), self.tr("Value")]) 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.__disMenu.addSeparator() self.__disMenu.addAction( self.tr('Configure...'), self.__configure) 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.__codeInfoMenu.addSeparator() self.__codeInfoMenu.addAction( self.tr('Configure...'), self.__configure) self.__errorColor = QBrush( Preferences.getPython("DisViewerErrorColor")) self.__currentInstructionColor = QBrush( Preferences.getPython("DisViewerCurrentColor")) self.__jumpTargetColor = QBrush( Preferences.getPython("DisViewerLabeledColor")) self.__showCodeInfoDetails = Preferences.getPython( "DisViewerExpandCodeInfoDetails") if self.__mode == PythonDisViewerModes.SourceDisassemblyMode: 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 __disContextMenuRequested(self, coord): """ Private slot to show the context menu of the disassembly widget. @param coord position of the mouse pointer @type QPoint """ 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): """ Private slot to handle a change of the current editor. @param editor reference to the current editor @type Editor """ if editor is not self.__editor: if self.__editor: self.__editor.clearAllHighlights() self.__editor = editor if self.__editor: self.__loadDIS() def __editorSaved(self, editor): """ Private slot to reload the Disassembly after the connected editor was saved. @param editor reference to the editor that performed a save action @type Editor """ if editor and editor is self.__editor: self.__loadDIS() def __editorLineChanged(self, editor, lineno): """ Private slot to handle a mouse button double click in the editor. @param editor reference to the editor, that emitted the signal @type Editor @param lineno line number of the editor's cursor (zero based) @type int """ if editor is self.__editor: if editor.isModified(): # reload the source QTimer.singleShot(0, self.__loadDIS) # highlight the corresponding entry QTimer.singleShot(0, self.__selectItemForEditorLine) def __editorLanguageChanged(self, editor): """ Private slot to handle a change of the editor language. @param editor reference to the editor which changed language @type Editor """ if editor is self.__editor: QTimer.singleShot(0, self.__loadDIS) def __lastEditorClosed(self): """ Private slot to handle the last editor closed signal of the view manager. """ self.hide() def show(self): """ Public slot to show the DIS viewer. """ super().show() if ( self.__mode == PythonDisViewerModes.SourceDisassemblyMode and not self.__vmConnected ): self.__vm.editorChangedEd.connect(self.__editorChanged) self.__vm.editorSavedEd.connect(self.__editorSaved) self.__vm.editorLineChangedEd.connect(self.__editorLineChanged) self.__vm.editorLanguageChanged.connect( self.__editorLanguageChanged) self.__vmConnected = True self.__styleLabels() def hide(self): """ Public slot to hide the DIS viewer. """ super().hide() if self.__editor: self.__editor.clearAllHighlights() if ( self.__mode == PythonDisViewerModes.SourceDisassemblyMode and self.__vmConnected ): self.__vm.editorChangedEd.disconnect(self.__editorChanged) self.__vm.editorSavedEd.disconnect(self.__editorSaved) self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged) self.__vm.editorLanguageChanged.disconnect( self.__editorLanguageChanged) self.__vmConnected = False def shutdown(self): """ Public method to perform shutdown actions. """ self.__editor = None def __disViewerStateChanged(self, on): """ Private slot to toggle the display of the Disassembly viewer. @param on flag indicating to show the Disassembly @type bool """ if self.__mode == PythonDisViewerModes.SourceDisassemblyMode: editor = self.__vm.activeWindow() if on: if editor is not self.__editor: self.__editor = editor self.show() self.__loadDIS() else: self.hide() self.__editor = None def __expandAllDis(self): """ Private slot to expand all items of the disassembly widget. """ block = self.disWidget.blockSignals(True) self.disWidget.expandAll() self.disWidget.blockSignals(block) self.__resizeDisColumns() def __collapseAllDis(self): """ Private slot to collapse all items of the disassembly widget. """ block = self.disWidget.blockSignals(True) self.disWidget.collapseAll() self.disWidget.blockSignals(block) self.__resizeDisColumns() def __createErrorItem(self, error): """ Private method to create a top level error item. @param error error message @type str @return generated item @rtype QTreeWidgetItem """ itm = QTreeWidgetItem(self.disWidget, [error]) itm.setFirstColumnSpanned(True) itm.setForeground(0, self.__errorColor) return itm def __createTitleItem(self, title, line, parentItem): """ Private method to create a title item. @param title titel string for the item @type str @param line start line of the titled disassembly @type int @param parentItem reference to the parent item @type QTreeWidget or QTreeWidgetItem @return generated item @rtype QTreeWidgetItem """ itm = QTreeWidgetItem(parentItem, [title]) itm.setFirstColumnSpanned(True) itm.setExpanded(True) itm.setData(0, self.StartLineRole, line) itm.setData(0, self.EndLineRole, line) return itm def __createInstructionItem(self, instr, parent, lasti=-1): """ Private method to create an item for the given instruction. @param instr instruction the item should be based on @type dis.Instruction @param parent reference to the parent item @type QTreeWidgetItem @param lasti index of the instruction of a traceback @type int @return generated item @rtype QTreeWidgetItem """ fields = [] # Column: Source code line number (right aligned) if instr.starts_line: fields.append("{0:d}".format(instr.starts_line)) else: fields.append("") # Column: Instruction offset from start of code sequence # (right aligned) fields.append("{0:d}".format(instr.offset)) # Column: Opcode name fields.append(instr.opname) # Column: Opcode argument (right aligned) if instr.arg is not None: fields.append(repr(instr.arg)) # Column: Opcode argument details if instr.argrepr: fields.append('(' + instr.argrepr + ')') itm = QTreeWidgetItem(parent, fields) for col in (0, 1, 3): itm.setTextAlignment(col, Qt.AlignmentFlag.AlignRight) # set font to indicate current instruction and jump target font = itm.font(0) if instr.offset == lasti: font.setItalic(True) if instr.is_jump_target: font.setBold(True) for col in range(itm.columnCount()): itm.setFont(col, font) # set color to indicate current instruction or jump target if instr.offset == lasti: foreground = self.__currentInstructionColor elif instr.is_jump_target: foreground = self.__jumpTargetColor else: foreground = None if foreground: for col in range(itm.columnCount()): itm.setForeground(col, foreground) itm.setExpanded(True) if instr.starts_line: itm.setData(0, self.StartLineRole, instr.starts_line) itm.setData(0, self.EndLineRole, instr.starts_line) else: # get line from parent (= start line) lineno = parent.data(0, self.StartLineRole) itm.setData(0, self.StartLineRole, lineno) itm.setData(0, self.EndLineRole, lineno) return itm def __updateItemEndLine(self, itm): """ Private method to update an items end line based on its children. @param itm reference to the item to be updated @type QTreeWidgetItem """ if itm.childCount(): endLine = max( itm.child(index).data(0, self.EndLineRole) for index in range(itm.childCount()) ) else: 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 """ codeInfoDict = { "name": co.co_name, "filename": co.co_filename, "firstlineno": co.co_firstlineno, "argcount": co.co_argcount, "kwonlyargcount": co.co_kwonlyargcount, "nlocals": co.co_nlocals, "stacksize": co.co_stacksize, "flags": dis.pretty_flags(co.co_flags), "consts": [str(const) for const in co.co_consts], "names": [str(name) for name in co.co_names], "varnames": [str(name) for name in co.co_varnames], "freevars": [str(var) for var in co.co_freevars], "cellvars": [str(var) for var in co.co_cellvars], } try: codeInfoDict["posonlyargcount"] = co.co_posonlyargcount except AttributeError: # does not exist prior to 3.8.0 codeInfoDict["posonlyargcount"] = 0 return codeInfoDict def __loadDIS(self): """ Private method to generate the Disassembly from the source of the current editor and visualize it. """ if self.__mode != PythonDisViewerModes.SourceDisassemblyMode: # wrong mode, just return return if not self.__editor: self.__createErrorItem(self.tr( "No editor has been opened." )) return self.clear() self.__editor.clearAllHighlights() self.codeInfoWidget.hide() source = self.__editor.text() if not source.strip(): # empty editor or white space only self.__createErrorItem(self.tr( "The current editor does not contain any source code." )) return if not self.__editor.isPyFile(): self.__createErrorItem(self.tr( "The current editor does not contain Python source code." )) return filename = self.__editor.getFileName() if filename: filename = os.path.basename(filename) else: filename = "<dis>" with E5OverrideCursor(): try: codeObject = self.__tryCompile(source, filename) except Exception as exc: codeObject = None self.__createErrorItem(str(exc)) if codeObject: self.setUpdatesEnabled(False) block = self.disWidget.blockSignals(True) self.__disassembleObject(codeObject, self.disWidget, filename) QTimer.singleShot(0, self.__resizeDisColumns) self.disWidget.blockSignals(block) self.setUpdatesEnabled(True) @pyqtSlot(dict) def showDisassembly(self, disassembly): """ Public slot to receive a code disassembly from the debug client. @param disassembly dictionary containing the disassembly information @type dict """ if ( self.__mode == PythonDisViewerModes.TracebackMode and disassembly and "instructions" in disassembly and disassembly["instructions"] ): self.disWidget.clear() self.setUpdatesEnabled(False) block = self.disWidget.blockSignals(True) titleItem = self.__createTitleItem( self.tr("Disassembly of last traceback"), disassembly["firstlineno"], self.disWidget ) lasti = disassembly["lasti"] lastStartItem = None for instrDict in disassembly["instructions"]: instr = dis.Instruction( instrDict["opname"], 0, # dummy value instrDict["arg"], "", # dummy value instrDict["argrepr"], instrDict["offset"], instrDict["lineno"], instrDict["isJumpTarget"], ) if instrDict["lineno"] > 0: if lastStartItem: self.__updateItemEndLine(lastStartItem) lastStartItem = self.__createInstructionItem( instr, titleItem, lasti=lasti) else: self.__createInstructionItem( instr, lastStartItem, lasti=lasti) if lastStartItem: self.__updateItemEndLine(lastStartItem) QTimer.singleShot(0, self.__resizeDisColumns) self.disWidget.blockSignals(block) self.setUpdatesEnabled(True) if lasti: lastInstructions = self.disWidget.findItems( "{0:d}".format(lasti), Qt.MatchFlag.MatchFixedString | Qt.MatchFlag.MatchRecursive, 1 ) if lastInstructions: self.disWidget.scrollToItem( lastInstructions[0], QAbstractItemView.ScrollHint.PositionAtCenter) if "codeinfo" in disassembly: self.__showCodeInfoData(disassembly["codeinfo"]) def __resizeDisColumns(self): """ Private method to resize the columns of the disassembly widget to suitable values. """ for col in range(self.disWidget.columnCount()): self.disWidget.resizeColumnToContents(col) def resizeEvent(self, evt): """ Protected method to handle resize events. @param evt resize event @type QResizeEvent """ # just adjust the sizes of the columns self.__resizeDisColumns() self.__resizeCodeInfoColumns() def __clearSelection(self): """ Private method to clear all selected items. """ for itm in self.disWidget.selectedItems(): itm.setSelected(False) def __selectChildren(self, itm, lineno): """ Private method to select children of the given item covering the given line number. @param itm reference to the item @type QTreeWidgetItem @param lineno line number to base the selection on @type int """ for index in range(itm.childCount()): child = itm.child(index) if ( child.data(0, self.StartLineRole) <= lineno <= child.data(0, self.EndLineRole) ): child.setSelected(True) self.__selectChildren(child, lineno) if child.data(0, self.StartLineRole) == lineno: self.disWidget.scrollToItem( child, QAbstractItemView.ScrollHint.PositionAtCenter) def __selectItemForEditorLine(self): """ Private slot to select the items corresponding with the cursor line of the current editor. """ # step 1: clear all selected items self.__clearSelection() # step 2: retrieve the editor cursor line cline, cindex = self.__editor.getCursorPosition() # make the line numbers 1-based cline += 1 for index in range(self.disWidget.topLevelItemCount()): itm = self.disWidget.topLevelItem(index) if ( itm.data(0, self.StartLineRole) <= cline <= itm.data(0, self.EndLineRole) ): itm.setSelected(True) self.__selectChildren(itm, cline) @pyqtSlot(QTreeWidgetItem, int) def __disItemClicked(self, itm, column): """ Private slot handling a user click on a Disassembly node item. @param itm reference to the clicked item @type QTreeWidgetItem @param column column number of the click @type int """ self.__editor.clearAllHighlights() if itm is not None: startLine = itm.data(0, self.StartLineRole) endLine = itm.data(0, self.EndLineRole) self.__editor.gotoLine(startLine, firstVisible=True, expand=True) self.__editor.setHighlight(startLine - 1, 0, endLine, -1) def __tryCompile(self, source, name): """ Private method to attempt to compile the given source, first as an expression and then as a statement if the first approach fails. @param source source code string to be compiled @type str @param name name of the file containing the source @type str @return compiled code @rtype code object """ try: c = compile(source, name, 'eval') except SyntaxError: c = compile(source, name, 'exec') return c def __disassembleObject(self, co, parentItem, parentName="", lasti=-1): """ Private method to disassemble the given code object recursively. @param co code object to be disassembled @type code object @param parentItem reference to the parent item @type QTreeWidget or QTreeWidgetItem @param parentName name of the parent code object @type str @param lasti index of the instruction of a traceback @type int """ if co.co_name == "<module>": title = os.path.basename(co.co_filename) name = "" else: if parentName: name = "{0}.{1}".format(parentName, co.co_name) else: name = co.co_name 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: if lastStartItem: self.__updateItemEndLine(lastStartItem) lastStartItem = self.__createInstructionItem( instr, titleItem, lasti=lasti) else: self.__createInstructionItem(instr, lastStartItem, lasti=lasti) if lastStartItem: self.__updateItemEndLine(lastStartItem) for x in co.co_consts: if hasattr(x, 'co_code'): self.__disassembleObject(x, titleItem, parentName=name, lasti=lasti) self.__updateItemEndLine(titleItem) @pyqtSlot() def preferencesChanged(self): """ Public slot handling changes of the Disassembly viewer settings. """ self.__errorColor = QBrush( Preferences.getPython("DisViewerErrorColor")) self.__currentInstructionColor = QBrush( Preferences.getPython("DisViewerCurrentColor")) self.__jumpTargetColor = QBrush( Preferences.getPython("DisViewerLabeledColor")) self.__showCodeInfoDetails = Preferences.getPython( "DisViewerExpandCodeInfoDetails") if self.isVisible(): self.__loadDIS() self.__styleLabels() def __styleLabels(self): """ Private method to style the info labels iaw. selected colors. """ # current instruction self.currentInfoLabel.setStyleSheet( "QLabel {{ color : {0}; }}".format( self.__currentInstructionColor.color().name() ) ) font = self.currentInfoLabel.font() font.setItalic(True) self.currentInfoLabel.setFont(font) # labeled instruction self.labeledInfoLabel.setStyleSheet( "QLabel {{ color : {0}; }}".format( self.__jumpTargetColor.color().name() ) ) font = self.labeledInfoLabel.font() font.setBold(True) self.labeledInfoLabel.setFont(font) @pyqtSlot() def clear(self): """ Public method to clear the display. """ self.disWidget.clear() self.codeInfoWidget.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))]) parent.setExpanded(self.__showCodeInfoDetails) for index, value in enumerate(infoList): itm = QTreeWidgetItem(parent, [str(index), str(value)]) itm.setTextAlignment(0, Qt.AlignmentFlag.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("First Line"), str(codeInfo["firstlineno"])]) 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) def __configure(self): """ Private method to open the configuration dialog. """ e5App().getObject("UserInterface").showPreferences( "pythonPage")