Skip to content

Commit 79a62de

Browse files
committed
Allow Pdb to move between chained exception.
This lets Pdb receive and exception, instead of a traceback, and when this is the case and the exception are chained, `exceptions` allow to list and move between the chained exceptions when reaching the tot/bottom. 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") # C def inner(x): 1 / x # E Only A is 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>``. I also change the default behavior of ``pdb.pm()``, to receive `sys.last_value` so that chained exception navigation is enabled. Closes gh-106670
1 parent 3e65bae commit 79a62de

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed

Lib/pdb.py

+40-1
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ def namespace(self):
206206
line_prefix = '\n-> ' # Probably a better default
207207

208208
class Pdb(bdb.Bdb, cmd.Cmd):
209+
_chained_exceptions = []
210+
_chained_index = 0
209211

210212
_previous_sigint_handler = None
211213

@@ -416,6 +418,17 @@ def preloop(self):
416418

417419
def interaction(self, frame, traceback):
418420
# Restore the previous signal handler at the Pdb prompt.
421+
422+
if isinstance(traceback, BaseException):
423+
traceback, exception = traceback.__traceback__, traceback
424+
self._chained_exceptions = [exception]
425+
current = exception
426+
while current := current.__context__:
427+
self._chained_index += 1
428+
self._chained_exceptions.insert(0, current)
429+
else:
430+
self._chained_exceptions = []
431+
419432
if Pdb._previous_sigint_handler:
420433
try:
421434
signal.signal(signal.SIGINT, Pdb._previous_sigint_handler)
@@ -1073,6 +1086,26 @@ def _select_frame(self, number):
10731086
self.print_stack_entry(self.stack[self.curindex])
10741087
self.lineno = None
10751088

1089+
def do_exceptions(self, arg):
1090+
if not arg or arg == "list":
1091+
for ix, exc in enumerate(self._chained_exceptions):
1092+
print(">" if ix == self._chained_index else " ", ix, repr(exc))
1093+
else:
1094+
try:
1095+
arg = int(arg)
1096+
except ValueError:
1097+
self.error("Argument must be an integer")
1098+
return
1099+
pass
1100+
else:
1101+
if 0 <= arg < len(self._chained_exceptions):
1102+
self._chained_index = arg
1103+
self.setup(None, self._chained_exceptions[arg].__traceback__)
1104+
self.curindex = 0
1105+
self._select_frame(0)
1106+
return
1107+
self.error("No exception with that number")
1108+
10761109
def do_up(self, arg):
10771110
"""u(p) [count]
10781111
@@ -1895,6 +1928,10 @@ def post_mortem(t=None):
18951928
If no traceback is given, it uses the one of the exception that is
18961929
currently being handled (an exception must be being handled if the
18971930
default is to be used).
1931+
1932+
If t is an Exception and is a chained exception (i.e it has a __context__),
1933+
pdb will be able to move to the sub-exception when reaching the bottom
1934+
frame.
18981935
"""
18991936
# handling the default
19001937
if t is None:
@@ -1912,7 +1949,9 @@ def post_mortem(t=None):
19121949

19131950
def pm():
19141951
"""Enter post-mortem debugging of the traceback found in sys.last_traceback."""
1915-
if hasattr(sys, 'last_exc'):
1952+
if hasattr(sys, "last_value"):
1953+
tb = sys.last_value
1954+
elif hasattr(sys, "last_exc"):
19161955
tb = sys.last_exc.__traceback__
19171956
else:
19181957
tb = sys.last_traceback

Lib/test/test_pdb.py

+78
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,84 @@ def test_convenience_variables():
826826
(Pdb) continue
827827
"""
828828

829+
830+
def test_post_mortem_chained():
831+
"""Test post mortem traceback debugging of chained exception
832+
833+
>>> def test_function_2():
834+
... try:
835+
... 1/0
836+
... finally:
837+
... print('Exception!')
838+
839+
>>> def test_function_reraise():
840+
... try:
841+
... test_function_2()
842+
... except ZeroDivisionError as e:
843+
... raise ZeroDivisionError('reraised') from e
844+
845+
>>> def test_function():
846+
... import pdb;
847+
... instance = pdb.Pdb(nosigint=True, readrc=False)
848+
... try:
849+
... test_function_reraise()
850+
... except Exception as e:
851+
... # same as pdb.post_mortem(e), but with custom pdb instance.
852+
... instance.reset()
853+
... instance.interaction(None, e)
854+
855+
>>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
856+
... 'exceptions',
857+
... 'exceptions 0',
858+
... 'down',
859+
... 'up',
860+
... 'exceptions 1',
861+
... 'down',
862+
... 'up',
863+
... 'exceptions -1',
864+
... 'exceptions 3',
865+
... 'down',
866+
... 'exit',
867+
... ]):
868+
... try:
869+
... test_function()
870+
... except ZeroDivisionError:
871+
... print('Correctly reraised.')
872+
Exception!
873+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
874+
-> raise ZeroDivisionError('reraised') from e
875+
(Pdb) exceptions
876+
0 ZeroDivisionError('division by zero')
877+
> 1 ZeroDivisionError('reraised')
878+
(Pdb) exceptions 0
879+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(3)test_function_reraise()
880+
-> test_function_2()
881+
(Pdb) down
882+
> <doctest test.test_pdb.test_post_mortem_chained[0]>(3)test_function_2()
883+
-> 1/0
884+
(Pdb) up
885+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(3)test_function_reraise()
886+
-> test_function_2()
887+
(Pdb) exceptions 1
888+
> <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
889+
-> test_function_reraise()
890+
(Pdb) down
891+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
892+
-> raise ZeroDivisionError('reraised') from e
893+
(Pdb) up
894+
> <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
895+
-> test_function_reraise()
896+
(Pdb) exceptions -1
897+
*** No exception with that number
898+
(Pdb) exceptions 3
899+
*** No exception with that number
900+
(Pdb) down
901+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
902+
-> raise ZeroDivisionError('reraised') from e
903+
(Pdb) exit
904+
"""
905+
906+
829907
def test_post_mortem():
830908
"""Test post mortem traceback debugging.
831909
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Allow Pdb to move between chained exceptions on post_mortem debugging.
2+
3+
If ``Pdb.post_mortem()`` is called with a chained exception, it will now allow
4+
the user to move between the chained exceptions using ``exceptions`` command to
5+
list exceptions, and ``exception <number>`` to switch to that exception.
6+
7+
We do not differentiate ``__cause__`` and ``__context__``.
8+
9+
That is to say if you have the following code
10+
11+
def out():
12+
try:
13+
middle()
14+
except Exception as e:
15+
raise ValueError("reraise middle() error") from e
16+
17+
def middle():
18+
try:
19+
return inner(0)
20+
except Exception as e:
21+
raise ValueError("Middle fail")
22+
23+
def inner(x):
24+
1 / x
25+
26+
Post mortem debugging allows us to move between the different exception:
27+
28+
>>> import pdb; pdb.pm()
29+
30+
> code.py(5)out()
31+
-> raise ValueError("reraise middle() error") from e
32+
33+
(Pdb) exceptions
34+
0 ZeroDivisionError('division by zero')
35+
1 ValueError('Middle fail')
36+
> 2 ValueError('reraise middle() error')
37+
38+
(Pdb) exceptions 0
39+
> code.py(10)middle()
40+
-> return inner(0)
41+
42+
(Pdb) down
43+
> code.py(16)inner()
44+
-> 1 / x
45+
46+
By default, ``pdb.pm()`` will now try to show the exception based on ``sys.last_value`` to allow moving between
47+
exceptions, instead of ``sys.last_traceback``.

0 commit comments

Comments
 (0)