-
-
Notifications
You must be signed in to change notification settings - Fork 33.2k
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 our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
d39fbce
920af94
d24ed3c
63a7347
66a3944
b4f79ad
e3b9e4a
948551c
9ba7bb8
3514edc
4e57c32
9e7354b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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,16 @@ 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): | ||
|
|
||
|
|
||
| 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) | ||
|
|
@@ -256,6 +263,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,23 +424,82 @@ def preloop(self): | |
| self.message('display %s: %r [old: %r]' % | ||
| (expr, newvalue, oldvalue)) | ||
|
|
||
| def interaction(self, frame, traceback): | ||
| 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: | ||
| 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") | ||
| _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() | ||
|
|
||
| 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 instead of a traceback." | ||
Carreau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ) | ||
| return | ||
| if not arg: | ||
Carreau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 resolved
Hide resolved
|
||
| self.error("No exception with that number") | ||
|
|
||
| def do_up(self, arg): | ||
| """u(p) [count] | ||
|
|
||
|
|
@@ -1890,11 +1997,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* | ||
iritkatriel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 resolved
Hide resolved
|
||
| 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 | ||
Carreau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """ | ||
| # handling the default | ||
| if t is None: | ||
|
|
@@ -1912,11 +2024,7 @@ def post_mortem(t=None): | |
|
|
||
| def pm(): | ||
| """Enter post-mortem debugging of the traceback found in sys.last_traceback.""" | ||
Carreau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Carreau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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 +2104,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") | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.