Skip to content

Commit def928d

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, up/down allow to 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 up/down. I do not change the default behavior of ``pdb.pm()``, but I think that arguably the default should be to pass `sys.last_value` so that chained exception navigation is enabled. Closes gh-106670
1 parent be1b968 commit def928d

File tree

3 files changed

+113
-2
lines changed

3 files changed

+113
-2
lines changed

Lib/pdb.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def namespace(self):
206206
line_prefix = '\n-> ' # Probably a better default
207207

208208
class Pdb(bdb.Bdb, cmd.Cmd):
209+
_chained_exceptions = []
209210

210211
_previous_sigint_handler = None
211212

@@ -416,6 +417,15 @@ def preloop(self):
416417

417418
def interaction(self, frame, traceback):
418419
# Restore the previous signal handler at the Pdb prompt.
420+
421+
if isinstance(traceback, BaseException):
422+
traceback, exception = traceback.__traceback__, traceback
423+
self._chained_exceptions = [exception]
424+
else:
425+
self._chained_exceptions = []
426+
427+
# list of exceptions in chain exception, we always start with the current one.
428+
419429
if Pdb._previous_sigint_handler:
420430
try:
421431
signal.signal(signal.SIGINT, Pdb._previous_sigint_handler)
@@ -1080,6 +1090,15 @@ def do_up(self, arg):
10801090
stack trace (to an older frame).
10811091
"""
10821092
if self.curindex == 0:
1093+
if len(self._chained_exceptions) > 1:
1094+
self.message("Oldest frame, moving to last frame upper exception")
1095+
self._chained_exceptions.pop()
1096+
self.setup(None, self._chained_exceptions[-1].__traceback__)
1097+
self.curindex = len(self.stack)
1098+
arg = "0"
1099+
else:
1100+
self.error("Oldest frame")
1101+
return
10831102
self.error('Oldest frame')
10841103
return
10851104
try:
@@ -1101,8 +1120,17 @@ def do_down(self, arg):
11011120
stack trace (to a newer frame).
11021121
"""
11031122
if self.curindex + 1 == len(self.stack):
1104-
self.error('Newest frame')
1105-
return
1123+
if self._chained_exceptions and self._chained_exceptions[-1].__context__:
1124+
new_ex = self._chained_exceptions[-1].__context__
1125+
1126+
self.message("Newest frame, moving into __context__ traceback.")
1127+
self._chained_exceptions.append(new_ex)
1128+
self.setup(None, new_ex.__traceback__)
1129+
self.curindex = -1
1130+
arg = "1"
1131+
else:
1132+
self.error("Newest frame")
1133+
return
11061134
try:
11071135
count = int(arg or 1)
11081136
except ValueError:
@@ -1895,6 +1923,10 @@ def post_mortem(t=None):
18951923
If no traceback is given, it uses the one of the exception that is
18961924
currently being handled (an exception must be being handled if the
18971925
default is to be used).
1926+
1927+
If t is an Exception and is a chained exception (i.e it has a __context__),
1928+
pdb will be able to move to the sub-exception when reaching the bottom
1929+
frame.
18981930
"""
18991931
# handling the default
19001932
if t is None:

Lib/test/test_pdb.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,77 @@ 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+
... 'up',
857+
... 'up',
858+
... 'down',
859+
... 'down',
860+
... 'down',
861+
... 'down',
862+
... 'up',
863+
... 'up',
864+
... 'exit',
865+
... ]):
866+
... try:
867+
... test_function()
868+
... except ZeroDivisionError:
869+
... print('Correctly reraised.')
870+
Exception!
871+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
872+
-> raise ZeroDivisionError('reraised') from e
873+
(Pdb) up
874+
> <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
875+
-> test_function_reraise()
876+
(Pdb) up
877+
*** Oldest frame
878+
(Pdb) down
879+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
880+
-> raise ZeroDivisionError('reraised') from e
881+
(Pdb) down
882+
Newest frame, moving into __context__ traceback.
883+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(3)test_function_reraise()
884+
-> test_function_2()
885+
(Pdb) down
886+
> <doctest test.test_pdb.test_post_mortem_chained[0]>(3)test_function_2()
887+
-> 1/0
888+
(Pdb) down
889+
*** Newest frame
890+
(Pdb) up
891+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(3)test_function_reraise()
892+
-> test_function_2()
893+
(Pdb) up
894+
Oldest frame, moving to last frame upper exception
895+
*** Oldest frame
896+
(Pdb) exit
897+
"""
898+
899+
829900
def test_post_mortem():
830901
"""Test post mortem traceback debugging.
831902
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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 ``up`` and ``down``
5+
commands.
6+
7+
When reaching the bottom of the stacktrace, Pdb will move the first frame of
8+
the next exception. And vice-versa, when reaching the top of the stacktrace.

0 commit comments

Comments
 (0)