diff --git a/Lib/bdb.py b/Lib/bdb.py --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -22,7 +22,7 @@ self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} - self.frame_returning = None + self._curframe = None def canonic(self, filename): if filename == "<" + filename[1:-1] + ">": @@ -41,6 +41,7 @@ self._set_stopinfo(None, None) def trace_dispatch(self, frame, event, arg): + self._curframe = frame if self.quitting: return # None if event == 'line': @@ -81,12 +82,23 @@ def dispatch_return(self, frame, arg): if self.stop_here(frame) or frame == self.returnframe: - try: - self.frame_returning = frame - self.user_return(frame, arg) - finally: - self.frame_returning = None + self.user_return(frame, arg) if self.quitting: raise BdbQuit + # The debugging session is terminated. + if frame is self.botframe: + self.stopframe = self.botframe + self.returnframe = None + self.stoplineno = -1 + # Issue #XXX: trace function not set, causing some Pdb commands to + # fail. + # Set the trace function in the caller (that may not have been set for + # performance reasons) when returning from the current frame after set, + # next, until, return commands. + elif (self.stopframe is None or self.stopframe is frame or + self.returnframe is frame): + if frame.f_back and not frame.f_back.f_trace: + frame.f_back.f_trace = self.trace_dispatch + self.stopframe = None return self.trace_dispatch def dispatch_exception(self, frame, arg): @@ -115,10 +127,8 @@ if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno - while frame is not None and frame is not self.stopframe: - if frame is self.botframe: - return True - frame = frame.f_back + if self.stopframe is None: + return True return False def break_here(self, frame): @@ -171,6 +181,16 @@ pass def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): + # Ensure that stopframe belongs to the stack frame in the interval + # [self.botframe, self._curframe] and that it gets a trace function. + frame = self._curframe + while stopframe and frame and frame is not stopframe: + if frame is self.botframe: + stopframe = self.botframe + break + frame = frame.f_back + if stopframe and not stopframe.f_trace: + stopframe.f_trace = self.trace_dispatch self.stopframe = stopframe self.returnframe = returnframe self.quitting = False @@ -191,14 +211,6 @@ def set_step(self): """Stop after one line of code.""" - # Issue #13183: pdb skips frames after hitting a breakpoint and running - # step commands. - # Restore the trace function in the caller (that may not have been set - # for performance reasons) when returning from the current frame. - if self.frame_returning: - caller_frame = self.frame_returning.f_back - if caller_frame and not caller_frame.f_trace: - caller_frame.f_trace = self.trace_dispatch self._set_stopinfo(None, None) def set_next(self, frame): diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -6,6 +6,7 @@ import unittest import subprocess import textwrap +import string from test import support # This little helper class is essential for testing pdb under doctest. @@ -667,6 +668,182 @@ any('main.py(5)foo()->None' in l for l in stdout.splitlines()), 'Fail to step into the caller after a return') + def test_issueXXX_1(self): + # test the step, next, until and return commands at a return statement + # when the caller frame does not have a trace function + script = """ + import bar + + def increment(arg): + v = bar.value() + result = arg + v + return result + + val = increment(100) + x = val + """ + commands = """ + break bar.value + continue + step + ${cmd} + quit + """ + bar = """ + def value(): + return 5 + """ + for cmd in ('step', 'next', 'until 999', 'return'): + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + stdout, stderr = self.run_pdb( script, + string.Template(commands).substitute(cmd=cmd)) + self.assertTrue( + any('-> result = arg + v' in l for l in stdout.splitlines()), + 'Fail to stop into the caller after a return') + self.addCleanup(support.unlink, 'bar.py') + + def test_issueXXX_2(self): + # test the step command after an exception when the caller frame does + # not have a trace function + script = """ + import bar + + def increment(arg): + v = 0 + try: + v = bar.value() + except: + pass + result = arg + v + return result + + val = increment(100) + x = val + """ + commands = """ + break bar.value + continue + step + step + step + quit + """ + bar = """ + def value(): + x = 1 / 0 + return + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + stdout, stderr = self.run_pdb(script, commands) + self.assertTrue( + any('-> v = bar.value()' in l for l in stdout.splitlines()), + 'Fail to stop into the caller after an exception') + + def test_issueXXX_3(self): + # test the next command when the selected frame does not have a trace + # function + script = """ + import bar + + def increment(arg): + v = bar.value() + result = arg + v + return result + + val = increment(100) + x = val + """ + commands = """ + break bar.value + continue + up + next + quit + """ + bar = """ + def value(): + return 5 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + stdout, stderr = self.run_pdb(script, commands) + self.assertTrue( + any('-> result = arg + v' in l for l in stdout.splitlines()), + 'Fail to stop into the selected frame after next') + + def test_issueXXX_4(self): + # test the until command when the selected frame does not have a trace + # function + script = """ + import bar + + def increment(arg): + v = bar.value() + result = arg + v + return result + + val = increment(100) + x = val + """ + commands = """ + break bar.value + continue + up + until 7 + quit + """ + bar = """ + def value(): + return 5 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + stdout, stderr = self.run_pdb(script, commands) + self.assertTrue( + any('-> return result' in l for l in stdout.splitlines()), + 'Fail to stop into the selected frame after until') + + def test_issueXXX_5(self): + # test the return command when the selected frame does not have a trace + # function + script = """ + import bar + + def increment(arg): + v = bar.value() + result = arg + v + return result + + def foo(): + val = increment(100) + + foo() + x = val + """ + commands = """ + break bar.value + continue + up + return + quit + """ + bar = """ + def value(): + return 5 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.addCleanup(support.unlink, 'bar.py') + stdout, stderr = self.run_pdb(script, commands) + self.assertTrue( + any('-> val = increment(100)' in l for l in stdout.splitlines()), + 'Fail to stop into the selected frame after return') + def tearDown(self): support.unlink(support.TESTFN)