Thu, 01 Nov 2012 15:31:06 +0100
Started implementing the call trace functionality.
--- a/DebugClients/Python3/DebugBase.py Thu Nov 01 10:15:08 2012 +0100 +++ b/DebugClients/Python3/DebugBase.py Thu Nov 01 15:31:06 2012 +0100 @@ -14,7 +14,7 @@ import inspect from DebugProtocol import ResponseClearWatch, ResponseClearBreak, ResponseLine, \ - ResponseSyntax, ResponseException + ResponseSyntax, ResponseException, CallTrace gRecursionLimit = 64 @@ -153,13 +153,39 @@ if event == 'return': self.cFrame = frame.f_back self.__recursionDepth -= 1 + self.__sendCallTrace(event, frame, self.cFrame) elif event == 'call': + self.__sendCallTrace(event, self.cFrame, frame) self.cFrame = frame self.__recursionDepth += 1 if self.__recursionDepth > gRecursionLimit: raise RuntimeError('maximum recursion depth exceeded\n' '(offending frame is two down the stack)') + def __sendCallTrace(self, event, fromFrame, toFrame): + """ + Private method to send a call/return trace. + + @param event trace event (string) + @param fromFrame originating frame (frame) + @param toFrame destination frame (frame) + """ + if self._dbgClient.callTraceEnabled: + if not self.__skip_it(fromFrame) and not self.__skip_it(toFrame): + if event in ["call", "return"]: + fr = fromFrame + fromStr = "{0}:{1}:{2}".format( + self._dbgClient.absPath(self.fix_frame_filename(fr)), + fr.f_lineno, + fr.f_code.co_name) + fr = toFrame + toStr = "{0}:{1}:{2}".format( + self._dbgClient.absPath(self.fix_frame_filename(fr)), + fr.f_lineno, + fr.f_code.co_name) + self._dbgClient.write("{0}{1}@@{2}@@{3}\n".format( + CallTrace, event[0], fromStr, toStr)) + def trace_dispatch(self, frame, event, arg): """ Reimplemented from bdb.py to do some special things. @@ -718,6 +744,9 @@ @param frame the frame object @return flag indicating whether the debugger should skip this frame """ + if frame is None: + return True + fn = self.fix_frame_filename(frame) # Eliminate things like <string> and <stdin>.
--- a/DebugClients/Python3/DebugClientBase.py Thu Nov 01 10:15:08 2012 +0100 +++ b/DebugClients/Python3/DebugClientBase.py Thu Nov 01 15:31:06 2012 +0100 @@ -199,6 +199,9 @@ self.errorstream = None self.pollingDisabled = False + self.callTraceEnabled = False + self.__newCallTraceEnabled = False + self.skipdirs = sys.path[:] self.variant = 'You should not see this' @@ -442,6 +445,17 @@ self.pendingResponse = DebugProtocol.ResponseOK return + if cmd == DebugProtocol.RequestCallTrace: + if arg.strip().lower() == "on": + callTraceEnabled = True + else: + callTraceEnabled = False + if self.debugging: + self.callTraceEnabled = callTraceEnabled + else: + self.__newCallTraceEnabled = callTraceEnabled # remember for later + return + if cmd == DebugProtocol.RequestEnv: env = eval(arg.replace("u'", "'")) for key, value in env.items(): @@ -494,6 +508,7 @@ sys.modules['__main__'] = self.debugMod code = self.__compileFileSource(self.running) if code: + self.callTraceEnabled = self.__newCallTraceEnabled res = self.mainThread.run(code, self.debugMod.__dict__) self.progTerminated(res) return
--- a/DebugClients/Python3/DebugProtocol.py Thu Nov 01 10:15:08 2012 +0100 +++ b/DebugClients/Python3/DebugProtocol.py Thu Nov 01 15:31:06 2012 +0100 @@ -77,4 +77,7 @@ PassiveStartup = '>PassiveStartup<' +RequestCallTrace = '>CallTrace<' +CallTrace = '>CallTrace<' + EOT = '>EOT<\n'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Debugger/CallTraceViewer.py Thu Nov 01 15:31:06 2012 +0100 @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2012 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the Call Trace viewer widget. +""" + +from PyQt4.QtCore import pyqtSlot, pyqtSignal +from PyQt4.QtGui import QWidget, QTreeWidgetItem + +from .Ui_CallTraceViewer import Ui_CallTraceViewer + +import UI.PixmapCache +import Preferences + + +class CallTraceViewer(QWidget, Ui_CallTraceViewer): + """ + Class implementing the Call Trace viewer widget. + + @signal sourceFile(str, int) emitted to show the source of a call/return point + """ + sourceFile = pyqtSignal(str, int) + + def __init__(self, debugServer, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super().__init__(parent) + self.setupUi(self) + + self.__dbs = debugServer + + self.startTraceButton.setIcon(UI.PixmapCache.getIcon("callTraceStart.png")) + self.stopTraceButton.setIcon(UI.PixmapCache.getIcon("callTraceStop.png")) + self.resizeButton.setIcon(UI.PixmapCache.getIcon("resizeColumns.png")) + self.clearButton.setIcon(UI.PixmapCache.getIcon("editDelete.png")) + self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSave.png")) + + self.__headerItem = QTreeWidgetItem(["", self.trUtf8("From"), self.trUtf8("To")]) + self.__headerItem.setIcon(0, UI.PixmapCache.getIcon("callReturn.png")) + self.callTrace.setHeaderItem(self.__headerItem) + + self.__callStack = [] + + self.__entryFormat = "{0}:{1} ({2})" + + self.__callTraceEnabled = Preferences.toBool( + Preferences.Prefs.settings.value("CallTrace/Enabled", False)) + + if self.__callTraceEnabled: + self.stopTraceButton.setEnabled(False) + else: + self.startTraceButton.setEnabled(False) + + self.__dbs.callTraceInfo.connect(self.__addCallTraceInfo) + + @pyqtSlot() + def on_startTraceButton_clicked(self): + """ + Private slot to start call tracing. + """ + self.__dbs.setCallTraceEnabled(True) + self.stopTraceButton.setEnabled(True) + self.startTraceButton.setEnabled(False) + Preferences.Prefs.settings.setValue("CallTrace/Enabled", True) + + @pyqtSlot() + def on_stopTraceButton_clicked(self): + """ + Private slot to start call tracing. + """ + self.__dbs.setCallTraceEnabled(False) + self.stopTraceButton.setEnabled(False) + self.startTraceButton.setEnabled(True) + Preferences.Prefs.settings.setValue("CallTrace/Enabled", False) + + @pyqtSlot() + def on_resizeButton_clicked(self): + """ + Private slot to resize the columns of the call trace to their contents. + """ + for column in range(self.callTrace.columnCount()): + self.callTrace.resizeColumnToContents(column) + + @pyqtSlot() + def on_clearButton_clicked(self): + """ + Private slot to clear the call trace. + """ + self.clear() + + @pyqtSlot() + def on_saveButton_clicked(self): + """ + Slot documentation goes here. + """ + # TODO: not implemented yet + raise NotImplementedError + + @pyqtSlot(QTreeWidgetItem, int) + def on_callTrace_itemDoubleClicked(self, item, column): + """ + Slot documentation goes here. + """ + # TODO: not implemented yet + raise NotImplementedError + + def clear(self): + """ + Public slot to clear the call trace info. + """ + self.callTrace.clear() + self.__callStack = [] + + def __addCallTraceInfo(self, isCall, fromFile, fromLine, fromFunction, + toFile, toLine, toFunction): + """ + Private method to add an entry to the call trace viewer. + + @param isCall flag indicating a 'call' (boolean) + @param fromFile name of the originating file (string) + @param fromLine line number in the originating file (string) + @param fromFunction name of the originating function (string) + @param toFile name of the target file (string) + @param toLine line number in the target file (string) + @param toFunction name of the target function (string) + """ + if isCall: + icon = UI.PixmapCache.getIcon("forward.png") + else: + icon = UI.PixmapCache.getIcon("back.png") + parentItem = self.__callStack[-1] if self.__callStack else self.callTrace + + itm = QTreeWidgetItem(parentItem, ["", + self.__entryFormat.format(fromFile, fromLine, fromFunction), + self.__entryFormat.format(toFile, toLine, toFunction)]) + itm.setIcon(0, icon) + itm.setExpanded(True) + + if isCall: + self.__callStack.append(itm) + else: + self.__callStack.pop(-1) + + def isCallTraceEnabled(self): + """ + Public method to get the state of the call trace function. + + @return flag indicating the state of the call trace function (boolean) + """ + return self.__callTraceEnabled
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Debugger/CallTraceViewer.ui Thu Nov 01 15:31:06 2012 +0100 @@ -0,0 +1,118 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CallTraceViewer</class> + <widget class="QWidget" name="CallTraceViewer"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>528</height> + </rect> + </property> + <property name="windowTitle"> + <string>Call Trace</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QToolButton" name="startTraceButton"> + <property name="toolTip"> + <string>Press to start tracing calls and returns</string> + </property> + <property name="text"> + <string notr="true">Start</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="stopTraceButton"> + <property name="toolTip"> + <string>Press to stop tracing calls and returns</string> + </property> + <property name="text"> + <string notr="true">Stop</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_2"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="resizeButton"> + <property name="toolTip"> + <string>Press to resize the columns to their contents</string> + </property> + <property name="text"> + <string notr="true">Resize</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="clearButton"> + <property name="toolTip"> + <string>Press to clear the call trace</string> + </property> + <property name="text"> + <string notr="true">Clear</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="saveButton"> + <property name="toolTip"> + <string>Press to save the call trace as a text file</string> + </property> + <property name="text"> + <string notr="true">Save</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeWidget" name="callTrace"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string> </string> + </property> + </column> + <column> + <property name="text"> + <string>From</string> + </property> + </column> + <column> + <property name="text"> + <string>To</string> + </property> + </column> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>startTraceButton</tabstop> + <tabstop>stopTraceButton</tabstop> + <tabstop>resizeButton</tabstop> + <tabstop>clearButton</tabstop> + <tabstop>saveButton</tabstop> + <tabstop>callTrace</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- a/Debugger/DebugProtocol.py Thu Nov 01 10:15:08 2012 +0100 +++ b/Debugger/DebugProtocol.py Thu Nov 01 15:31:06 2012 +0100 @@ -74,4 +74,7 @@ PassiveStartup = '>PassiveStartup<' +RequestCallTrace = '>CallTrace<' +CallTrace = '>CallTrace<' + EOT = '>EOT<\n'
--- a/Debugger/DebugServer.py Thu Nov 01 10:15:08 2012 +0100 +++ b/Debugger/DebugServer.py Thu Nov 01 15:31:06 2012 +0100 @@ -95,6 +95,8 @@ an expected test failure @signal utTestSucceededUnexpected(testname, id) emitted after the client reported an unexpected test success + @signal callTraceInfo(isCall, fromFile, fromLine, fromFunction, toFile, toLine, + toFunction) emitted after the client reported the call trace data """ clientClearBreak = pyqtSignal(str, int) clientClearWatch = pyqtSignal(str) @@ -129,6 +131,7 @@ utTestSucceededUnexpected = pyqtSignal(str, str) utFinished = pyqtSignal() passiveDebugStarted = pyqtSignal(str, bool) + callTraceInfo = pyqtSignal(bool, str, str, str, str, str, str) def __init__(self): """ @@ -665,7 +668,7 @@ def remoteLoad(self, fn, argv, wd, env, autoClearShell=True, tracePython=False, autoContinue=True, forProject=False, runInConsole=False, autoFork=False, forkChild=False, - clientType=""): + clientType="", enableCallTrace=False): """ Public method to load a new program to debug. @@ -685,6 +688,8 @@ @keyparam autoFork flag indicating the automatic fork mode (boolean) @keyparam forkChild flag indicating to debug the child after forking (boolean) @keyparam clientType client type to be used (string) + @keyparam enableCallTrace flag indicating to enable the call trace + function (boolean) """ self.__autoClearShell = autoClearShell self.__autoContinue = autoContinue @@ -699,6 +704,7 @@ self.__setClientType('Python3') # assume it is a Python3 file self.startClient(False, forProject=forProject, runInConsole=runInConsole) + self.setCallTraceEnabled(enableCallTrace) self.remoteEnvironment(env) self.debuggerInterface.remoteLoad(fn, argv, wd, tracePython, autoContinue, @@ -977,6 +983,18 @@ """ self.debuggerInterface.remoteClientSetFilter(scope, filter) + def setCallTraceEnabled(self, on): + """ + Public method to set the call trace state. + + @param on flag indicating to enable the call trace function (boolean) + """ + # TODO: remove the try/except once all interface have been adjusted + try: + self.debuggerInterface.setCallTraceEnabled(on) + except AttributeError: + pass + def remoteEval(self, arg): """ Public method to evaluate arg in the current context of the debugged program. @@ -1238,6 +1256,22 @@ """ self.clientCompletionList.emit(completionList, text) + def signalClientCallTrace(self, isCall, fromFile, fromLine, fromFunction, + toFile, toLine, toFunction): + """ + Public method to process the client call trace data. + + @param isCall flag indicating a 'call' (boolean) + @param fromFile name of the originating file (string) + @param fromLine line number in the originating file (string) + @param fromFunction name of the originating function (string) + @param toFile name of the target file (string) + @param toLine line number in the target file (string) + @param toFunction name of the target function (string) + """ + self.callTraceInfo.emit(isCall, fromFile, fromLine, fromFunction, + toFile, toLine, toFunction) + def clientUtPrepared(self, result, exceptionType, exceptionValue): """ Public method to process the client unittest prepared info.
--- a/Debugger/DebugUI.py Thu Nov 01 10:15:08 2012 +0100 +++ b/Debugger/DebugUI.py Thu Nov 01 15:31:06 2012 +0100 @@ -1776,12 +1776,18 @@ fn = os.path.join(getConfig('ericDir'), "eric5.py") tracePython = True # override flag because it must be true + # Ask the client to send call trace info + enableCallTrace = self.debugViewer.isCallTraceEnabled() + if enableCallTrace: + self.debugViewer.clearCallTrace() + # Ask the client to open the new program. self.debugServer.remoteLoad(fn, argv, wd, env, autoClearShell=self.autoClearShell, tracePython=tracePython, autoContinue=autoContinue, forProject=debugProject, runInConsole=console, autoFork=forkAutomatically, - forkChild=forkIntoChild, clientType=self.clientType) + forkChild=forkIntoChild, clientType=self.clientType, + enableCallTrace=enableCallTrace) # Signal that we have started a debugging session self.debuggingStarted.emit(fn) @@ -1827,12 +1833,18 @@ fn = os.path.join(getConfig('ericDir'), "eric5.py") if self.lastStartAction in [1, 2]: + # Ask the client to send call trace info + enableCallTrace = self.debugViewer.isCallTraceEnabled() + if enableCallTrace: + self.debugViewer.clearCallTrace() + # Ask the client to debug the new program. self.debugServer.remoteLoad(fn, argv, wd, env, autoClearShell=self.autoClearShell, tracePython=self.tracePython, autoContinue=self.autoContinue, forProject=forProject, runInConsole=self.runInConsole, autoFork=self.forkAutomatically, - forkChild=self.forkIntoChild, clientType=self.clientType) + forkChild=self.forkIntoChild, clientType=self.clientType, + enableCallTrace=enableCallTrace) # Signal that we have started a debugging session self.debuggingStarted.emit(fn)
--- a/Debugger/DebugViewer.py Thu Nov 01 10:15:08 2012 +0100 +++ b/Debugger/DebugViewer.py Thu Nov 01 15:31:06 2012 +0100 @@ -10,6 +10,7 @@ <ul> <li>variables viewer for global variables</li> <li>variables viewer for local variables</li> + <li>call trace viewer</li> <li>viewer for breakpoints</li> <li>viewer for watch expressions</li> <li>viewer for exceptions</li> @@ -30,6 +31,7 @@ from .ExceptionLogger import ExceptionLogger from .BreakPointViewer import BreakPointViewer from .WatchPointViewer import WatchPointViewer +from .CallTraceViewer import CallTraceViewer import UI.PixmapCache import Preferences @@ -183,6 +185,12 @@ self.setLocalsFilterButton.clicked[()].connect(self.__setLocalsFilter) self.localsFilterEdit.returnPressed.connect(self.__setLocalsFilter) + # add the call trace viewer + self.callTraceViewer = CallTraceViewer(self.debugServer) + index = self.__tabWidget.addTab(self.callTraceViewer, + UI.PixmapCache.getIcon("callTrace.png"), "") + self.__tabWidget.setTabToolTip(index, self.callTraceViewer.windowTitle()) + # add the breakpoint viewer self.breakpointViewer = BreakPointViewer() self.breakpointViewer.setModel(self.debugServer.getBreakPointModel()) @@ -278,7 +286,21 @@ if self.embeddedShell: self.saveCurrentPage() self.__tabWidget.setCurrentWidget(self.shellAssembly) - + + def isCallTraceEnabled(self): + """ + Public method to get the state of the call trace function. + + @return flag indicating the state of the call trace function (boolean) + """ + return self.callTraceViewer.isCallTraceEnabled() + + def clearCallTrace(self): + """ + Public method to clear the recorded call trace. + """ + self.callTraceViewer.clear() + def showVariables(self, vlist, globals): """ Public method to show the variables in the respective window.
--- a/Debugger/DebuggerInterfacePython3.py Thu Nov 01 10:15:08 2012 +0100 +++ b/Debugger/DebuggerInterfacePython3.py Thu Nov 01 15:31:06 2012 +0100 @@ -666,6 +666,18 @@ self.__sendCommand('{0}{1:d}, "{2}"\n'.format( DebugProtocol.RequestSetFilter, scope, filter)) + def setCallTraceEnabled(self, on): + """ + Public method to set the call trace state. + + @param on flag indicating to enable the call trace function (boolean) + """ + if on: + cmd = "on" + else: + cmd = "off" + self.__sendCommand('{0}{1}\n'.format(DebugProtocol.RequestCallTrace, cmd)) + def remoteEval(self, arg): """ Public method to evaluate arg in the current context of the debugged program. @@ -800,6 +812,16 @@ self.debugServer.signalClientStack(stack) continue + if resp == DebugProtocol.CallTrace: + event, fromStr, toStr = line[eoc:-1].split("@@") + isCall = event.lower() == "c" + fromFile, fromLineno, fromFunc = fromStr.rsplit(":", 2) + toFile, toLineno, toFunc = toStr.rsplit(":", 2) + self.debugServer.signalClientCallTrace(isCall, + fromFile, fromLineno, fromFunc, + toFile, toLineno, toFunc) + continue + if resp == DebugProtocol.ResponseThreadList: currentId, threadList = eval(line[eoc:-1]) self.debugServer.signalClientThreadList(currentId, threadList)
--- a/changelog Thu Nov 01 10:15:08 2012 +0100 +++ b/changelog Thu Nov 01 15:31:06 2012 +0100 @@ -1,5 +1,8 @@ Change Log ---------- +Version 5.3-snapshot-2012mmdd: +- bug fixes + Version 5.3-snapshot-20121101: - bug fixes - General
--- a/eric5.e4p Thu Nov 01 10:15:08 2012 +0100 +++ b/eric5.e4p Thu Nov 01 15:31:06 2012 +0100 @@ -1049,6 +1049,7 @@ <Source>Toolbox/Startup.py</Source> <Source>Preferences/ConfigurationPages/HelpInterfacePage.py</Source> <Source>E5Gui/E5MainWindow.py</Source> + <Source>Debugger/CallTraceViewer.py</Source> </Sources> <Forms> <Form>PyUnit/UnittestDialog.ui</Form> @@ -1349,6 +1350,7 @@ <Form>Helpviewer/GreaseMonkey/GreaseMonkeyConfiguration/GreaseMonkeyConfigurationScriptInfoDialog.ui</Form> <Form>Helpviewer/AdBlock/AdBlockExceptionsDialog.ui</Form> <Form>Preferences/ConfigurationPages/HelpInterfacePage.ui</Form> + <Form>Debugger/CallTraceViewer.ui</Form> </Forms> <Translations> <Translation>i18n/eric5_cs.qm</Translation>