Skip to content
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

gh-106670: Allow Pdb to move between chained exceptions #106676

Merged
merged 12 commits into from
Aug 28, 2023
52 changes: 50 additions & 2 deletions Doc/library/pdb.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 resolved Hide resolved

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 resolved Hide resolved
chained exceptions using ``exceptions`` command to list exceptions, and
``exception <number>`` to switch to that exception.
iritkatriel marked this conversation as resolved.
Show resolved Hide resolved


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 resolved Hide resolved
.. rubric:: Footnotes

.. [1] Whether a frame is considered to originate in a certain module
Expand Down
155 changes: 131 additions & 24 deletions Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import traceback
import linecache

from contextlib import contextmanager
from typing import Union


Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comment in the right place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved and rephrased.

_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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be inside the _hold_exceptions() block?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not need to, I reduced the spam of the context manager.

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
Expand Down Expand Up @@ -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.
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]

Expand Down Expand Up @@ -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.
Show resolved Hide resolved
"""
# handling the default
if t is None:
Expand All @@ -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.
Show resolved Hide resolved
Carreau marked this conversation as resolved.
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
Expand Down Expand Up @@ -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")

Expand Down
Loading