1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2020 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a widget to visualize the Python Disassembly for some
8Python sources.
9"""
10
11import os
12import dis
13
14import enum
15
16
17from PyQt5.QtCore import pyqtSlot, Qt, QTimer
18from PyQt5.QtGui import QBrush
19from PyQt5.QtWidgets import (
20    QTreeWidgetItem, QAbstractItemView, QWidget, QMenu
21)
22
23from E5Gui.E5Application import e5App
24from E5Gui.E5OverrideCursor import E5OverrideCursor
25
26import Preferences
27
28from .Ui_PythonDisViewer import Ui_PythonDisViewer
29
30
31class PythonDisViewerModes(enum.Enum):
32    """
33    Class implementing the disassembly viewer operation modes.
34    """
35    SOURCEDISASSEMBLY = 0
36    TRACEBACK = 1
37
38
39class PythonDisViewer(QWidget, Ui_PythonDisViewer):
40    """
41    Class implementing a widget to visualize the Python Disassembly for some
42    Python sources.
43    """
44    StartLineRole = Qt.ItemDataRole.UserRole
45    EndLineRole = Qt.ItemDataRole.UserRole + 1
46    CodeInfoRole = Qt.ItemDataRole.UserRole + 2
47
48    def __init__(self, viewmanager,
49                 mode=PythonDisViewerModes.SOURCEDISASSEMBLY,
50                 parent=None):
51        """
52        Constructor
53
54        @param viewmanager reference to the viewmanager object
55        @type ViewManager
56        @param mode operation mode of the viewer
57        @type int
58        @param parent reference to the parent widget
59        @type QWidget
60        """
61        super().__init__(parent)
62        self.setupUi(self)
63
64        self.setWindowTitle(self.tr("Disassembly"))
65
66        self.__vm = viewmanager
67        self.__vmConnected = False
68
69        self.__mode = mode
70
71        self.__editor = None
72        self.__source = ""
73
74        self.disWidget.setHeaderLabels(
75            [self.tr("Line"), self.tr("Offset"), self.tr("Operation"),
76             self.tr("Parameters"), self.tr("Interpreted Parameters")])
77        self.codeInfoWidget.setHeaderLabels(
78            [self.tr("Key"), self.tr("Value")])
79
80        self.__disMenu = QMenu(self.disWidget)
81        if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY:
82            self.__codeInfoAct = self.__disMenu.addAction(
83                self.tr("Show Code Info"), self.__showCodeInfo)
84            self.__disMenu.addSeparator()
85        self.__disMenu.addAction(
86            self.tr('Expand All'), self.__expandAllDis)
87        self.__disMenu.addAction(
88            self.tr('Collapse All'), self.__collapseAllDis)
89        self.__disMenu.addSeparator()
90        self.__disMenu.addAction(
91            self.tr('Configure...'), self.__configure)
92
93        self.__codeInfoMenu = QMenu(self.codeInfoWidget)
94        if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY:
95            self.__codeInfoMenu.addAction(
96                self.tr("Hide"), self.codeInfoWidget.hide)
97        self.__codeInfoMenu.addAction(
98            self.tr('Expand All'), self.__expandAllCodeInfo)
99        self.__codeInfoMenu.addAction(
100            self.tr('Collapse All'), self.__collapseAllCodeInfo)
101        self.__codeInfoMenu.addSeparator()
102        self.__codeInfoMenu.addAction(
103            self.tr('Configure...'), self.__configure)
104
105        self.__errorColor = QBrush(
106            Preferences.getPython("DisViewerErrorColor"))
107        self.__currentInstructionColor = QBrush(
108            Preferences.getPython("DisViewerCurrentColor"))
109        self.__jumpTargetColor = QBrush(
110            Preferences.getPython("DisViewerLabeledColor"))
111
112        self.__showCodeInfoDetails = Preferences.getPython(
113            "DisViewerExpandCodeInfoDetails")
114
115        if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY:
116            self.disWidget.itemClicked.connect(self.__disItemClicked)
117        self.disWidget.itemCollapsed.connect(self.__resizeDisColumns)
118        self.disWidget.itemExpanded.connect(self.__resizeDisColumns)
119        self.disWidget.customContextMenuRequested.connect(
120            self.__disContextMenuRequested)
121
122        self.codeInfoWidget.itemCollapsed.connect(self.__resizeCodeInfoColumns)
123        self.codeInfoWidget.itemExpanded.connect(self.__resizeCodeInfoColumns)
124        self.codeInfoWidget.customContextMenuRequested.connect(
125            self.__codeInfoContextMenuRequested)
126
127        if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY:
128            self.__vm.disViewerStateChanged.connect(
129                self.__disViewerStateChanged)
130
131            self.codeInfoWidget.hide()
132            self.hide()
133
134        elif self.__mode == PythonDisViewerModes.TRACEBACK:
135            self.__styleLabels()
136
137    def __disContextMenuRequested(self, coord):
138        """
139        Private slot to show the context menu of the disassembly widget.
140
141        @param coord position of the mouse pointer
142        @type QPoint
143        """
144        if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY:
145            itm = self.disWidget.itemAt(coord)
146            self.__codeInfoAct.setEnabled(bool(itm.data(0, self.CodeInfoRole)))
147            self.disWidget.setCurrentItem(itm)
148
149        if self.disWidget.topLevelItemCount() > 0:
150            # don't show context menu on empty list
151            coord = self.disWidget.mapToGlobal(coord)
152            self.__disMenu.popup(coord)
153
154    def __editorChanged(self, editor):
155        """
156        Private slot to handle a change of the current editor.
157
158        @param editor reference to the current editor
159        @type Editor
160        """
161        if editor is not self.__editor:
162            if self.__editor:
163                self.__editor.clearAllHighlights()
164            self.__editor = editor
165            if self.__editor:
166                self.__loadDIS()
167
168    def __editorSaved(self, editor):
169        """
170        Private slot to reload the Disassembly after the connected editor was
171        saved.
172
173        @param editor reference to the editor that performed a save action
174        @type Editor
175        """
176        if editor and editor is self.__editor:
177            self.__loadDIS()
178
179    def __editorLineChanged(self, editor, lineno):
180        """
181        Private slot to handle a mouse button double click in the editor.
182
183        @param editor reference to the editor, that emitted the signal
184        @type Editor
185        @param lineno line number of the editor's cursor (zero based)
186        @type int
187        """
188        if editor is self.__editor:
189            if editor.isModified():
190                # reload the source
191                QTimer.singleShot(0, self.__loadDIS)
192
193            # highlight the corresponding entry
194            QTimer.singleShot(0, self.__selectItemForEditorLine)
195
196    def __editorLanguageChanged(self, editor):
197        """
198        Private slot to handle a change of the editor language.
199
200        @param editor reference to the editor which changed language
201        @type Editor
202        """
203        if editor is self.__editor:
204            QTimer.singleShot(0, self.__loadDIS)
205
206    def __lastEditorClosed(self):
207        """
208        Private slot to handle the last editor closed signal of the view
209        manager.
210        """
211        self.hide()
212
213    def show(self):
214        """
215        Public slot to show the DIS viewer.
216        """
217        super().show()
218
219        if (
220            self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY and
221            not self.__vmConnected
222        ):
223            self.__vm.editorChangedEd.connect(self.__editorChanged)
224            self.__vm.editorSavedEd.connect(self.__editorSaved)
225            self.__vm.editorLineChangedEd.connect(self.__editorLineChanged)
226            self.__vm.editorLanguageChanged.connect(
227                self.__editorLanguageChanged)
228            self.__vmConnected = True
229
230        self.__styleLabels()
231
232    def hide(self):
233        """
234        Public slot to hide the DIS viewer.
235        """
236        super().hide()
237
238        if self.__editor:
239            self.__editor.clearAllHighlights()
240
241        if (
242            self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY and
243            self.__vmConnected
244        ):
245            self.__vm.editorChangedEd.disconnect(self.__editorChanged)
246            self.__vm.editorSavedEd.disconnect(self.__editorSaved)
247            self.__vm.editorLineChangedEd.disconnect(self.__editorLineChanged)
248            self.__vm.editorLanguageChanged.disconnect(
249                self.__editorLanguageChanged)
250            self.__vmConnected = False
251
252    def shutdown(self):
253        """
254        Public method to perform shutdown actions.
255        """
256        self.__editor = None
257
258    def __disViewerStateChanged(self, on):
259        """
260        Private slot to toggle the display of the Disassembly viewer.
261
262        @param on flag indicating to show the Disassembly
263        @type bool
264        """
265        if self.__mode == PythonDisViewerModes.SOURCEDISASSEMBLY:
266            editor = self.__vm.activeWindow()
267            if on:
268                if editor is not self.__editor:
269                    self.__editor = editor
270                self.show()
271                self.__loadDIS()
272            else:
273                self.hide()
274                self.__editor = None
275
276    def __expandAllDis(self):
277        """
278        Private slot to expand all items of the disassembly widget.
279        """
280        block = self.disWidget.blockSignals(True)
281        self.disWidget.expandAll()
282        self.disWidget.blockSignals(block)
283        self.__resizeDisColumns()
284
285    def __collapseAllDis(self):
286        """
287        Private slot to collapse all items of the disassembly widget.
288        """
289        block = self.disWidget.blockSignals(True)
290        self.disWidget.collapseAll()
291        self.disWidget.blockSignals(block)
292        self.__resizeDisColumns()
293
294    def __createErrorItem(self, error):
295        """
296        Private method to create a top level error item.
297
298        @param error error message
299        @type str
300        @return generated item
301        @rtype QTreeWidgetItem
302        """
303        itm = QTreeWidgetItem(self.disWidget, [error])
304        itm.setFirstColumnSpanned(True)
305        itm.setForeground(0, self.__errorColor)
306        return itm
307
308    def __createTitleItem(self, title, line, parentItem):
309        """
310        Private method to create a title item.
311
312        @param title titel string for the item
313        @type str
314        @param line start line of the titled disassembly
315        @type int
316        @param parentItem reference to the parent item
317        @type QTreeWidget or QTreeWidgetItem
318        @return generated item
319        @rtype QTreeWidgetItem
320        """
321        itm = QTreeWidgetItem(parentItem, [title])
322        itm.setFirstColumnSpanned(True)
323        itm.setExpanded(True)
324
325        itm.setData(0, self.StartLineRole, line)
326        itm.setData(0, self.EndLineRole, line)
327
328        return itm
329
330    def __createInstructionItem(self, instr, parent, lasti=-1):
331        """
332        Private method to create an item for the given instruction.
333
334        @param instr instruction the item should be based on
335        @type dis.Instruction
336        @param parent reference to the parent item
337        @type QTreeWidgetItem
338        @param lasti index of the instruction of a traceback
339        @type int
340        @return generated item
341        @rtype QTreeWidgetItem
342        """
343        fields = []
344        # Column: Source code line number (right aligned)
345        if instr.starts_line:
346            fields.append("{0:d}".format(instr.starts_line))
347        else:
348            fields.append("")
349        # Column: Instruction offset from start of code sequence
350        # (right aligned)
351        fields.append("{0:d}".format(instr.offset))
352        # Column: Opcode name
353        fields.append(instr.opname)
354        # Column: Opcode argument (right aligned)
355        if instr.arg is not None:
356            fields.append(repr(instr.arg))
357            # Column: Opcode argument details
358            if instr.argrepr:
359                fields.append('(' + instr.argrepr + ')')
360
361        itm = QTreeWidgetItem(parent, fields)
362        for col in (0, 1, 3):
363            itm.setTextAlignment(col, Qt.AlignmentFlag.AlignRight)
364        # set font to indicate current instruction and jump target
365        font = itm.font(0)
366        if instr.offset == lasti:
367            font.setItalic(True)
368        if instr.is_jump_target:
369            font.setBold(True)
370        for col in range(itm.columnCount()):
371            itm.setFont(col, font)
372        # set color to indicate current instruction or jump target
373        if instr.offset == lasti:
374            foreground = self.__currentInstructionColor
375        elif instr.is_jump_target:
376            foreground = self.__jumpTargetColor
377        else:
378            foreground = None
379        if foreground:
380            for col in range(itm.columnCount()):
381                itm.setForeground(col, foreground)
382
383        itm.setExpanded(True)
384
385        if instr.starts_line:
386            itm.setData(0, self.StartLineRole, instr.starts_line)
387            itm.setData(0, self.EndLineRole, instr.starts_line)
388        else:
389            # get line from parent (= start line)
390            lineno = parent.data(0, self.StartLineRole)
391            itm.setData(0, self.StartLineRole, lineno)
392            itm.setData(0, self.EndLineRole, lineno)
393        return itm
394
395    def __updateItemEndLine(self, itm):
396        """
397        Private method to update an items end line based on its children.
398
399        @param itm reference to the item to be updated
400        @type QTreeWidgetItem
401        """
402        endLine = (
403            max(itm.child(index).data(0, self.EndLineRole)
404                for index in range(itm.childCount()))
405            if itm.childCount() else
406            itm.data(0, self.StartLineRole)
407        )
408        itm.setData(0, self.EndLineRole, endLine)
409
410    def __createCodeInfo(self, co):
411        """
412        Private method to create a dictionary containing the code info data.
413
414        @param co reference to the code object to generate the info for
415        @type code
416        @return dictionary containing the code info data
417        @rtype dict
418        """
419        codeInfoDict = {
420            "name": co.co_name,
421            "filename": co.co_filename,
422            "firstlineno": co.co_firstlineno,
423            "argcount": co.co_argcount,
424            "kwonlyargcount": co.co_kwonlyargcount,
425            "nlocals": co.co_nlocals,
426            "stacksize": co.co_stacksize,
427            "flags": dis.pretty_flags(co.co_flags),
428            "consts": [str(const) for const in co.co_consts],
429            "names": [str(name) for name in co.co_names],
430            "varnames": [str(name) for name in co.co_varnames],
431            "freevars": [str(var) for var in co.co_freevars],
432            "cellvars": [str(var) for var in co.co_cellvars],
433        }
434        try:
435            codeInfoDict["posonlyargcount"] = co.co_posonlyargcount
436        except AttributeError:
437            # does not exist prior to 3.8.0
438            codeInfoDict["posonlyargcount"] = 0
439
440        return codeInfoDict
441
442    def __loadDIS(self):
443        """
444        Private method to generate the Disassembly from the source of the
445        current editor and visualize it.
446        """
447        if self.__mode != PythonDisViewerModes.SOURCEDISASSEMBLY:
448            # wrong mode, just return
449            return
450
451        if not self.__editor:
452            self.__createErrorItem(self.tr(
453                "No editor has been opened."
454            ))
455            return
456
457        self.clear()
458        self.__editor.clearAllHighlights()
459        self.codeInfoWidget.hide()
460
461        source = self.__editor.text()
462        if not source.strip():
463            # empty editor or white space only
464            self.__createErrorItem(self.tr(
465                "The current editor does not contain any source code."
466            ))
467            return
468
469        if not self.__editor.isPyFile():
470            self.__createErrorItem(self.tr(
471                "The current editor does not contain Python source code."
472            ))
473            return
474
475        filename = self.__editor.getFileName()
476        filename = os.path.basename(filename) if filename else "<dis>"
477
478        with E5OverrideCursor():
479            try:
480                codeObject = self.__tryCompile(source, filename)
481            except Exception as exc:
482                codeObject = None
483                self.__createErrorItem(str(exc))
484
485            if codeObject:
486                self.setUpdatesEnabled(False)
487                block = self.disWidget.blockSignals(True)
488
489                self.__disassembleObject(codeObject, self.disWidget, filename)
490                QTimer.singleShot(0, self.__resizeDisColumns)
491
492                self.disWidget.blockSignals(block)
493                self.setUpdatesEnabled(True)
494
495    @pyqtSlot(dict)
496    def showDisassembly(self, disassembly):
497        """
498        Public slot to receive a code disassembly from the debug client.
499
500        @param disassembly dictionary containing the disassembly information
501        @type dict
502        """
503        if (
504            self.__mode == PythonDisViewerModes.TRACEBACK and
505            disassembly and
506            "instructions" in disassembly and
507            disassembly["instructions"]
508        ):
509            self.disWidget.clear()
510
511            self.setUpdatesEnabled(False)
512            block = self.disWidget.blockSignals(True)
513
514            titleItem = self.__createTitleItem(
515                self.tr("Disassembly of last traceback"),
516                disassembly["firstlineno"],
517                self.disWidget
518            )
519
520            lasti = disassembly["lasti"]
521            lastStartItem = None
522            for instrDict in disassembly["instructions"]:
523                instr = dis.Instruction(
524                    instrDict["opname"],
525                    0,                              # dummy value
526                    instrDict["arg"],
527                    "",                             # dummy value
528                    instrDict["argrepr"],
529                    instrDict["offset"],
530                    instrDict["lineno"],
531                    instrDict["isJumpTarget"],
532                )
533                if instrDict["lineno"] > 0:
534                    if lastStartItem:
535                        self.__updateItemEndLine(lastStartItem)
536                    lastStartItem = self.__createInstructionItem(
537                        instr, titleItem, lasti=lasti)
538                else:
539                    self.__createInstructionItem(
540                        instr, lastStartItem, lasti=lasti)
541            if lastStartItem:
542                self.__updateItemEndLine(lastStartItem)
543
544            QTimer.singleShot(0, self.__resizeDisColumns)
545
546            self.disWidget.blockSignals(block)
547            self.setUpdatesEnabled(True)
548
549            if lasti:
550                lastInstructions = self.disWidget.findItems(
551                    "{0:d}".format(lasti),
552                    Qt.MatchFlag.MatchFixedString |
553                    Qt.MatchFlag.MatchRecursive,
554                    1
555                )
556                if lastInstructions:
557                    self.disWidget.scrollToItem(
558                        lastInstructions[0],
559                        QAbstractItemView.ScrollHint.PositionAtCenter)
560
561            if "codeinfo" in disassembly:
562                self.__showCodeInfoData(disassembly["codeinfo"])
563
564    def __resizeDisColumns(self):
565        """
566        Private method to resize the columns of the disassembly widget to
567        suitable values.
568        """
569        for col in range(self.disWidget.columnCount()):
570            self.disWidget.resizeColumnToContents(col)
571
572    def resizeEvent(self, evt):
573        """
574        Protected method to handle resize events.
575
576        @param evt resize event
577        @type QResizeEvent
578        """
579        # just adjust the sizes of the columns
580        self.__resizeDisColumns()
581        self.__resizeCodeInfoColumns()
582
583    def __clearSelection(self):
584        """
585        Private method to clear all selected items.
586        """
587        for itm in self.disWidget.selectedItems():
588            itm.setSelected(False)
589
590    def __selectChildren(self, itm, lineno):
591        """
592        Private method to select children of the given item covering the given
593        line number.
594
595        @param itm reference to the item
596        @type QTreeWidgetItem
597        @param lineno line number to base the selection on
598        @type int
599        """
600        for index in range(itm.childCount()):
601            child = itm.child(index)
602            if (
603                child.data(0, self.StartLineRole) <= lineno <=
604                child.data(0, self.EndLineRole)
605            ):
606                child.setSelected(True)
607                self.__selectChildren(child, lineno)
608
609            if child.data(0, self.StartLineRole) == lineno:
610                self.disWidget.scrollToItem(
611                    child, QAbstractItemView.ScrollHint.PositionAtCenter)
612
613    def __selectItemForEditorLine(self):
614        """
615        Private slot to select the items corresponding with the cursor line
616        of the current editor.
617        """
618        # step 1: clear all selected items
619        self.__clearSelection()
620
621        # step 2: retrieve the editor cursor line
622        cline, cindex = self.__editor.getCursorPosition()
623        # make the line numbers 1-based
624        cline += 1
625
626        for index in range(self.disWidget.topLevelItemCount()):
627            itm = self.disWidget.topLevelItem(index)
628            if (
629                itm.data(0, self.StartLineRole) <= cline <=
630                itm.data(0, self.EndLineRole)
631            ):
632                itm.setSelected(True)
633                self.__selectChildren(itm, cline)
634
635    @pyqtSlot(QTreeWidgetItem, int)
636    def __disItemClicked(self, itm, column):
637        """
638        Private slot handling a user click on a Disassembly node item.
639
640        @param itm reference to the clicked item
641        @type QTreeWidgetItem
642        @param column column number of the click
643        @type int
644        """
645        self.__editor.clearAllHighlights()
646
647        if itm is not None:
648            startLine = itm.data(0, self.StartLineRole)
649            endLine = itm.data(0, self.EndLineRole)
650
651            self.__editor.gotoLine(startLine, firstVisible=True,
652                                   expand=True)
653            self.__editor.setHighlight(startLine - 1, 0, endLine, -1)
654
655    def __tryCompile(self, source, name):
656        """
657        Private method to attempt to compile the given source, first as an
658        expression and then as a statement if the first approach fails.
659
660        @param source source code string to be compiled
661        @type str
662        @param name name of the file containing the source
663        @type str
664        @return compiled code
665        @rtype code object
666        """
667        try:
668            c = compile(source, name, 'eval')
669        except SyntaxError:
670            c = compile(source, name, 'exec')
671        return c
672
673    def __disassembleObject(self, co, parentItem, parentName="", lasti=-1):
674        """
675        Private method to disassemble the given code object recursively.
676
677        @param co code object to be disassembled
678        @type code object
679        @param parentItem reference to the parent item
680        @type QTreeWidget or QTreeWidgetItem
681        @param parentName name of the parent code object
682        @type str
683        @param lasti index of the instruction of a traceback
684        @type int
685        """
686        if co.co_name == "<module>":
687            title = os.path.basename(co.co_filename)
688            name = ""
689        else:
690            if parentName:
691                name = "{0}.{1}".format(parentName, co.co_name)
692            else:
693                name = co.co_name
694            title = self.tr("Code Object '{0}'").format(name)
695        titleItem = self.__createTitleItem(title, co.co_firstlineno,
696                                           parentItem)
697        codeInfo = self.__createCodeInfo(co)
698        if codeInfo:
699            titleItem.setData(0, self.CodeInfoRole, codeInfo)
700
701        lastStartItem = None
702        for instr in dis.get_instructions(co):
703            if instr.starts_line:
704                if lastStartItem:
705                    self.__updateItemEndLine(lastStartItem)
706                lastStartItem = self.__createInstructionItem(
707                    instr, titleItem, lasti=lasti)
708            else:
709                self.__createInstructionItem(instr, lastStartItem, lasti=lasti)
710        if lastStartItem:
711            self.__updateItemEndLine(lastStartItem)
712
713        for x in co.co_consts:
714            if hasattr(x, 'co_code'):
715                self.__disassembleObject(x, titleItem, parentName=name,
716                                         lasti=lasti)
717
718        self.__updateItemEndLine(titleItem)
719
720    @pyqtSlot()
721    def preferencesChanged(self):
722        """
723        Public slot handling changes of the Disassembly viewer settings.
724        """
725        self.__errorColor = QBrush(
726            Preferences.getPython("DisViewerErrorColor"))
727        self.__currentInstructionColor = QBrush(
728            Preferences.getPython("DisViewerCurrentColor"))
729        self.__jumpTargetColor = QBrush(
730            Preferences.getPython("DisViewerLabeledColor"))
731
732        self.__showCodeInfoDetails = Preferences.getPython(
733            "DisViewerExpandCodeInfoDetails")
734
735        if self.isVisible():
736            self.__loadDIS()
737
738        self.__styleLabels()
739
740    def __styleLabels(self):
741        """
742        Private method to style the info labels iaw. selected colors.
743        """
744        # current instruction
745        self.currentInfoLabel.setStyleSheet(
746            "QLabel {{ color : {0}; }}".format(
747                self.__currentInstructionColor.color().name()
748            )
749        )
750        font = self.currentInfoLabel.font()
751        font.setItalic(True)
752        self.currentInfoLabel.setFont(font)
753
754        # labeled instruction
755        self.labeledInfoLabel.setStyleSheet(
756            "QLabel {{ color : {0}; }}".format(
757                self.__jumpTargetColor.color().name()
758            )
759        )
760        font = self.labeledInfoLabel.font()
761        font.setBold(True)
762        self.labeledInfoLabel.setFont(font)
763
764    @pyqtSlot()
765    def clear(self):
766        """
767        Public method to clear the display.
768        """
769        self.disWidget.clear()
770        self.codeInfoWidget.clear()
771
772    def __showCodeInfo(self):
773        """
774        Private slot handling the context menu action to show code info.
775        """
776        itm = self.disWidget.currentItem()
777        codeInfo = itm.data(0, self.CodeInfoRole)
778        if codeInfo:
779            self.codeInfoWidget.show()
780            self.__showCodeInfoData(codeInfo)
781
782    def __showCodeInfoData(self, codeInfo):
783        """
784        Private method to show the passed code info data.
785
786        @param codeInfo dictionary containing the code info data
787        @type dict
788        """
789        def createCodeInfoItems(title, infoList):
790            """
791            Function to create code info items for the given list.
792
793            @param title title string for the list
794            @type str
795            @param infoList list of info strings
796            @type list of str
797            """
798            parent = QTreeWidgetItem(self.codeInfoWidget,
799                                     [title, str(len(infoList))])
800            parent.setExpanded(self.__showCodeInfoDetails)
801
802            for index, value in enumerate(infoList):
803                itm = QTreeWidgetItem(parent, [str(index), str(value)])
804                itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight)
805
806        self.codeInfoWidget.clear()
807
808        if codeInfo:
809            QTreeWidgetItem(self.codeInfoWidget, [
810                self.tr("Name"), codeInfo["name"]])
811            QTreeWidgetItem(self.codeInfoWidget, [
812                self.tr("Filename"), codeInfo["filename"]])
813            QTreeWidgetItem(self.codeInfoWidget, [
814                self.tr("First Line"), str(codeInfo["firstlineno"])])
815            QTreeWidgetItem(self.codeInfoWidget, [
816                self.tr("Argument Count"), str(codeInfo["argcount"])])
817            QTreeWidgetItem(self.codeInfoWidget, [
818                self.tr("Positional-only Arguments"),
819                str(codeInfo["posonlyargcount"])])
820            QTreeWidgetItem(self.codeInfoWidget, [
821                self.tr("Keyword-only Arguments"),
822                str(codeInfo["kwonlyargcount"])])
823            QTreeWidgetItem(self.codeInfoWidget, [
824                self.tr("Number of Locals"), str(codeInfo["nlocals"])])
825            QTreeWidgetItem(self.codeInfoWidget, [
826                self.tr("Stack Size"), str(codeInfo["stacksize"])])
827            QTreeWidgetItem(self.codeInfoWidget, [
828                self.tr("Flags"), codeInfo["flags"]])
829            if codeInfo["consts"]:
830                createCodeInfoItems(self.tr("Constants"),
831                                    codeInfo["consts"])
832            if codeInfo["names"]:
833                createCodeInfoItems(self.tr("Names"),
834                                    codeInfo["names"])
835            if codeInfo["varnames"]:
836                createCodeInfoItems(self.tr("Variable Names"),
837                                    codeInfo["varnames"])
838            if codeInfo["freevars"]:
839                createCodeInfoItems(self.tr("Free Variables"),
840                                    codeInfo["freevars"])
841            if codeInfo["cellvars"]:
842                createCodeInfoItems(self.tr("Cell Variables"),
843                                    codeInfo["cellvars"])
844
845            QTimer.singleShot(0, self.__resizeCodeInfoColumns)
846
847    def __resizeCodeInfoColumns(self):
848        """
849        Private method to resize the columns of the code info widget to
850        suitable values.
851        """
852        for col in range(self.codeInfoWidget.columnCount()):
853            self.codeInfoWidget.resizeColumnToContents(col)
854
855    def __expandAllCodeInfo(self):
856        """
857        Private slot to expand all items of the code info widget.
858        """
859        block = self.codeInfoWidget.blockSignals(True)
860        self.codeInfoWidget.expandAll()
861        self.codeInfoWidget.blockSignals(block)
862        self.__resizeCodeInfoColumns()
863
864    def __collapseAllCodeInfo(self):
865        """
866        Private slot to collapse all items of the code info widget.
867        """
868        block = self.codeInfoWidget.blockSignals(True)
869        self.codeInfoWidget.collapseAll()
870        self.codeInfoWidget.blockSignals(block)
871        self.__resizeCodeInfoColumns()
872
873    def __codeInfoContextMenuRequested(self, coord):
874        """
875        Private slot to show the context menu of the code info widget.
876
877        @param coord position of the mouse pointer
878        @type QPoint
879        """
880        if self.disWidget.topLevelItemCount() > 0:
881            # don't show context menu on empty list
882            coord = self.codeInfoWidget.mapToGlobal(coord)
883            self.__codeInfoMenu.popup(coord)
884
885    def __configure(self):
886        """
887        Private method to open the configuration dialog.
888        """
889        e5App().getObject("UserInterface").showPreferences(
890            "pythonPage")
891