eric6/UI/PythonDisViewer.py

Sat, 19 Sep 2020 19:04:21 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 19 Sep 2020 19:04:21 +0200
changeset 7704
9251c4dc4f7a
child 7705
90a9aefd4253
permissions
-rw-r--r--

Started to implement a Python Disassembly Viewer showing the byte code generated from a Python source file loaded in an editor pane.

# -*- coding: utf-8 -*-

# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a widget to visualize the Python Disassembly for some
Python sources.
"""

import os
import dis

from PyQt5.QtCore import pyqtSlot, Qt, QTimer
from PyQt5.QtGui import QCursor, QBrush
from PyQt5.QtWidgets import (
    QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget,
    QVBoxLayout, QLabel
)


class PythonDisViewer(QWidget):
    """
    Class implementing a widget to visualize the Python Disassembly for some
    Python sources.
    """
    StartLineRole = Qt.UserRole
    EndLineRole = Qt.UserRole + 1
    
    def __init__(self, viewmanager, parent=None):
        """
        Constructor
        
        @param viewmanager reference to the viewmanager object
        @type ViewManager
        @param parent reference to the parent widget
        @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.__infoLabel = QLabel(self.tr(
            "italic: current instruction\n"
            "bold: labelled instruction"
        ))
        self.__layout.addWidget(self.__infoLabel)
        
        self.__vm = viewmanager
        self.__vmConnected = False
        
        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.__disWidget.setSortingEnabled(False)
        self.__disWidget.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection)
        self.__disWidget.setAlternatingRowColors(True)
        
        self.__disWidget.itemClicked.connect(self.__disItemClicked)
        
        self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged)
        
        self.hide()
    
    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 __editorDoubleClicked(self, editor, pos, buttons):
        """
        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 pos position of the double click
        @type QPoint
        @param buttons mouse buttons that were double clicked
        @type Qt.MouseButtons
        """
        if editor is self.__editor and buttons == Qt.LeftButton:
            if editor.isModified():
                # reload the source
                QTimer.singleShot(0, self.__loadDIS)
            
            # highlight the corresponding entry
##            QTimer.singleShot(0, self.__selectItemForEditorSelection)
            QTimer.singleShot(0, self.__grabFocus)
    
    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(PythonDisViewer, self).show()
        
        if not self.__vmConnected:
            self.__vm.editorChangedEd.connect(self.__editorChanged)
            self.__vm.editorSavedEd.connect(self.__editorSaved)
            self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked)
            self.__vmConnected = True
    
    def hide(self):
        """
        Public slot to hide the DIS viewer.
        """
        super(PythonDisViewer, self).hide()
        
        if self.__editor:
            self.__editor.clearAllHighlights()
        
        if self.__vmConnected:
            self.__vm.editorChangedEd.disconnect(self.__editorChanged)
            self.__vm.editorSavedEd.disconnect(self.__editorSaved)
            self.__vm.editorDoubleClickedEd.disconnect(
                self.__editorDoubleClicked)
            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
        """
        editor = self.__vm.activeWindow()
        if on and editor and editor.isPyFile():
            if editor is not self.__editor:
                self.__editor = editor
            self.show()
            self.__loadDIS()
        else:
            self.hide()
            self.__editor = None
    
    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, QBrush(Qt.red))
        return itm
    
    def __createTitleItem(self, title, line):
        """
        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
        @return generated item
        @rtype QTreeWidgetItem
        """
        itm = QTreeWidgetItem(self.__disWidget, [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.AlignRight)
        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)
        
        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 __loadDIS(self):
        """
        Private method to generate the Disassembly from the source of the
        current editor and visualize it.
        """
        if not self.__editor:
            return
        
        self.__disWidget.clear()
        self.__editor.clearAllHighlights()
        
        if not self.__editor.isPyFile():
            self.__createErrorItem(self.tr(
                "The current editor text does not contain Python source."
            ))
            return
        
        source = self.__editor.text()
        if not source.strip():
            # empty editor or white space only
            return
        
        filename = self.__editor.getFileName()
        if not filename:
            filename = "<dis>"
        
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        try:
            codeObject = self.__tryCompile(source, filename)
        except Exception as exc:
            codeObject = None
            self.__createErrorItem(str(exc))
        
        if codeObject:
            self.setUpdatesEnabled(False)
            
            self.__disassembleObject(codeObject, self.__disWidget, filename)
            QTimer.singleShot(0, self.__resizeColumns)
            
            self.setUpdatesEnabled(True)
        
        QApplication.restoreOverrideCursor()
        
        self.__grabFocus()
    
    def __resizeColumns(self):
        """
        Private method to resize the columns 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.__resizeColumns()
    
    def __grabFocus(self):
        """
        Private method to grab the input focus.
        """
        self.__disWidget.setFocus(Qt.OtherFocusReason)
    
    @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, name="", 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 name name of the code object
        @type str
        @param lasti index of the instruction of a traceback
        @type int
        """
        if co.co_name == "<module>":
            title = (
                self.tr("Disassembly of module '{0}'")
                .format(os.path.basename(co.co_filename))
            )
        else:
            title = (
                self.tr("Disassembly of code object '{0}'")
                .format(co.co_name)
            )
        titleItem = self.__createTitleItem(title, co.co_firstlineno)
        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)
        
        self.__updateItemEndLine(titleItem)
        
        for x in co.co_consts:
            if hasattr(x, 'co_code'):
                self.__disassembleObject(x, self.__disWidget, lasti=lasti)

eric ide

mercurial