Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 14a3b06

Browse files
Carreauiritkatriel
andcommitted
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 ``exceptions <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 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. - We ensure we do not hold references to exceptions too long with a new context manager. - Have the MAX_CHAINED_EXCEPTION_DEPTH class variable to control the maximum number we allow Co-authored-by: Irit Katriel <[email protected]>
1 parent 47d7eba commit 14a3b06

File tree

5 files changed

+527
-20
lines changed

5 files changed

+527
-20
lines changed

Doc/library/pdb.rst

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ slightly different way:
175175

176176
.. function:: pm()
177177

178-
Enter post-mortem debugging of the traceback found in
179-
:data:`sys.last_traceback`.
178+
Enter post-mortem debugging of the exception found in
179+
:data:`sys.last_exc`.
180180

181181

182182
The ``run*`` functions and :func:`set_trace` are aliases for instantiating the
@@ -639,6 +639,55 @@ can be overridden by the local file.
639639

640640
Print the return value for the last return of the current function.
641641

642+
.. pdbcommand:: exceptions [excnumber]
643+
644+
List or jump between chained exceptions.
645+
646+
When using ``pdb.pm()`` or ``Pdb.post_mortem(...)`` with a chained exception
647+
instead of a traceback, it allows the user to move between the
648+
chained exceptions using ``exceptions`` command to list exceptions, and
649+
``exception <number>`` to switch to that exception.
650+
651+
652+
Example::
653+
654+
def out():
655+
try:
656+
middle()
657+
except Exception as e:
658+
raise ValueError("reraise middle() error") from e
659+
660+
def middle():
661+
try:
662+
return inner(0)
663+
except Exception as e:
664+
raise ValueError("Middle fail")
665+
666+
def inner(x):
667+
1 / x
668+
669+
out()
670+
671+
calling ``pdb.pm()`` will allow to move between exceptions::
672+
673+
> example.py(5)out()
674+
-> raise ValueError("reraise middle() error") from e
675+
676+
(Pdb) exceptions
677+
0 ZeroDivisionError('division by zero')
678+
1 ValueError('Middle fail')
679+
> 2 ValueError('reraise middle() error')
680+
681+
(Pdb) exceptions 0
682+
> example.py(16)inner()
683+
-> 1 / x
684+
685+
(Pdb) up
686+
> example.py(10)middle()
687+
-> return inner(0)
688+
689+
.. versionadded:: 3.13
690+
642691
.. rubric:: Footnotes
643692

644693
.. [1] Whether a frame is considered to originate in a certain module

Doc/whatsnew/3.13.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,13 @@ pathlib
158158
:meth:`~pathlib.Path.is_dir`.
159159
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.)
160160

161+
pdb
162+
---
163+
164+
* Add ability to move between chained exceptions during post mortem debugging in :func:`pm()` using
165+
the new ``exceptions [exc_number]`` command for Pdb. (Contributed by Matthias
166+
Bussonnier in :gh:`106676`.)
167+
161168
sqlite3
162169
-------
163170

@@ -189,6 +196,7 @@ typing
189196
check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in
190197
:gh:`104873`.)
191198

199+
192200
Optimizations
193201
=============
194202

Lib/pdb.py

Lines changed: 124 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
import traceback
8686
import linecache
8787

88+
from contextlib import contextmanager
8889
from typing import Union
8990

9091

@@ -205,10 +206,15 @@ def namespace(self):
205206
# line_prefix = ': ' # Use this to get the old situation back
206207
line_prefix = '\n-> ' # Probably a better default
207208

208-
class Pdb(bdb.Bdb, cmd.Cmd):
209209

210+
211+
class Pdb(bdb.Bdb, cmd.Cmd):
210212
_previous_sigint_handler = None
211213

214+
# Limit the maximum depth of chained exceptions, we should be handling cycles,
215+
# but in case there are recursions, we stop at 999.
216+
MAX_CHAINED_EXCEPTION_DEPTH = 999
217+
212218
def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
213219
nosigint=False, readrc=True):
214220
bdb.Bdb.__init__(self, skip=skip)
@@ -256,6 +262,9 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
256262
self.commands_bnum = None # The breakpoint number for which we are
257263
# defining a list
258264

265+
self._chained_exceptions = tuple()
266+
self._chained_exception_index = 0
267+
259268
def sigint_handler(self, signum, frame):
260269
if self.allow_kbdint:
261270
raise KeyboardInterrupt
@@ -414,7 +423,64 @@ def preloop(self):
414423
self.message('display %s: %r [old: %r]' %
415424
(expr, newvalue, oldvalue))
416425

417-
def interaction(self, frame, traceback):
426+
def _get_tb_and_exceptions(self, tb_or_exc):
427+
"""
428+
Given a tracecack or an exception, return a tuple of chained exceptions
429+
and current traceback to inspect.
430+
431+
This will deal with selecting the right ``__cause__`` or ``__context__``
432+
as well as handling cycles, and return a flattened list of exceptions we
433+
can jump to with do_exceptions.
434+
435+
"""
436+
_exceptions = []
437+
if isinstance(tb_or_exc, BaseException):
438+
traceback, current = tb_or_exc.__traceback__, tb_or_exc
439+
440+
while current is not None:
441+
if current in _exceptions:
442+
break
443+
_exceptions.append(current)
444+
if current.__cause__ is not None:
445+
current = current.__cause__
446+
elif (
447+
current.__context__ is not None and not current.__suppress_context__
448+
):
449+
current = current.__context__
450+
451+
if len(_exceptions) >= self.MAX_CHAINED_EXCEPTION_DEPTH:
452+
self.message(
453+
f"More than {self.MAX_CHAINED_EXCEPTION_DEPTH}"
454+
" chained exceptions found, not all exceptions"
455+
"will be browsable with `exceptions`."
456+
)
457+
break
458+
else:
459+
traceback = tb_or_exc
460+
return tuple(reversed(_exceptions)), traceback
461+
462+
@contextmanager
463+
def _hold_exceptions(self, exceptions):
464+
"""
465+
Context manager to ensure proper cleaning of exceptions references
466+
467+
When given a chained exception instead of a traceback,
468+
pdb may hold references to many objects which may leak memory.
469+
470+
We use this context manager to make sure everything is properly cleaned
471+
472+
"""
473+
try:
474+
self._chained_exceptions = exceptions
475+
self._chained_exception_index = len(exceptions) - 1
476+
yield
477+
finally:
478+
# we can't put those in forget as otherwise they would
479+
# be cleared on exception change
480+
self._chained_exceptions = tuple()
481+
self._chained_exception_index = 0
482+
483+
def interaction(self, frame, tb_or_exc):
418484
# Restore the previous signal handler at the Pdb prompt.
419485
if Pdb._previous_sigint_handler:
420486
try:
@@ -423,14 +489,17 @@ def interaction(self, frame, traceback):
423489
pass
424490
else:
425491
Pdb._previous_sigint_handler = None
426-
if self.setup(frame, traceback):
427-
# no interaction desired at this time (happens if .pdbrc contains
428-
# a command like "continue")
492+
493+
_chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc)
494+
with self._hold_exceptions(_chained_exceptions):
495+
if self.setup(frame, tb):
496+
# no interaction desired at this time (happens if .pdbrc contains
497+
# a command like "continue")
498+
self.forget()
499+
return
500+
self.print_stack_entry(self.stack[self.curindex])
501+
self._cmdloop()
429502
self.forget()
430-
return
431-
self.print_stack_entry(self.stack[self.curindex])
432-
self._cmdloop()
433-
self.forget()
434503

435504
def displayhook(self, obj):
436505
"""Custom displayhook for the exec in default(), which prevents
@@ -1073,6 +1142,44 @@ def _select_frame(self, number):
10731142
self.print_stack_entry(self.stack[self.curindex])
10741143
self.lineno = None
10751144

1145+
def do_exceptions(self, arg):
1146+
"""exceptions [number]
1147+
1148+
List or change current exception in an exception chain.
1149+
1150+
Without arguments, list all the current exception in the exception
1151+
chain. Exceptions will be numbered, with the current exception indicated
1152+
with an arrow.
1153+
1154+
If given an integer as argument, switch to the exception at that index.
1155+
"""
1156+
if not self._chained_exceptions:
1157+
self.message(
1158+
"Did not find chained exceptions. To move between"
1159+
" exceptions, pdb/post_mortem must be given an exception"
1160+
" object rather than a traceback."
1161+
)
1162+
return
1163+
if not arg:
1164+
for ix, exc in enumerate(self._chained_exceptions):
1165+
prompt = ">" if ix == self._chained_exception_index else " "
1166+
rep = repr(exc)
1167+
if len(rep) > 80:
1168+
rep = rep[:77] + "..."
1169+
self.message(f"{prompt} {ix:>3} {rep}")
1170+
else:
1171+
try:
1172+
number = int(arg)
1173+
except ValueError:
1174+
self.error("Argument must be an integer")
1175+
return
1176+
if 0 <= number < len(self._chained_exceptions):
1177+
self._chained_exception_index = number
1178+
self.setup(None, self._chained_exceptions[number].__traceback__)
1179+
self.print_stack_entry(self.stack[self.curindex])
1180+
else:
1181+
self.error("No exception with that number")
1182+
10761183
def do_up(self, arg):
10771184
"""u(p) [count]
10781185
@@ -1890,11 +1997,15 @@ def set_trace(*, header=None):
18901997
# Post-Mortem interface
18911998

18921999
def post_mortem(t=None):
1893-
"""Enter post-mortem debugging of the given *traceback* object.
2000+
"""Enter post-mortem debugging of the given *traceback*, or *exception*
2001+
object.
18942002
18952003
If no traceback is given, it uses the one of the exception that is
18962004
currently being handled (an exception must be being handled if the
18972005
default is to be used).
2006+
2007+
If `t` is an exception object, the `exceptions` command makes it possible to
2008+
list and inspect its chained exceptions (if any).
18982009
"""
18992010
# handling the default
19002011
if t is None:
@@ -1911,12 +2022,8 @@ def post_mortem(t=None):
19112022
p.interaction(None, t)
19122023

19132024
def pm():
1914-
"""Enter post-mortem debugging of the traceback found in sys.last_traceback."""
1915-
if hasattr(sys, 'last_exc'):
1916-
tb = sys.last_exc.__traceback__
1917-
else:
1918-
tb = sys.last_traceback
1919-
post_mortem(tb)
2025+
"""Enter post-mortem debugging of the traceback found in sys.last_exc."""
2026+
post_mortem(sys.last_exc)
19202027

19212028

19222029
# Main program for testing
@@ -1996,8 +2103,7 @@ def main():
19962103
traceback.print_exc()
19972104
print("Uncaught exception. Entering post mortem debugging")
19982105
print("Running 'cont' or 'step' will restart the program")
1999-
t = e.__traceback__
2000-
pdb.interaction(None, t)
2106+
pdb.interaction(None, e)
20012107
print("Post mortem debugger finished. The " + target +
20022108
" will be restarted")
20032109

0 commit comments

Comments
 (0)