Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork33.7k
gh-106670: Allow Pdb to move between chained exceptions#106676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
d39fbce920af94d24ed3c63a734766a3944b4f79ade3b9e4a948551c9ba7bb83514edc4e57c329e7354bFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -175,8 +175,8 @@ slightly different way: | ||
| .. function:: pm() | ||
| Enter post-mortem debugging of theexception found in | ||
| :data:`sys.last_exc`. | ||
| The ``run*`` functions and :func:`set_trace` are aliases for instantiating the | ||
| @@ -639,6 +639,55 @@ can be overridden by the local file. | ||
| Print the return value for the last return of the current function. | ||
| .. pdbcommand:: exceptions [excnumber] | ||
iritkatriel marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| List or jump between chained exceptions. | ||
| When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception | ||
| instead of a traceback, it allows the user to move between the | ||
| chained exceptions using ``exceptions`` command to list exceptions, and | ||
| ``exception <number>`` to switch to that exception. | ||
iritkatriel marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| 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) | ||
| .. versionadded:: 3.13 | ||
iritkatriel marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| .. rubric:: Footnotes | ||
| .. [1] Whether a frame is considered to originate in a certain module | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -85,6 +85,7 @@ | ||
| import traceback | ||
| import linecache | ||
| from contextlib import contextmanager | ||
| from typing import Union | ||
| @@ -205,10 +206,15 @@ def namespace(self): | ||
| # line_prefix = ': ' # Use this to get the old situation back | ||
| line_prefix = '\n-> ' # Probably a better default | ||
| class Pdb(bdb.Bdb, cmd.Cmd): | ||
| _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) | ||
| @@ -256,6 +262,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 | ||
| @@ -414,7 +423,64 @@ def preloop(self): | ||
| self.message('display %s: %r [old: %r]' % | ||
| (expr, newvalue, oldvalue)) | ||
| 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, current = tb_or_exc.__traceback__, tb_or_exc | ||
| 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 | ||
| return tuple(reversed(_exceptions)), traceback | ||
| @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. | ||
| if Pdb._previous_sigint_handler: | ||
| try: | ||
| @@ -423,14 +489,17 @@ def interaction(self, frame, traceback): | ||
| 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 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() | ||
| def displayhook(self, obj): | ||
| """Custom displayhook for the exec in default(), which prevents | ||
| @@ -1073,6 +1142,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 rather than a traceback." | ||
| ) | ||
| return | ||
| if not arg: | ||
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| 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: | ||
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| self.error("No exception with that number") | ||
| def do_up(self, arg): | ||
| """u(p) [count] | ||
| @@ -1890,11 +1997,15 @@ def set_trace(*, header=None): | ||
| # Post-Mortem interface | ||
| def post_mortem(t=None): | ||
| """Enter post-mortem debugging of the given *traceback*, or *exception* | ||
iritkatriel marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| 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). | ||
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
| 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: | ||
| @@ -1911,12 +2022,8 @@ def post_mortem(t=None): | ||
| p.interaction(None, t) | ||
| def pm(): | ||
| """Enter post-mortem debugging of the traceback found in sys.last_exc.""" | ||
| post_mortem(sys.last_exc) | ||
| # Main program for testing | ||
| @@ -1996,8 +2103,7 @@ def main(): | ||
| traceback.print_exc() | ||
| print("Uncaught exception. Entering post mortem debugging") | ||
| print("Running 'cont' or 'step' will restart the program") | ||
| pdb.interaction(None, e) | ||
| print("Post mortem debugger finished. The " + target + | ||
| " will be restarted") | ||
Uh oh!
There was an error while loading.Please reload this page.