diff --git a/Lib/bdb.py b/Lib/bdb.py --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -3,6 +3,7 @@ import fnmatch import sys import os +from inspect import CO_GENERATOR __all__ = ["BdbQuit", "Bdb", "Breakpoint"] @@ -75,22 +76,38 @@ if not (self.stop_here(frame) or self.break_anywhere(frame)): # No need to trace this function return # None + # Ignore call events in generator except when stepping. + if self.stopframe and frame.f_code.co_flags & CO_GENERATOR: + return self.trace_dispatch self.user_call(frame, arg) if self.quitting: raise BdbQuit return self.trace_dispatch def dispatch_return(self, frame, arg): 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 & CO_GENERATOR: + return self.trace_dispatch try: self.frame_returning = frame self.user_return(frame, arg) finally: self.frame_returning = None if self.quitting: raise BdbQuit + # The user issued a 'next' or 'until' command. + if self.stopframe is frame and self.stoplineno != -1: + self._set_stopinfo(None, None) return self.trace_dispatch def dispatch_exception(self, frame, arg): - if self.stop_here(frame): + # 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. + if (self.stop_here(frame) or + (self.stopframe and + self.stopframe.f_code.co_flags & CO_GENERATOR and + arg[0] in (StopIteration, GeneratorExit))): self.user_exception(frame, arg) if self.quitting: raise BdbQuit return self.trace_dispatch @@ -115,10 +132,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 not self.stopframe: + return True return False def break_here(self, frame): @@ -207,7 +222,10 @@ def set_return(self, frame): """Stop when returning from the given frame.""" - self._set_stopinfo(frame.f_back, frame) + if frame.f_code.co_flags & CO_GENERATOR: + self._set_stopinfo(frame, None, -1) + else: + self._set_stopinfo(frame.f_back, frame) def set_trace(self, frame=None): """Start debugging from `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 @@ -596,6 +596,223 @@ (Pdb) continue """ +def test_next_until_return_at_return_event(): + """Test that pdb stops after a next/until/return issued at a return debug event. + + >>> def test_function_2(): + ... x = 1 + ... x = 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True).set_trace() + ... test_function_2() + ... test_function_2() + ... test_function_2() + ... end = 1 + + >>> with PdbTestInput(['break test_function_2', + ... 'continue', + ... 'return', + ... 'next', + ... 'continue', + ... 'return', + ... 'until', + ... 'continue', + ... 'return', + ... 'return', + ... 'continue']): + ... test_function() + > (3)test_function() + -> test_function_2() + (Pdb) break test_function_2 + Breakpoint 1 at :1 + (Pdb) continue + > (2)test_function_2() + -> x = 1 + (Pdb) return + --Return-- + > (3)test_function_2()->None + -> x = 2 + (Pdb) next + > (4)test_function() + -> test_function_2() + (Pdb) continue + > (2)test_function_2() + -> x = 1 + (Pdb) return + --Return-- + > (3)test_function_2()->None + -> x = 2 + (Pdb) until + > (5)test_function() + -> test_function_2() + (Pdb) continue + > (2)test_function_2() + -> x = 1 + (Pdb) return + --Return-- + > (3)test_function_2()->None + -> x = 2 + (Pdb) return + > (6)test_function() + -> end = 1 + (Pdb) continue + """ + +def test_pdb_next_command_for_generator(): + """Testing skip unwindng stack on yield for generators for "next" command + + >>> def test_gen(): + ... yield 0 + ... return 1 + ... yield 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True).set_trace() + ... it = test_gen() + ... try: + ... assert next(it) == 0 + ... next(it) + ... except StopIteration as ex: + ... assert ex.value == 1 + ... print("finished") + + >>> with PdbTestInput(['step', + ... 'step', + ... 'step', + ... 'next', + ... 'next', + ... 'step', + ... 'step', + ... 'continue']): + ... test_function() + > (3)test_function() + -> it = test_gen() + (Pdb) step + > (4)test_function() + -> try: + (Pdb) step + > (5)test_function() + -> assert next(it) == 0 + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) next + > (2)test_gen() + -> yield 0 + (Pdb) next + > (3)test_gen() + -> return 1 + (Pdb) step + --Return-- + > (3)test_gen()->1 + -> return 1 + (Pdb) step + StopIteration: 1 + > (6)test_function() + -> next(it) + (Pdb) continue + finished + """ + +def test_pdb_return_command_for_generator(): + """Testing no unwindng stack on yield for generators + for "return" command + + >>> def test_gen(): + ... yield 0 + ... return 1 + ... yield 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True).set_trace() + ... it = test_gen() + ... try: + ... assert next(it) == 0 + ... next(it) + ... except StopIteration as ex: + ... assert ex.value == 1 + ... print("finished") + + >>> with PdbTestInput(['step', + ... 'step', + ... 'step', + ... 'return', + ... 'step', + ... 'step', + ... 'continue']): + ... test_function() + > (3)test_function() + -> it = test_gen() + (Pdb) step + > (4)test_function() + -> try: + (Pdb) step + > (5)test_function() + -> assert next(it) == 0 + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) return + StopIteration: 1 + > (6)test_function() + -> next(it) + (Pdb) step + > (7)test_function() + -> except StopIteration as ex: + (Pdb) step + > (8)test_function() + -> assert ex.value == 1 + (Pdb) continue + finished + """ + +def test_pdb_until_command_for_generator(): + """Testing no unwindng stack on yield for generators + for "until" command if target breakpoing is not reached + + >>> def test_gen(): + ... yield 0 + ... yield 1 + ... yield 2 + + >>> def test_function(): + ... import pdb; pdb.Pdb(nosigint=True).set_trace() + ... for i in test_gen(): + ... print(i) + ... print("finished") + + >>> with PdbTestInput(['step', + ... 'until 4', + ... 'step', + ... 'step', + ... 'continue']): + ... test_function() + > (3)test_function() + -> for i in test_gen(): + (Pdb) step + --Call-- + > (1)test_gen() + -> def test_gen(): + (Pdb) until 4 + 0 + 1 + > (4)test_gen() + -> yield 2 + (Pdb) step + --Return-- + > (4)test_gen()->2 + -> yield 2 + (Pdb) step + > (4)test_function() + -> print(i) + (Pdb) continue + 2 + finished + """ + class PdbTestCase(unittest.TestCase):