From d39fbceda41ab9d8ff03beabbe6e1f2ba505401d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 12 Jul 2023 11:22:20 +0200 Subject: [PATCH 01/11] Allow Pdb to move between chained exception. This lets Pdb receive an exception, instead of a traceback, and when this is the case and the exception are chained, the new `exceptions` command allows to both list (no arguments) and move between the chained exceptions. That is to say if you have something like def out(): try: middle() # B except Exception as e: raise ValueError("foo(): bar failed") # A def middle(): try: return inner(0) # D except Exception as e: raise ValueError("Middle fail") from e # C def inner(x): 1 / x # E Only A was reachable after calling `out()` and doing post mortem debug. With this all A-E points are reachable with a combination of up/down, and ``exception ``. This also change the default behavior of ``pdb.pm()``, as well as `python -m pdb ` to receive `sys.last_exc` so that chained exception navigation is enabled. We do follow the logic of the ``traceback`` module and handle the ``_context__`` and ``__cause__`` in the same way. That is to say, we try ``__cause__`` first, and if not present chain with ``__context__``. In the same vein, if we encounter an exception that has ``__suppress_context__`` (like when ``raise ... from None``), we do stop walking the chain. Some implementation notes: - We do handle cycle in exceptions - cleanup of references to tracebacks are not cleared in ``forget()``, as ``setup()`` and ``forget()`` are both for setting a single exception. - We do not handle sub-exceptions of exception groups. Closes gh-106670 --- Doc/library/pdb.rst | 52 ++- Lib/pdb.py | 95 ++++- Lib/test/test_pdb.py | 343 ++++++++++++++++++ ...-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst | 3 + 4 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index ef52370bff8058..0822531276c305 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -175,8 +175,8 @@ slightly different way: .. function:: pm() - Enter post-mortem debugging of the traceback found in - :data:`sys.last_traceback`. + Enter post-mortem debugging of the exception found in + :data:`sys.last_exc`. The ``run*`` functions and :func:`set_trace` are aliases for instantiating the @@ -639,6 +639,54 @@ can be overridden by the local file. Print the return value for the last return of the current function. +.. pdbcommand:: exceptions [excnumber] + + List or jump between chained exceptions. + + When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception + instead of a traceback, it will now allow the user to move between the + chained exceptions using ``exceptions`` command to list exceptions, and + ``exception `` to switch to that exception. + + + Example:: + + def out(): + try: + middle() + except Exception as e: + raise ValueError("reraise middle() error") from e + + def middle(): + try: + return inner(0) + except Exception as e: + raise ValueError("Middle fail") + + def inner(x): + 1 / x + + out() + + calling ``pdb.pm()`` will allow to move between exceptions:: + + > example.py(5)out() + -> raise ValueError("reraise middle() error") from e + + (Pdb) exceptions + 0 ZeroDivisionError('division by zero') + 1 ValueError('Middle fail') + > 2 ValueError('reraise middle() error') + + (Pdb) exceptions 0 + > example.py(16)inner() + -> 1 / x + + (Pdb) up + > example.py(10)middle() + -> return inner(0) + + .. rubric:: Footnotes .. [1] Whether a frame is considered to originate in a certain module diff --git a/Lib/pdb.py b/Lib/pdb.py index 3db3e6a5be1a7b..4cc6c5e9d1c887 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -206,6 +206,10 @@ def namespace(self): line_prefix = '\n-> ' # Probably a better default class Pdb(bdb.Bdb, cmd.Cmd): + # the max number of chained exceptions + exception groups we accept to navigate. + _max_chained_exception_depth = 999 + _chained_exceptions = tuple() + _chained_exception_index = 0 _previous_sigint_handler = None @@ -414,8 +418,37 @@ def preloop(self): self.message('display %s: %r [old: %r]' % (expr, newvalue, oldvalue)) - def interaction(self, frame, traceback): + def interaction(self, frame, tb_or_exc): # Restore the previous signal handler at the Pdb prompt. + + _exceptions = [] + if isinstance(tb_or_exc, BaseException): + traceback, exception = tb_or_exc.__traceback__, tb_or_exc + current = exception + + while current is not None: + if current in _exceptions: + break + _exceptions.append(current) + if current.__cause__ is not None: + current = current.__cause__ + elif ( + current.__context__ is not None and not current.__suppress_context__ + ): + current = current.__context__ + + if len(_exceptions) >= self._max_chained_exception_depth: + self.message( + f"More than {self._max_chained_exception_depth}" + " chained exceptions found, not all exceptions" + "will be browsable with `exceptions`." + ) + break + else: + traceback = tb_or_exc + self._chained_exceptions = tuple(reversed(_exceptions)) + self._chained_exception_index = len(_exceptions) - 1 + if Pdb._previous_sigint_handler: try: signal.signal(signal.SIGINT, Pdb._previous_sigint_handler) @@ -432,6 +465,12 @@ def interaction(self, frame, traceback): self._cmdloop() self.forget() + # we can't put those in forget as otherwise they would + # be cleared on exception change + self._chained_exceptions = tuple() + self._chained_exception_index = 0 + + def displayhook(self, obj): """Custom displayhook for the exec in default(), which prevents assignment of the _ variable in the builtins. @@ -1073,6 +1112,44 @@ def _select_frame(self, number): self.print_stack_entry(self.stack[self.curindex]) self.lineno = None + def do_exceptions(self, arg): + """exceptions [number] + + List or change current exception in an exception chain. + + Without arguments, list all the current exception in the exception + chain. Exceptions will be numbered, with the current exception indicated + with an arrow. + + If given an integer as argument, switch to the exception at that index. + """ + if not self._chained_exceptions: + self.message( + "Did not find chained exceptions. To move between" + " exceptions, pdb/post_mortem must be given an exception" + " object instead of a traceback." + ) + return + if not arg: + for ix, exc in enumerate(self._chained_exceptions): + prompt = ">" if ix == self._chained_exception_index else " " + rep = repr(exc) + if len(rep) > 80: + rep = rep[:77] + "..." + self.message(f"{prompt} {ix:>3} {rep}") + else: + try: + number = int(arg) + except ValueError: + self.error("Argument must be an integer") + return + if 0 <= number < len(self._chained_exceptions): + self._chained_exception_index = number + self.setup(None, self._chained_exceptions[number].__traceback__) + self.print_stack_entry(self.stack[self.curindex]) + else: + self.error("No exception with that number") + def do_up(self, arg): """u(p) [count] @@ -1890,11 +1967,16 @@ def set_trace(*, header=None): # Post-Mortem interface def post_mortem(t=None): - """Enter post-mortem debugging of the given *traceback* object. + """Enter post-mortem debugging of the given *traceback*, or *exception* + object. If no traceback is given, it uses the one of the exception that is currently being handled (an exception must be being handled if the default is to be used). + + If `t` is an Exception and is a chained exception (i.e it has a __context__, + or a __cause__), pdb will be able to list and move to other exceptions in + the chain using the `exceptions` command """ # handling the default if t is None: @@ -1912,11 +1994,7 @@ def post_mortem(t=None): def pm(): """Enter post-mortem debugging of the traceback found in sys.last_traceback.""" - if hasattr(sys, 'last_exc'): - tb = sys.last_exc.__traceback__ - else: - tb = sys.last_traceback - post_mortem(tb) + post_mortem(sys.last_exc) # Main program for testing @@ -1996,8 +2074,7 @@ def main(): traceback.print_exc() print("Uncaught exception. Entering post mortem debugging") print("Running 'cont' or 'step' will restart the program") - t = e.__traceback__ - pdb.interaction(None, t) + pdb.interaction(None, e) print("Post mortem debugger finished. The " + target + " will be restarted") diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index a66953557e52dc..734b5c83cdff7d 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -826,6 +826,349 @@ def test_convenience_variables(): (Pdb) continue """ + +def test_post_mortem_chained(): + """Test post mortem traceback debugging of chained exception + + >>> def test_function_2(): + ... try: + ... 1/0 + ... finally: + ... print('Exception!') + + >>> def test_function_reraise(): + ... try: + ... test_function_2() + ... except ZeroDivisionError as e: + ... raise ZeroDivisionError('reraised') from e + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... test_function_reraise() + ... except Exception as e: + ... # same as pdb.post_mortem(e), but with custom pdb instance. + ... instance.reset() + ... instance.interaction(None, e) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exceptions 0', + ... 'up', + ... 'down', + ... 'exceptions 1', + ... 'up', + ... 'down', + ... 'exceptions -1', + ... 'exceptions 3', + ... 'up', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ZeroDivisionError: + ... print('Correctly reraised.') + Exception! + > (5)test_function_reraise() + -> raise ZeroDivisionError('reraised') from e + (Pdb) exceptions + 0 ZeroDivisionError('division by zero') + > 1 ZeroDivisionError('reraised') + (Pdb) exceptions 0 + > (3)test_function_2() + -> 1/0 + (Pdb) up + > (3)test_function_reraise() + -> test_function_2() + (Pdb) down + > (3)test_function_2() + -> 1/0 + (Pdb) exceptions 1 + > (5)test_function_reraise() + -> raise ZeroDivisionError('reraised') from e + (Pdb) up + > (5)test_function() + -> test_function_reraise() + (Pdb) down + > (5)test_function_reraise() + -> raise ZeroDivisionError('reraised') from e + (Pdb) exceptions -1 + *** No exception with that number + (Pdb) exceptions 3 + *** No exception with that number + (Pdb) up + > (5)test_function() + -> test_function_reraise() + (Pdb) exit + """ + + +def test_post_mortem_cause_no_context(): + """Test post mortem traceback debugging of chained exception + + >>> def main(): + ... try: + ... raise ValueError('Context Not Shown') + ... except Exception as e1: + ... raise ValueError("With Cause") from TypeError('The Cause') + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... # same as pdb.post_mortem(e), but with custom pdb instance. + ... instance.reset() + ... instance.interaction(None, e) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exceptions 1', + ... 'up', + ... 'down', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ValueError: + ... print('Ok.') + > (5)main() + -> raise ValueError("With Cause") from TypeError('The Cause') + (Pdb) exceptions + 0 TypeError('The Cause') + > 1 ValueError('With Cause') + (Pdb) exceptions 1 + > (5)main() + -> raise ValueError("With Cause") from TypeError('The Cause') + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (5)main() + -> raise ValueError("With Cause") from TypeError('The Cause') + (Pdb) exit""" + + +def test_post_mortem_context_of_the_cause(): + """Test post mortem traceback debugging of chained exception + + + >>> def main(): + ... try: + ... raise TypeError('Context of the cause') + ... except Exception as e1: + ... try: + ... raise ValueError('Root Cause') + ... except Exception as e2: + ... ex = e2 + ... raise ValueError("With Cause, and cause has context") from ex + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... # same as pdb.post_mortem(e), but with custom pdb instance. + ... instance.reset() + ... instance.interaction(None, e) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exceptions 2', + ... 'up', + ... 'down', + ... 'exceptions 3', + ... 'up', + ... 'down', + ... 'exceptions 4', + ... 'up', + ... 'down', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exceptions + 0 TypeError('Context of the cause') + 1 ValueError('Root Cause') + > 2 ValueError('With Cause, and cause has context') + (Pdb) exceptions 2 + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exceptions 3 + *** No exception with that number + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exceptions 4 + *** No exception with that number + (Pdb) up + > (5)test_function() + -> main() + (Pdb) down + > (9)main() + -> raise ValueError("With Cause, and cause has context") from ex + (Pdb) exit + """ + + +def test_post_mortem_from_none(): + """Test post mortem traceback debugging of chained exception + + In particular that cause from None (which sets __supress_context__ to True) + does not show context. + + + >>> def main(): + ... try: + ... raise TypeError('Context of the cause') + ... except Exception as e1: + ... raise ValueError("With Cause, and cause has context") from None + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... # same as pdb.post_mortem(e), but with custom pdb instance. + ... instance.reset() + ... instance.interaction(None, e) + + >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... 'exceptions', + ... 'exit', + ... ]): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (5)main() + -> raise ValueError("With Cause, and cause has context") from None + (Pdb) exceptions + > 0 ValueError('With Cause, and cause has context') + (Pdb) exit + """ + + +def test_post_mortem_complex(): + """Test post mortem traceback debugging of chained exception + + Test with simple and complex cycles, exception groups,... + + >>> def make_ex_with_stack(type_, *content, from_=None): + ... try: + ... raise type_(*content) from from_ + ... except Exception as out: + ... return out + ... + + >>> def cycle(): + ... try: + ... raise ValueError("Cycle Leaf") + ... except Exception as e: + ... raise e from e + ... + + >>> def tri_cycle(): + ... a = make_ex_with_stack(ValueError, "Cycle1") + ... b = make_ex_with_stack(ValueError, "Cycle2") + ... c = make_ex_with_stack(ValueError, "Cycle3") + ... + ... a.__cause__ = b + ... b.__cause__ = c + ... + ... raise c from a + ... + + >>> def cause(): + ... try: + ... raise ValueError("Cause Leaf") + ... except Exception as e: + ... raise e + ... + + >>> def context(n=10): + ... try: + ... raise ValueError(f"Context Leaf {n}") + ... except Exception as e: + ... if n == 0: + ... raise ValueError(f"With Context {n}") from e + ... else: + ... context(n - 1) + ... + + >>> def main(): + ... try: + ... cycle() + ... except Exception as e1: + ... try: + ... tri_cycle() + ... except Exception as e2: + ... ex = e2 + ... raise ValueError("With Context and With Cause") from ex + + + >>> def test_function(): + ... import pdb; + ... instance = pdb.Pdb(nosigint=True, readrc=False) + ... try: + ... main() + ... except Exception as e: + ... # same as pdb.post_mortem(e), but with custom pdb instance. + ... instance.reset() + ... instance.interaction(None, e) + + >>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ... ["exceptions", + ... "exceptions 0", + ... "exceptions 1", + ... "exceptions 2", + ... "exceptions 3", + ... "exit"], + ... ): + ... try: + ... test_function() + ... except ValueError: + ... print('Correctly reraised.') + > (9)main() + -> raise ValueError("With Context and With Cause") from ex + (Pdb) exceptions + 0 ValueError('Cycle2') + 1 ValueError('Cycle1') + 2 ValueError('Cycle3') + > 3 ValueError('With Context and With Cause') + (Pdb) exceptions 0 + > (3)make_ex_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 1 + > (3)make_ex_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 2 + > (3)make_ex_with_stack() + -> raise type_(*content) from from_ + (Pdb) exceptions 3 + > (9)main() + -> raise ValueError("With Context and With Cause") from ex + (Pdb) exit + """ + + def test_post_mortem(): """Test post mortem traceback debugging. diff --git a/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst b/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst new file mode 100644 index 00000000000000..e15f0e93bc6a50 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst @@ -0,0 +1,3 @@ +Add the new ``exceptions`` command the Pdb debugger to move between chained exceptions when using post mortem debugging. + + From 920af940498200f773cef2f5f68e74f968dcfdd8 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 16 Aug 2023 09:47:28 +0200 Subject: [PATCH 02/11] Move the exception logic to its own function. Also move the release of the list of exception to a context manager for security --- Lib/pdb.py | 75 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 4cc6c5e9d1c887..c3526bb68f155d 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -85,6 +85,7 @@ import traceback import linecache +from contextlib import contextmanager from typing import Union @@ -418,13 +419,19 @@ def preloop(self): self.message('display %s: %r [old: %r]' % (expr, newvalue, oldvalue)) - def interaction(self, frame, tb_or_exc): - # Restore the previous signal handler at the Pdb prompt. + def _get_tb_and_exceptions(self, tb_or_exc): + """ + Given a tracecack or an exception, return a tuple of chained exceptions + and current traceback to inspect. + This will deal with selecting the right ``__cause__`` or ``__context__`` + as well as handling cycles, and return a flattened list of exceptions we + can jump to with do_exceptions. + + """ _exceptions = [] if isinstance(tb_or_exc, BaseException): - traceback, exception = tb_or_exc.__traceback__, tb_or_exc - current = exception + traceback, current = tb_or_exc.__traceback__, tb_or_exc while current is not None: if current in _exceptions: @@ -446,29 +453,49 @@ def interaction(self, frame, tb_or_exc): break else: traceback = tb_or_exc - self._chained_exceptions = tuple(reversed(_exceptions)) - self._chained_exception_index = len(_exceptions) - 1 + return tuple(reversed(_exceptions)), traceback - if Pdb._previous_sigint_handler: - try: - signal.signal(signal.SIGINT, Pdb._previous_sigint_handler) - except ValueError: # ValueError: signal only works in main thread - pass - else: - Pdb._previous_sigint_handler = None - if self.setup(frame, traceback): - # no interaction desired at this time (happens if .pdbrc contains - # a command like "continue") + @contextmanager + def _hold_exceptions(self, exceptions): + """ + Context manager to ensure proper cleaning of exceptions references + + When given a chained exception instead of a traceback, + pdb may hold references to many objects which may leak memory. + + We use this context manager to make sure everything is properly cleaned + + """ + try: + self._chained_exceptions = exceptions + self._chained_exception_index = len(exceptions) - 1 + yield + finally: + # we can't put those in forget as otherwise they would + # be cleared on exception change + self._chained_exceptions = tuple() + self._chained_exception_index = 0 + + def interaction(self, frame, tb_or_exc): + # Restore the previous signal handler at the Pdb prompt. + _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) + with self._hold_exceptions(_chained_exceptions): + if Pdb._previous_sigint_handler: + try: + signal.signal(signal.SIGINT, Pdb._previous_sigint_handler) + except ValueError: # ValueError: signal only works in main thread + pass + else: + Pdb._previous_sigint_handler = None + if self.setup(frame, tb): + # no interaction desired at this time (happens if .pdbrc contains + # a command like "continue") + self.forget() + return + self.print_stack_entry(self.stack[self.curindex]) + self._cmdloop() self.forget() - return - self.print_stack_entry(self.stack[self.curindex]) - self._cmdloop() - self.forget() - # we can't put those in forget as otherwise they would - # be cleared on exception change - self._chained_exceptions = tuple() - self._chained_exception_index = 0 def displayhook(self, obj): From d24ed3c1e0f12d3b596cda15729787630454b456 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Wed, 23 Aug 2023 10:21:58 +0200 Subject: [PATCH 03/11] Make variable instance variable. And max chain depth a global one --- Lib/pdb.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index c3526bb68f155d..ed561fbb0376f0 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -206,12 +206,11 @@ def namespace(self): # line_prefix = ': ' # Use this to get the old situation back line_prefix = '\n-> ' # Probably a better default +MAX_CHAINED_EXCEPTION_DEPTH = 999 + + class Pdb(bdb.Bdb, cmd.Cmd): # the max number of chained exceptions + exception groups we accept to navigate. - _max_chained_exception_depth = 999 - _chained_exceptions = tuple() - _chained_exception_index = 0 - _previous_sigint_handler = None def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, @@ -261,6 +260,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, self.commands_bnum = None # The breakpoint number for which we are # defining a list + self._chained_exceptions = tuple() + self._chained_exception_index = 0 + def sigint_handler(self, signum, frame): if self.allow_kbdint: raise KeyboardInterrupt @@ -444,9 +446,9 @@ def _get_tb_and_exceptions(self, tb_or_exc): ): current = current.__context__ - if len(_exceptions) >= self._max_chained_exception_depth: + if len(_exceptions) >= MAX_CHAINED_EXCEPTION_DEPTH: self.message( - f"More than {self._max_chained_exception_depth}" + f"More than {MAX_CHAINED_EXCEPTION_DEPTH}" " chained exceptions found, not all exceptions" "will be browsable with `exceptions`." ) @@ -496,8 +498,6 @@ def interaction(self, frame, tb_or_exc): self._cmdloop() self.forget() - - def displayhook(self, obj): """Custom displayhook for the exec in default(), which prevents assignment of the _ variable in the builtins. From 66a39443aa4e8c35d07f6439ff4510ecb16d1351 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Thu, 24 Aug 2023 13:04:32 +0200 Subject: [PATCH 04/11] Move back MAX_CHAINED_EXCEPTION_DEPTH to class variable --- Lib/pdb.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index ed561fbb0376f0..04b2cb5db082ff 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -206,13 +206,16 @@ def namespace(self): # line_prefix = ': ' # Use this to get the old situation back line_prefix = '\n-> ' # Probably a better default -MAX_CHAINED_EXCEPTION_DEPTH = 999 class Pdb(bdb.Bdb, cmd.Cmd): # the max number of chained exceptions + exception groups we accept to navigate. _previous_sigint_handler = None + # Limit the maximum depth of chained exceptions, we should be handling cycles, + # but in case there are recursions, we stop at 999. + MAX_CHAINED_EXCEPTION_DEPTH = 999 + def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, nosigint=False, readrc=True): bdb.Bdb.__init__(self, skip=skip) @@ -446,9 +449,9 @@ def _get_tb_and_exceptions(self, tb_or_exc): ): current = current.__context__ - if len(_exceptions) >= MAX_CHAINED_EXCEPTION_DEPTH: + if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH: self.message( - f"More than {MAX_CHAINED_EXCEPTION_DEPTH}" + f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}" " chained exceptions found, not all exceptions" "will be browsable with `exceptions`." ) From b4f79adf771ff80db163a2a097d97fbff8beddca Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Aug 2023 05:03:01 -0700 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> --- Doc/library/pdb.rst | 2 +- Lib/pdb.py | 7 +++---- .../Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst | 4 +--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index 0822531276c305..d8d4989fc6d59d 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -644,7 +644,7 @@ can be overridden by the local file. List or jump between chained exceptions. When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception - instead of a traceback, it will now allow the user to move between the + instead of a traceback, it allows the user to move between the chained exceptions using ``exceptions`` command to list exceptions, and ``exception `` to switch to that exception. diff --git a/Lib/pdb.py b/Lib/pdb.py index 04b2cb5db082ff..8b11e89295bf73 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1157,7 +1157,7 @@ def do_exceptions(self, arg): self.message( "Did not find chained exceptions. To move between" " exceptions, pdb/post_mortem must be given an exception" - " object instead of a traceback." + " object rather than a traceback." ) return if not arg: @@ -2004,9 +2004,8 @@ def post_mortem(t=None): currently being handled (an exception must be being handled if the default is to be used). - If `t` is an Exception and is a chained exception (i.e it has a __context__, - or a __cause__), pdb will be able to list and move to other exceptions in - the chain using the `exceptions` command + If `t` is an exception object, the `exceptions` command makes it possible to + list and inspect its chained exceptions (if any). """ # handling the default if t is None: diff --git a/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst b/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst index e15f0e93bc6a50..0bb18312a673fc 100644 --- a/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst +++ b/Misc/NEWS.d/next/Library/2023-07-12-10-59-08.gh-issue-106670.goQ2Sy.rst @@ -1,3 +1 @@ -Add the new ``exceptions`` command the Pdb debugger to move between chained exceptions when using post mortem debugging. - - +Add the new ``exceptions`` command to the Pdb debugger. It makes it possible to move between chained exceptions when using post mortem debugging. From e3b9e4ab09c56d56c6c980d01cabcfc8caef555d Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Aug 2023 14:08:26 +0200 Subject: [PATCH 06/11] Adress some of the reviews --- Doc/library/pdb.rst | 1 + Lib/pdb.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index d8d4989fc6d59d..4be69cc7f0af65 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -639,6 +639,7 @@ can be overridden by the local file. Print the return value for the last return of the current function. +.. versionadded:: 3.7 .. pdbcommand:: exceptions [excnumber] List or jump between chained exceptions. diff --git a/Lib/pdb.py b/Lib/pdb.py index 8b11e89295bf73..87ca63f7af2487 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -209,7 +209,6 @@ def namespace(self): class Pdb(bdb.Bdb, cmd.Cmd): - # the max number of chained exceptions + exception groups we accept to navigate. _previous_sigint_handler = None # Limit the maximum depth of chained exceptions, we should be handling cycles, @@ -483,15 +482,16 @@ def _hold_exceptions(self, exceptions): def interaction(self, frame, tb_or_exc): # Restore the previous signal handler at the Pdb prompt. + if Pdb._previous_sigint_handler: + try: + signal.signal(signal.SIGINT, Pdb._previous_sigint_handler) + except ValueError: # ValueError: signal only works in main thread + pass + else: + Pdb._previous_sigint_handler = None + _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) with self._hold_exceptions(_chained_exceptions): - if Pdb._previous_sigint_handler: - try: - signal.signal(signal.SIGINT, Pdb._previous_sigint_handler) - except ValueError: # ValueError: signal only works in main thread - pass - else: - Pdb._previous_sigint_handler = None if self.setup(frame, tb): # no interaction desired at this time (happens if .pdbrc contains # a command like "continue") From 948551c47b3b9a87e6015f66e2004b1a7a468bb7 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Aug 2023 14:17:34 +0200 Subject: [PATCH 07/11] add versionadded --- Doc/library/pdb.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index 4be69cc7f0af65..c8a823b8596614 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -639,9 +639,14 @@ can be overridden by the local file. Print the return value for the last return of the current function. -.. versionadded:: 3.7 .. pdbcommand:: exceptions [excnumber] + .. versionadded:: 3.13 + + ``exceptions [excnumber]`` command and passing exceptions by default to + :func:`post_mortem`. + + List or jump between chained exceptions. When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception From 9ba7bb8e8ae5a5fa459089dfc95b831d79a8d743 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Aug 2023 14:23:00 +0200 Subject: [PATCH 08/11] Fix doc --- Doc/library/pdb.rst | 7 +------ Lib/pdb.py | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index c8a823b8596614..3aaac15ee5780c 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -641,12 +641,6 @@ can be overridden by the local file. .. pdbcommand:: exceptions [excnumber] - .. versionadded:: 3.13 - - ``exceptions [excnumber]`` command and passing exceptions by default to - :func:`post_mortem`. - - List or jump between chained exceptions. When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception @@ -692,6 +686,7 @@ can be overridden by the local file. > example.py(10)middle() -> return inner(0) + .. versionadded:: 3.13 .. rubric:: Footnotes diff --git a/Lib/pdb.py b/Lib/pdb.py index 87ca63f7af2487..90f26a2eb99848 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -2022,7 +2022,7 @@ def post_mortem(t=None): p.interaction(None, t) def pm(): - """Enter post-mortem debugging of the traceback found in sys.last_traceback.""" + """Enter post-mortem debugging of the traceback found in sys.last_exc.""" post_mortem(sys.last_exc) From 3514edc10d8bcf263b51666736eebcee4b2f53c4 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Sat, 26 Aug 2023 21:29:14 +0200 Subject: [PATCH 09/11] add whatsnew --- Doc/whatsnew/3.13.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 8509e18a7d792e..d85c937c8ab786 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -189,6 +189,14 @@ typing check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in :gh:`104873`.) + +pdb +--- + +* Add ability to move between chained exceptions during post mortem debugging in :func:`pm()` using + the new ``exceptions [exc_number]`` command for Pdb. (Contributed by Matthias + Bussonnier in :gh:`106676`.) + Optimizations ============= From 4e57c328ad1bebfe6c0e26873d97419f6c4b2ea3 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 28 Aug 2023 00:13:51 -0700 Subject: [PATCH 10/11] Update Doc/whatsnew/3.13.rst Co-authored-by: Alex Waygood --- Doc/whatsnew/3.13.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index d85c937c8ab786..09c699243c0b93 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -193,7 +193,7 @@ typing pdb --- -* Add ability to move between chained exceptions during post mortem debugging in :func:`pm()` using +* Add ability to move between chained exceptions during post mortem debugging in :func:`~pdb.pm` using the new ``exceptions [exc_number]`` command for Pdb. (Contributed by Matthias Bussonnier in :gh:`106676`.) From 9e7354b925640bbc4de5ea73a2a2687334421705 Mon Sep 17 00:00:00 2001 From: Matthias Bussonnier Date: Mon, 28 Aug 2023 19:59:54 +0200 Subject: [PATCH 11/11] move paragraph --- Doc/whatsnew/3.13.rst | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 09c699243c0b93..91fa04e0c48350 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -158,6 +158,13 @@ pathlib :meth:`~pathlib.Path.is_dir`. (Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.) +pdb +--- + +* Add ability to move between chained exceptions during post mortem debugging in :func:`~pdb.pm` using + the new ``exceptions [exc_number]`` command for Pdb. (Contributed by Matthias + Bussonnier in :gh:`106676`.) + sqlite3 ------- @@ -189,14 +196,6 @@ typing check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in :gh:`104873`.) - -pdb ---- - -* Add ability to move between chained exceptions during post mortem debugging in :func:`~pdb.pm` using - the new ``exceptions [exc_number]`` command for Pdb. (Contributed by Matthias - Bussonnier in :gh:`106676`.) - Optimizations =============