diff --git a/Lib/bdb.py b/Lib/bdb.py --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -24,7 +24,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] + ">": @@ -43,6 +43,7 @@ self._set_stopinfo(None, None) def trace_dispatch(self, frame, event, arg): + self._curframe = frame if self.quitting: return # None if event == 'line': @@ -83,12 +84,24 @@ 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 + self._curframe = None + # Issue #14728: 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): @@ -117,10 +130,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): @@ -174,6 +185,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 = 0 @@ -191,14 +212,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 @@ -7,6 +7,7 @@ import unittest import subprocess import textwrap +import string from test import test_support # This little helper class is essential for testing pdb under doctest. @@ -15,11 +16,22 @@ class PdbTestCase(unittest.TestCase): + def safe_unlink(self, filename): + self.addCleanup(test_support.unlink, filename) + base, ext = os.path.splitext(filename) + if ext == '.py': + # avoid a file timestamp conflict by removing an existing + # byte-compiled module of the same name used in the previous test + bname = filename + 'c' + test_support.unlink(bname) + self.addCleanup(test_support.unlink, bname) + def run_pdb(self, script, commands): """Run 'script' lines with pdb and the pdb 'commands'.""" filename = 'main.py' with open(filename, 'w') as f: f.write(textwrap.dedent(script)) + self.safe_unlink('main.py') cmd = [sys.executable, '-m', 'pdb', filename] stdout = stderr = None proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, @@ -61,9 +73,186 @@ """ with open('bar.py', 'w') as f: f.write(textwrap.dedent(bar)) + self.safe_unlink('bar.py') stdout, stderr = self.run_pdb(script, commands) - self.assertIn('main.py(5)foo()->None', stdout.split('\n')[-3], - 'Fail to step into the caller after a return') + self.assertTrue( + any('main.py(5)foo()->None' in l for l in stdout.splitlines()), + 'Fail to step into the caller after a return') + + def test_issue14728_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 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.safe_unlink('bar.py') + for cmd in ('step', 'next', 'until 999', 'return'): + 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') + + def test_issue14728_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.safe_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_issue14728_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.safe_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_issue14728_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 + quit + """ + bar = """ + def value(): + return 5 + """ + with open('bar.py', 'w') as f: + f.write(textwrap.dedent(bar)) + self.safe_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 until') + + def test_issue14728_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() + """ + 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.safe_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') class PdbTestInput(object):