Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork32.4k
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 from1 commit
d39fbce
920af94
d24ed3c
63a7347
66a3944
b4f79ad
e3b9e4a
948551c
9ba7bb8
3514edc
4e57c32
9e7354b
File 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
This lets Pdb receive an exception, instead of a traceback, and whenthis is the case and the exception are chained, the new `exceptions` commandallows 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 # EOnly 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 <number>``.This also change the default behavior of ``pdb.pm()``, as well as`python -m pdb <script.py>` to receive `sys.last_exc` so that chainedexception 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__``. Inthe same vein, if we encounter an exception that has``__suppress_context__`` (like when ``raise ... from None``), we do stopwalking 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.Closesgh-106670
- Loading branch information
Uh oh!
There was an error while loading.Please reload this page.
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,54 @@ 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 will now allow the user to move between the | ||
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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) | ||
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 |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. is this comment in the right place? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. moved and rephrased. | ||
_max_chained_exception_depth = 999 | ||
_chained_exceptions = tuple() | ||
_chained_exception_index = 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Why do we put these in the class? Is there any case As for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I rarely see multiple Pdb instances used so I don't have any particular preferences. Though I don't quite like global variables, and I have the impression that attaching constant to the only class that use them make things a tiny bit more self contained and discoverable. I still did both of the changes you suggested though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I obviously misrepresented myself. I meant use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Ah, sure, I moved it back as a class variable. | ||
_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,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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. We should keep the single line between methods. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Done. | ||
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." | ||
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
) | ||
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 +1967,16 @@ 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 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. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
""" | ||
# 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.""" | ||
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page.
Carreau marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
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") | ||
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.