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,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): @@ -115,10 +128,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 +182,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 +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 @@ -3,9 +3,11 @@ import imp import pdb import sys +import os import unittest import subprocess import textwrap +import string from test import support # This little helper class is essential for testing pdb under doctest. @@ -599,12 +601,22 @@ class PdbTestCase(unittest.TestCase): + def safe_unlink(self, filename): + self.addCleanup(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' + support.unlink(bname) + self.addCleanup(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.addCleanup(support.unlink, filename) + self.safe_unlink(filename) cmd = [sys.executable, '-m', 'pdb', filename] stdout = stderr = None with subprocess.Popen(cmd, stdout=subprocess.PIPE, @@ -661,12 +673,187 @@ """ with open('bar.py', 'w') as f: f.write(textwrap.dedent(bar)) - self.addCleanup(support.unlink, 'bar.py') + self.safe_unlink('bar.py') stdout, stderr = self.run_pdb(script, commands) 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 7 + 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('-> return result' 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') + def tearDown(self): support.unlink(support.TESTFN)