--- a/eric6/DebugClients/Python/DebugBase.py Sun Jan 17 13:53:08 2021 +0100 +++ b/eric6/DebugClients/Python/DebugBase.py Mon Feb 01 10:38:16 2021 +0100 @@ -14,7 +14,6 @@ import inspect import ctypes import time -from inspect import CO_GENERATOR import dis from BreakpointWatch import Breakpoint, Watch @@ -24,6 +23,15 @@ gRecursionLimit = 64 +try: + GENERATOR_AND_COROUTINE_FLAGS = ( + inspect.CO_GENERATOR | inspect.CO_COROUTINE | + inspect.CO_ASYNC_GENERATOR + ) +except AttributeError: + # Python < 3.7 + GENERATOR_AND_COROUTINE_FLAGS = inspect.CO_GENERATOR + def printerr(s): """ @@ -84,6 +92,7 @@ self.skipFrames = 0 self.isBroken = False + self.isException = False self.cFrame = None # current frame we are at @@ -105,7 +114,7 @@ # Use it like this: # if hasattr(sys, 'breakpoint): sys.breakpoint() sys.breakpoint = self.set_trace - if sys.version_info[:2] >= (3, 7): + if sys.version_info >= (3, 7): sys.breakpointhook = self.set_trace def __eventPollTimer(self): @@ -136,7 +145,7 @@ Public method to return the locals dictionary of the current frame or a frame below. - @keyparam frmnr distance of frame to get locals dictionary of. 0 is + @param frmnr distance of frame to get locals dictionary of. 0 is the current frame (int) @return locals dictionary of the frame """ @@ -151,7 +160,7 @@ Public method to store the locals into the frame, so an access to frame.f_locals returns the last data. - @keyparam frmnr distance of frame to store locals dictionary to. 0 is + @param frmnr distance of frame to store locals dictionary to. 0 is the current frame (int) """ cf = self.currentFrame @@ -309,8 +318,11 @@ if event == 'line': if self.stop_here(frame) or self.break_here(frame): - if (self.stop_everywhere and frame.f_back and - frame.f_back.f_code.co_name == "prepareJsonCommand"): + if ( + self.stop_everywhere and + frame.f_back and + frame.f_back.f_code.co_name == "prepareJsonCommand" + ): # Just stepped into print statement, so skip these frames self._set_stopinfo(None, frame.f_back) else: @@ -318,19 +330,33 @@ return self.trace_dispatch if event == 'call': - if (self.stop_here(frame) or - self.__checkBreakInFrame(frame) or - Watch.watches != []): + if ( + self.stop_here(frame) or + self.__checkBreakInFrame(frame) or + Watch.watches != [] + ): + return self.trace_dispatch + elif ( + self.stopframe and + frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS + ): return self.trace_dispatch else: # No need to trace this function return None if event == 'return': - if frame == self.returnframe: - # Only true if we didn't stopped in this frame, because it's + if self.stop_here(frame) or frame == self.returnframe: + # Ignore return events in generator except when stepping. + if ( + self.stopframe and + frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS + ): + return self.trace_dispatch + # Only true if we didn't stop in this frame, because it's # belonging to the eric debugger. - self._set_stopinfo(None, frame.f_back) + if self.stopframe is frame and self.stoplineno != -1: + self._set_stopinfo(None, frame.f_back) return None if event == 'exception': @@ -339,16 +365,23 @@ # skip the internal StopIteration exception (with no traceback) # triggered by a subiterator run with the 'yield from' # statement. - if not (frame.f_code.co_flags & CO_GENERATOR and - arg[0] is StopIteration and arg[2] is None): + if not ( + frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS and + arg[0] is StopIteration and + arg[2] is None + ): self.user_exception(arg) # Stop at the StopIteration or GeneratorExit exception when the # user has set stopframe in a generator by issuing a return # command, or a next/until command at the last statement in the # generator before the exception. - elif (self.stopframe and frame is not self.stopframe and - self.stopframe.f_code.co_flags & CO_GENERATOR and - arg[0] in (StopIteration, GeneratorExit)): + elif ( + self.stopframe and + frame is not self.stopframe and + (self.stopframe.f_code.co_flags & + GENERATOR_AND_COROUTINE_FLAGS) and + arg[0] in (StopIteration, GeneratorExit) + ): self.user_exception(arg) return None @@ -362,6 +395,7 @@ print('DebugBase.trace_dispatch:' # __IGNORE_WARNING_M801__ ' unknown debugging event: ', repr(event)) + return self.trace_dispatch def set_trace(self, frame=None): @@ -372,7 +406,7 @@ Because of jump optimizations it's not possible to use sys.breakpoint() as last instruction in a function or method. - @keyparam frame frame to start debugging from + @param frame frame to start debugging from @type frame object """ if frame is None: @@ -383,7 +417,7 @@ frame.f_trace = self.trace_dispatch while frame.f_back is not None: # stop at eric's debugger frame or a threading bootstrap - if (frame.f_back.f_code == stopOnHandleCommand): + if frame.f_back.f_code == stopOnHandleCommand: frame.f_trace = self.trace_dispatch break @@ -420,17 +454,23 @@ sys.settrace(None) sys.setprofile(None) - def run(self, cmd, globalsDict=None, localsDict=None, debug=True): + def run(self, cmd, globalsDict=None, localsDict=None, debug=True, + closeSession=True): """ Public method to start a given command under debugger control. @param cmd command / code to execute under debugger control @type str or CodeType - @keyparam globalsDict dictionary of global variables for cmd + @param globalsDict dictionary of global variables for cmd + @type dict + @param localsDict dictionary of local variables for cmd @type dict - @keyparam localsDict dictionary of local variables for cmd - @type dict - @keyparam debug flag if command should run under debugger control + @param debug flag if command should run under debugger control + @type bool + @return exit code of the program + @rtype int + @param closeSession flag indicating to close the debugger session + at exit @type bool """ if globalsDict is None: @@ -453,20 +493,24 @@ try: exec(cmd, globalsDict, localsDict) # secok atexit._run_exitfuncs() - self._dbgClient.progTerminated(0) + self._dbgClient.progTerminated(0, closeSession=closeSession) + exitcode = 0 except SystemExit: atexit._run_exitfuncs() excinfo = sys.exc_info() exitcode, message = self.__extractSystemExitMessage(excinfo) - self._dbgClient.progTerminated(exitcode, message) + self._dbgClient.progTerminated(exitcode, message=message, + closeSession=closeSession) except Exception: excinfo = sys.exc_info() self.user_exception(excinfo, True) + exitcode = 242 finally: self.quitting = True sys.settrace(None) + return exitcode - def _set_stopinfo(self, stopframe, returnframe): + def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): """ Protected method to update the frame pointers. @@ -474,9 +518,17 @@ @type frame object @param returnframe the frame object where to stop on a function return @type frame object + @param stoplineno line number to stop at. If stoplineno is greater than + or equal to 0, then stop at line greater than or equal to the + stopline. If stoplineno is -1, then don't stop at all. + @type int """ self.stopframe = stopframe self.returnframe = returnframe + # stoplineno >= 0 means: stop at line >= the stoplineno + # stoplineno -1 means: don't stop at all + self.stoplineno = stoplineno + if returnframe is not None: # Ensure to be able to stop on the return frame returnframe.f_trace = self.trace_dispatch @@ -491,13 +543,30 @@ """ # Here we only set a new stop frame if it is a normal continue. if not special: - self._set_stopinfo(None, None) + self._set_stopinfo(None, None, -1) # Disable tracing if not started in debug mode if not self._dbgClient.debugging: sys.settrace(None) sys.setprofile(None) + def set_until(self, frame=None, lineno=None): + """ + Public method to stop when the line with the lineno greater than the + current one is reached or when returning from current frame. + + @param frame reference to the frame object + @type frame object + @param lineno line number to continue to + @type int + """ + # the name "until" is borrowed from gdb + if frame is None: + frame = self.currentFrame + if lineno is None: + lineno = frame.f_lineno + 1 + self._set_stopinfo(frame, frame, lineno) + def set_step(self): """ Public method to stop after one line of code. @@ -534,7 +603,7 @@ try: self.currentFrame.f_lineno = lineno stack = self.getStack(self.currentFrame) - self._dbgClient.sendResponseLine(stack) + self._dbgClient.sendResponseLine(stack, self.name) except Exception as e: printerr(e) @@ -692,9 +761,9 @@ """ Public method to get the stack. - @keyparam frame frame object to inspect + @param frame frame object to inspect @type frame object or list - @keyparam applyTrace flag to assign trace function to fr.f_trace + @param applyTrace flag to assign trace function to fr.f_trace @type bool @return list of lists with file name (string), line number (integer) and function name (string) @@ -720,8 +789,9 @@ fname = self._dbgClient.absPath(self.fix_frame_filename(fr)) # Always show at least one stack frame, even if it's from eric. if stack and os.path.basename(fname).startswith( - ("DebugBase.py", "DebugClientBase.py", - "ThreadExtension.py", "threading.py")): + ("DebugBase.py", "DebugClientBase.py", + "ThreadExtension.py", "threading.py") + ): break fline = tb_lineno or fr.f_lineno @@ -773,19 +843,21 @@ self._dbgClient.currentThread = self self._dbgClient.currentThreadExec = self - self._dbgClient.sendResponseLine(stack) + self._dbgClient.sendResponseLine(stack, self.name) self._dbgClient.eventLoop() self.isBroken = False self._dbgClient.unlockClient() + self._dbgClient.dumpThreadList() + def user_exception(self, excinfo, unhandled=False): """ Public method reimplemented to report an exception to the debug server. @param excinfo details about the exception @type tuple(Exception, excval object, traceback frame object) - @keyparam unhandled flag indicating an uncaught exception + @param unhandled flag indicating an uncaught exception @type bool """ exctype, excval, exctb = excinfo @@ -825,7 +897,7 @@ if realSyntaxError: self._dbgClient.sendSyntaxError( - message, filename, lineno, charno) + message, filename, lineno, charno, self.name) self._dbgClient.eventLoop() return @@ -865,6 +937,7 @@ self.stop_everywhere = False self.isBroken = True + self.isException = True disassembly = None stack = [] @@ -879,7 +952,7 @@ self._dbgClient.lockClient() self._dbgClient.currentThread = self self._dbgClient.currentThreadExec = self - self._dbgClient.sendException(exctypetxt, excvaltxt, stack) + self._dbgClient.sendException(exctypetxt, excvaltxt, stack, self.name) self._dbgClient.setDisassembly(disassembly) self._dbgClient.dumpThreadList() @@ -891,11 +964,14 @@ self.skipFrames = 0 self.isBroken = False + self.isException = False stop_everywhere = self.stop_everywhere self.stop_everywhere = False self.eventPollFlag = False self._dbgClient.unlockClient() self.stop_everywhere = stop_everywhere + + self._dbgClient.dumpThreadList() def __extractExceptionName(self, exctype): """ @@ -1009,6 +1085,9 @@ elif isinstance(code, int): exitcode = code message = "" + elif code is None: + exitcode = 0 + message = "" else: exitcode = 1 message = str(code) @@ -1017,7 +1096,7 @@ message = str(excval) return exitcode, message - + def stop_here(self, frame): """ Public method reimplemented to filter out debugger files. @@ -1033,9 +1112,11 @@ if self.__skipFrame(frame): return False - return (self.stop_everywhere or - frame is self.stopframe or - frame is self.returnframe) + if frame is self.stopframe: + if self.stoplineno == -1: + return False + return frame.f_lineno >= self.stoplineno + return self.stop_everywhere or frame is self.returnframe def tracePythonLibs(self, enable): """