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

Skip to content

Commit ac868ba

Browse files
committed
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, `exceptions` allow to list 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 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>``. This also change the default behavior of ``pdb.pm()``, to receive `sys.last_exc` so that chained exception navigation is enabled. Closes gh-106670
1 parent 37d8b90 commit ac868ba

File tree

4 files changed

+171
-10
lines changed

4 files changed

+171
-10
lines changed

Doc/library/pdb.rst

Lines changed: 50 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,54 @@ 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 will now allow 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+
642690
.. rubric:: Footnotes
643691

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

Lib/pdb.py

Lines changed: 40 additions & 8 deletions
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_exception_index = 0
209211

210212
_previous_sigint_handler = None
211213

@@ -414,16 +416,27 @@ def preloop(self):
414416
self.message('display %s: %r [old: %r]' %
415417
(expr, newvalue, oldvalue))
416418

417-
def interaction(self, frame, traceback):
419+
def interaction(self, frame, tb_or_exc):
418420
# Restore the previous signal handler at the Pdb prompt.
421+
422+
if isinstance(tb_or_exc, BaseException):
423+
tb_or_exc, exception = tb_or_exc.__traceback__, tb_or_exc
424+
self._chained_exceptions = [exception]
425+
current = exception
426+
while current := current.__context__:
427+
self._chained_exception_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)
422435
except ValueError: # ValueError: signal only works in main thread
423436
pass
424437
else:
425438
Pdb._previous_sigint_handler = None
426-
if self.setup(frame, traceback):
439+
if self.setup(frame, tb_or_exc):
427440
# no interaction desired at this time (happens if .pdbrc contains
428441
# a command like "continue")
429442
self.forget()
@@ -1073,6 +1086,24 @@ 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:
1091+
for ix, exc in enumerate(self._chained_exceptions):
1092+
prompt = ">" if ix == self._chained_exception_index else " "
1093+
self.message(f"{prompt} {ix} {exc!r}")
1094+
else:
1095+
try:
1096+
arg = int(arg)
1097+
except ValueError:
1098+
self.error("Argument must be an integer")
1099+
return
1100+
if 0 <= arg < len(self._chained_exceptions):
1101+
self._chained_exception_index = arg
1102+
self.setup(None, self._chained_exceptions[arg].__traceback__)
1103+
self.print_stack_entry(self.stack[self.curindex])
1104+
else:
1105+
self.error("No exception with that number")
1106+
10761107
def do_up(self, arg):
10771108
"""u(p) [count]
10781109
@@ -1890,11 +1921,16 @@ def set_trace(*, header=None):
18901921
# Post-Mortem interface
18911922

18921923
def post_mortem(t=None):
1893-
"""Enter post-mortem debugging of the given *traceback* object.
1924+
"""Enter post-mortem debugging of the given *traceback*, or *exception*
1925+
object.
18941926
18951927
If no traceback is given, it uses the one of the exception that is
18961928
currently being handled (an exception must be being handled if the
18971929
default is to be used).
1930+
1931+
If `t` is an Exception and is a chained exception (i.e it has a __context__),
1932+
pdb will be able to move to the sub-exception when reaching the bottom
1933+
frame.
18981934
"""
18991935
# handling the default
19001936
if t is None:
@@ -1912,11 +1948,7 @@ def post_mortem(t=None):
19121948

19131949
def pm():
19141950
"""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)
1951+
post_mortem(sys.last_exc)
19201952

19211953

19221954
# Main program for testing

Lib/test/test_pdb.py

Lines changed: 78 additions & 0 deletions
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+
... 'up',
859+
... 'down',
860+
... 'exceptions 1',
861+
... 'up',
862+
... 'down',
863+
... 'exceptions -1',
864+
... 'exceptions 3',
865+
... 'up',
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[0]>(3)test_function_2()
880+
-> 1/0
881+
(Pdb) up
882+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(3)test_function_reraise()
883+
-> test_function_2()
884+
(Pdb) down
885+
> <doctest test.test_pdb.test_post_mortem_chained[0]>(3)test_function_2()
886+
-> 1/0
887+
(Pdb) exceptions 1
888+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
889+
-> raise ZeroDivisionError('reraised') from e
890+
(Pdb) up
891+
> <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
892+
-> test_function_reraise()
893+
(Pdb) down
894+
> <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
895+
-> raise ZeroDivisionError('reraised') from e
896+
(Pdb) exceptions -1
897+
*** No exception with that number
898+
(Pdb) exceptions 3
899+
*** No exception with that number
900+
(Pdb) up
901+
> <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
902+
-> test_function_reraise()
903+
(Pdb) exit
904+
"""
905+
906+
829907
def test_post_mortem():
830908
"""Test post mortem traceback debugging.
831909
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add the new ``exceptions`` command the Pdb debugger to move between chained exceptions when using post mortem debugging.
2+
3+

0 commit comments

Comments
 (0)