diff --git a/Lib/bdb.py b/Lib/bdb.py --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -4,6 +4,9 @@ import fnmatch import sys import os +from inspect import CO_GENERATOR + + __all__ = ["BdbQuit", "Bdb", "Breakpoint"] class BdbQuit(Exception): @@ -75,7 +78,8 @@ class Bdb: if not (self.stop_here(frame) or self.break_anywhere(frame)): # No need to trace this function return # None - self.user_call(frame, arg) + if self.skip_yield is None and self.skip_yield != frame: + self.user_call(frame, arg) if self.quitting: raise BdbQuit return self.trace_dispatch @@ -83,13 +87,20 @@ class Bdb: if self.stop_here(frame) or frame == self.returnframe: try: self.frame_returning = frame - self.user_return(frame, arg) + skip = self.skip_yield is not None + if skip: + skip = (self.skip_yield == frame or + self.skip_yield == self.returnframe) + if not skip: + self.user_return(frame, arg) finally: self.frame_returning = None if self.quitting: raise BdbQuit return self.trace_dispatch def dispatch_exception(self, frame, arg): + if isinstance(arg[0], (StopIteration, GeneratorExit)): + self.skip_yield = None if self.stop_here(frame): self.user_exception(frame, arg) if self.quitting: raise BdbQuit @@ -115,6 +126,8 @@ class Bdb: if self.stoplineno == -1: return False return frame.f_lineno >= self.stoplineno + if self.skip_yield is not None: + return False while frame is not None and frame is not self.stopframe: if frame is self.botframe: return True @@ -170,13 +183,23 @@ class Bdb: but only if we are to stop at or just below this level.""" pass - def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): + def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, + *, skip_yield=False): self.stopframe = stopframe self.returnframe = returnframe self.quitting = False # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don't stop at all self.stoplineno = stoplineno + if skip_yield: + frame = returnframe or stopframe + code = frame.f_code + if code is not None: + isgenerator = code.co_flags & CO_GENERATOR + if isgenerator: + self.skip_yield = frame + return + self.skip_yield = None # Derived classes and clients can call the following methods # to affect the stepping state. @@ -187,7 +210,7 @@ class Bdb: # the name "until" is borrowed from gdb if lineno is None: lineno = frame.f_lineno + 1 - self._set_stopinfo(frame, frame, lineno) + self._set_stopinfo(frame, frame, lineno, skip_yield=True) def set_step(self): """Stop after one line of code.""" @@ -203,11 +226,11 @@ class Bdb: def set_next(self, frame): """Stop on the next line in or below the given frame.""" - self._set_stopinfo(frame, None) + self._set_stopinfo(frame, None, skip_yield=True) def set_return(self, frame): """Stop when returning from the given frame.""" - self._set_stopinfo(frame.f_back, frame) + self._set_stopinfo(frame.f_back, frame, skip_yield=True) 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,160 @@ def test_pdb_run_with_code_object(): (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 + > (6)test_function() + -> next(it) + (Pdb) step + --Call-- + > (2)test_gen() + -> yield 0 + (Pdb) step + > (3)test_gen() + -> return 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):