eric6/UI/PythonDisViewer.py

changeset 7704
9251c4dc4f7a
child 7705
90a9aefd4253
equal deleted inserted replaced
7703:1f800f8295ea 7704:9251c4dc4f7a
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a widget to visualize the Python Disassembly for some
8 Python sources.
9 """
10
11 import os
12 import dis
13
14 from PyQt5.QtCore import pyqtSlot, Qt, QTimer
15 from PyQt5.QtGui import QCursor, QBrush
16 from PyQt5.QtWidgets import (
17 QTreeWidget, QApplication, QTreeWidgetItem, QAbstractItemView, QWidget,
18 QVBoxLayout, QLabel
19 )
20
21
22 class PythonDisViewer(QWidget):
23 """
24 Class implementing a widget to visualize the Python Disassembly for some
25 Python sources.
26 """
27 StartLineRole = Qt.UserRole
28 EndLineRole = Qt.UserRole + 1
29
30 def __init__(self, viewmanager, parent=None):
31 """
32 Constructor
33
34 @param viewmanager reference to the viewmanager object
35 @type ViewManager
36 @param parent reference to the parent widget
37 @type QWidget
38 """
39 super(PythonDisViewer, self).__init__(parent)
40
41 self.__layout = QVBoxLayout(self)
42 self.setLayout(self.__layout)
43 self.__disWidget = QTreeWidget(self)
44 self.__layout.addWidget(self.__disWidget)
45 self.__layout.setContentsMargins(0, 0, 0, 0)
46
47 self.__infoLabel = QLabel(self.tr(
48 "italic: current instruction\n"
49 "bold: labelled instruction"
50 ))
51 self.__layout.addWidget(self.__infoLabel)
52
53 self.__vm = viewmanager
54 self.__vmConnected = False
55
56 self.__editor = None
57 self.__source = ""
58
59 self.__disWidget.setHeaderLabels(
60 [self.tr("Line"), self.tr("Offset"), self.tr("Operation"),
61 self.tr("Parameters"), self.tr("Interpreted Parameters")])
62 self.__disWidget.setSortingEnabled(False)
63 self.__disWidget.setSelectionBehavior(QAbstractItemView.SelectRows)
64 self.__disWidget.setSelectionMode(QAbstractItemView.SingleSelection)
65 self.__disWidget.setAlternatingRowColors(True)
66
67 self.__disWidget.itemClicked.connect(self.__disItemClicked)
68
69 self.__vm.disViewerStateChanged.connect(self.__disViewerStateChanged)
70
71 self.hide()
72
73 def __editorChanged(self, editor):
74 """
75 Private slot to handle a change of the current editor.
76
77 @param editor reference to the current editor
78 @type Editor
79 """
80 if editor is not self.__editor:
81 if self.__editor:
82 self.__editor.clearAllHighlights()
83 self.__editor = editor
84 if self.__editor:
85 self.__loadDIS()
86
87 def __editorSaved(self, editor):
88 """
89 Private slot to reload the Disassembly after the connected editor was
90 saved.
91
92 @param editor reference to the editor that performed a save action
93 @type Editor
94 """
95 if editor and editor is self.__editor:
96 self.__loadDIS()
97
98 def __editorDoubleClicked(self, editor, pos, buttons):
99 """
100 Private slot to handle a mouse button double click in the editor.
101
102 @param editor reference to the editor, that emitted the signal
103 @type Editor
104 @param pos position of the double click
105 @type QPoint
106 @param buttons mouse buttons that were double clicked
107 @type Qt.MouseButtons
108 """
109 if editor is self.__editor and buttons == Qt.LeftButton:
110 if editor.isModified():
111 # reload the source
112 QTimer.singleShot(0, self.__loadDIS)
113
114 # highlight the corresponding entry
115 ## QTimer.singleShot(0, self.__selectItemForEditorSelection)
116 QTimer.singleShot(0, self.__grabFocus)
117
118 def __lastEditorClosed(self):
119 """
120 Private slot to handle the last editor closed signal of the view
121 manager.
122 """
123 self.hide()
124
125 def show(self):
126 """
127 Public slot to show the DIS viewer.
128 """
129 super(PythonDisViewer, self).show()
130
131 if not self.__vmConnected:
132 self.__vm.editorChangedEd.connect(self.__editorChanged)
133 self.__vm.editorSavedEd.connect(self.__editorSaved)
134 self.__vm.editorDoubleClickedEd.connect(self.__editorDoubleClicked)
135 self.__vmConnected = True
136
137 def hide(self):
138 """
139 Public slot to hide the DIS viewer.
140 """
141 super(PythonDisViewer, self).hide()
142
143 if self.__editor:
144 self.__editor.clearAllHighlights()
145
146 if self.__vmConnected:
147 self.__vm.editorChangedEd.disconnect(self.__editorChanged)
148 self.__vm.editorSavedEd.disconnect(self.__editorSaved)
149 self.__vm.editorDoubleClickedEd.disconnect(
150 self.__editorDoubleClicked)
151 self.__vmConnected = False
152
153 def shutdown(self):
154 """
155 Public method to perform shutdown actions.
156 """
157 self.__editor = None
158
159 def __disViewerStateChanged(self, on):
160 """
161 Private slot to toggle the display of the Disassembly viewer.
162
163 @param on flag indicating to show the Disassembly
164 @type bool
165 """
166 editor = self.__vm.activeWindow()
167 if on and editor and editor.isPyFile():
168 if editor is not self.__editor:
169 self.__editor = editor
170 self.show()
171 self.__loadDIS()
172 else:
173 self.hide()
174 self.__editor = None
175
176 def __createErrorItem(self, error):
177 """
178 Private method to create a top level error item.
179
180 @param error error message
181 @type str
182 @return generated item
183 @rtype QTreeWidgetItem
184 """
185 itm = QTreeWidgetItem(self.__disWidget, [error])
186 itm.setFirstColumnSpanned(True)
187 itm.setForeground(0, QBrush(Qt.red))
188 return itm
189
190 def __createTitleItem(self, title, line):
191 """
192 Private method to create a title item.
193
194 @param title titel string for the item
195 @type str
196 @param line start line of the titled disassembly
197 @type int
198 @return generated item
199 @rtype QTreeWidgetItem
200 """
201 itm = QTreeWidgetItem(self.__disWidget, [title])
202 itm.setFirstColumnSpanned(True)
203 itm.setExpanded(True)
204
205 itm.setData(0, self.StartLineRole, line)
206 itm.setData(0, self.EndLineRole, line)
207
208 return itm
209
210 def __createInstructionItem(self, instr, parent, lasti=-1):
211 """
212 Private method to create an item for the given instruction.
213
214 @param instr instruction the item should be based on
215 @type dis.Instruction
216 @param parent reference to the parent item
217 @type QTreeWidgetItem
218 @param lasti index of the instruction of a traceback
219 @type int
220 @return generated item
221 @rtype QTreeWidgetItem
222 """
223 fields = []
224 # Column: Source code line number (right aligned)
225 if instr.starts_line:
226 fields.append("{0:d}".format(instr.starts_line))
227 else:
228 fields.append("")
229 # Column: Instruction offset from start of code sequence
230 # (right aligned)
231 fields.append("{0:d}".format(instr.offset))
232 # Column: Opcode name
233 fields.append(instr.opname)
234 # Column: Opcode argument (right aligned)
235 if instr.arg is not None:
236 fields.append(repr(instr.arg))
237 # Column: Opcode argument details
238 if instr.argrepr:
239 fields.append('(' + instr.argrepr + ')')
240
241 itm = QTreeWidgetItem(parent, fields)
242 for col in (0, 1, 3):
243 itm.setTextAlignment(col, Qt.AlignRight)
244 font = itm.font(0)
245 if instr.offset == lasti:
246 font.setItalic(True)
247 if instr.is_jump_target:
248 font.setBold(True)
249 for col in range(itm.columnCount()):
250 itm.setFont(col, font)
251
252 itm.setExpanded(True)
253
254 if instr.starts_line:
255 itm.setData(0, self.StartLineRole, instr.starts_line)
256 itm.setData(0, self.EndLineRole, instr.starts_line)
257 else:
258 # get line from parent (= start line)
259 lineno = parent.data(0, self.StartLineRole)
260 itm.setData(0, self.StartLineRole, lineno)
261 itm.setData(0, self.EndLineRole, lineno)
262 return itm
263
264 def __updateItemEndLine(self, itm):
265 """
266 Private method to update an items end line based on its children.
267
268 @param itm reference to the item to be updated
269 @type QTreeWidgetItem
270 """
271 if itm.childCount():
272 endLine = max(
273 itm.child(index).data(0, self.EndLineRole)
274 for index in range(itm.childCount())
275 )
276 else:
277 endLine = itm.data(0, self.StartLineRole)
278 itm.setData(0, self.EndLineRole, endLine)
279
280 def __loadDIS(self):
281 """
282 Private method to generate the Disassembly from the source of the
283 current editor and visualize it.
284 """
285 if not self.__editor:
286 return
287
288 self.__disWidget.clear()
289 self.__editor.clearAllHighlights()
290
291 if not self.__editor.isPyFile():
292 self.__createErrorItem(self.tr(
293 "The current editor text does not contain Python source."
294 ))
295 return
296
297 source = self.__editor.text()
298 if not source.strip():
299 # empty editor or white space only
300 return
301
302 filename = self.__editor.getFileName()
303 if not filename:
304 filename = "<dis>"
305
306 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
307 try:
308 codeObject = self.__tryCompile(source, filename)
309 except Exception as exc:
310 codeObject = None
311 self.__createErrorItem(str(exc))
312
313 if codeObject:
314 self.setUpdatesEnabled(False)
315
316 self.__disassembleObject(codeObject, self.__disWidget, filename)
317 QTimer.singleShot(0, self.__resizeColumns)
318
319 self.setUpdatesEnabled(True)
320
321 QApplication.restoreOverrideCursor()
322
323 self.__grabFocus()
324
325 def __resizeColumns(self):
326 """
327 Private method to resize the columns to suitable values.
328 """
329 for col in range(self.__disWidget.columnCount()):
330 self.__disWidget.resizeColumnToContents(col)
331
332 def resizeEvent(self, evt):
333 """
334 Protected method to handle resize events.
335
336 @param evt resize event
337 @type QResizeEvent
338 """
339 # just adjust the sizes of the columns
340 self.__resizeColumns()
341
342 def __grabFocus(self):
343 """
344 Private method to grab the input focus.
345 """
346 self.__disWidget.setFocus(Qt.OtherFocusReason)
347
348 @pyqtSlot(QTreeWidgetItem, int)
349 def __disItemClicked(self, itm, column):
350 """
351 Private slot handling a user click on a Disassembly node item.
352
353 @param itm reference to the clicked item
354 @type QTreeWidgetItem
355 @param column column number of the click
356 @type int
357 """
358 self.__editor.clearAllHighlights()
359
360 if itm is not None:
361 startLine = itm.data(0, self.StartLineRole)
362 endLine = itm.data(0, self.EndLineRole)
363
364 self.__editor.gotoLine(startLine, firstVisible=True,
365 expand=True)
366 self.__editor.setHighlight(startLine - 1, 0, endLine, -1)
367
368 def __tryCompile(self, source, name):
369 """
370 Private method to attempt to compile the given source, first as an
371 expression and then as a statement if the first approach fails.
372
373 @param source source code string to be compiled
374 @type str
375 @param name name of the file containing the source
376 @type str
377 @return compiled code
378 @rtype code object
379 """
380 try:
381 c = compile(source, name, 'eval')
382 except SyntaxError:
383 c = compile(source, name, 'exec')
384 return c
385
386 def __disassembleObject(self, co, parentItem, name="", lasti=-1):
387 """
388 Private method to disassemble the given code object recursively.
389
390 @param co code object to be disassembled
391 @type code object
392 @param parentItem reference to the parent item
393 @type QTreeWidget or QTreeWidgetItem
394 @param name name of the code object
395 @type str
396 @param lasti index of the instruction of a traceback
397 @type int
398 """
399 if co.co_name == "<module>":
400 title = (
401 self.tr("Disassembly of module '{0}'")
402 .format(os.path.basename(co.co_filename))
403 )
404 else:
405 title = (
406 self.tr("Disassembly of code object '{0}'")
407 .format(co.co_name)
408 )
409 titleItem = self.__createTitleItem(title, co.co_firstlineno)
410 lastStartItem = None
411 for instr in dis.get_instructions(co):
412 if instr.starts_line:
413 if lastStartItem:
414 self.__updateItemEndLine(lastStartItem)
415 lastStartItem = self.__createInstructionItem(
416 instr, titleItem, lasti=lasti)
417 else:
418 self.__createInstructionItem(instr, lastStartItem, lasti=lasti)
419 if lastStartItem:
420 self.__updateItemEndLine(lastStartItem)
421
422 self.__updateItemEndLine(titleItem)
423
424 for x in co.co_consts:
425 if hasattr(x, 'co_code'):
426 self.__disassembleObject(x, self.__disWidget, lasti=lasti)

eric ide

mercurial