import traceback
import linecache
+from contextlib import contextmanager
from typing import Union
# 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):
_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)
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
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:
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 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
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 rather than a traceback."
+ )
+ return
+ if not arg:
+ 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:
+ self.error("No exception with that number")
+
def do_up(self, arg):
"""u(p) [count]
# 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*
+ 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).
+
+ If `t` is an exception object, the `exceptions` command makes it possible to
+ list and inspect its chained exceptions (if any).
"""
# handling the default
if t is None:
p.interaction(None, t)
def pm():
- """Enter post-mortem debugging of the traceback found in sys.last_traceback."""
- if hasattr(sys, 'last_exc'):
- tb = sys.last_exc.__traceback__
- else:
- tb = sys.last_traceback
- post_mortem(tb)
+ """Enter post-mortem debugging of the traceback found in sys.last_exc."""
+ post_mortem(sys.last_exc)
# Main program for testing
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")
(Pdb) continue
"""
+
+def test_post_mortem_chained():
+ """Test post mortem traceback debugging of chained exception
+
+ >>> def test_function_2():
+ ... try:
+ ... 1/0
+ ... finally:
+ ... print('Exception!')
+
+ >>> def test_function_reraise():
+ ... try:
+ ... test_function_2()
+ ... except ZeroDivisionError as e:
+ ... raise ZeroDivisionError('reraised') from e
+
+ >>> def test_function():
+ ... import pdb;
+ ... instance = pdb.Pdb(nosigint=True, readrc=False)
+ ... try:
+ ... test_function_reraise()
+ ... except Exception as e:
+ ... # same as pdb.post_mortem(e), but with custom pdb instance.
+ ... instance.reset()
+ ... instance.interaction(None, e)
+
+ >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ ... 'exceptions',
+ ... 'exceptions 0',
+ ... 'up',
+ ... 'down',
+ ... 'exceptions 1',
+ ... 'up',
+ ... 'down',
+ ... 'exceptions -1',
+ ... 'exceptions 3',
+ ... 'up',
+ ... 'exit',
+ ... ]):
+ ... try:
+ ... test_function()
+ ... except ZeroDivisionError:
+ ... print('Correctly reraised.')
+ Exception!
+ > <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
+ -> raise ZeroDivisionError('reraised') from e
+ (Pdb) exceptions
+ 0 ZeroDivisionError('division by zero')
+ > 1 ZeroDivisionError('reraised')
+ (Pdb) exceptions 0
+ > <doctest test.test_pdb.test_post_mortem_chained[0]>(3)test_function_2()
+ -> 1/0
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_chained[1]>(3)test_function_reraise()
+ -> test_function_2()
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem_chained[0]>(3)test_function_2()
+ -> 1/0
+ (Pdb) exceptions 1
+ > <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
+ -> raise ZeroDivisionError('reraised') from e
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
+ -> test_function_reraise()
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem_chained[1]>(5)test_function_reraise()
+ -> raise ZeroDivisionError('reraised') from e
+ (Pdb) exceptions -1
+ *** No exception with that number
+ (Pdb) exceptions 3
+ *** No exception with that number
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_chained[2]>(5)test_function()
+ -> test_function_reraise()
+ (Pdb) exit
+ """
+
+
+def test_post_mortem_cause_no_context():
+ """Test post mortem traceback debugging of chained exception
+
+ >>> def main():
+ ... try:
+ ... raise ValueError('Context Not Shown')
+ ... except Exception as e1:
+ ... raise ValueError("With Cause") from TypeError('The Cause')
+
+ >>> def test_function():
+ ... import pdb;
+ ... instance = pdb.Pdb(nosigint=True, readrc=False)
+ ... try:
+ ... main()
+ ... except Exception as e:
+ ... # same as pdb.post_mortem(e), but with custom pdb instance.
+ ... instance.reset()
+ ... instance.interaction(None, e)
+
+ >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ ... 'exceptions',
+ ... 'exceptions 1',
+ ... 'up',
+ ... 'down',
+ ... 'exit',
+ ... ]):
+ ... try:
+ ... test_function()
+ ... except ValueError:
+ ... print('Ok.')
+ > <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(5)main()
+ -> raise ValueError("With Cause") from TypeError('The Cause')
+ (Pdb) exceptions
+ 0 TypeError('The Cause')
+ > 1 ValueError('With Cause')
+ (Pdb) exceptions 1
+ > <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(5)main()
+ -> raise ValueError("With Cause") from TypeError('The Cause')
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_cause_no_context[1]>(5)test_function()
+ -> main()
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(5)main()
+ -> raise ValueError("With Cause") from TypeError('The Cause')
+ (Pdb) exit"""
+
+
+def test_post_mortem_context_of_the_cause():
+ """Test post mortem traceback debugging of chained exception
+
+
+ >>> def main():
+ ... try:
+ ... raise TypeError('Context of the cause')
+ ... except Exception as e1:
+ ... try:
+ ... raise ValueError('Root Cause')
+ ... except Exception as e2:
+ ... ex = e2
+ ... raise ValueError("With Cause, and cause has context") from ex
+
+ >>> def test_function():
+ ... import pdb;
+ ... instance = pdb.Pdb(nosigint=True, readrc=False)
+ ... try:
+ ... main()
+ ... except Exception as e:
+ ... # same as pdb.post_mortem(e), but with custom pdb instance.
+ ... instance.reset()
+ ... instance.interaction(None, e)
+
+ >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ ... 'exceptions',
+ ... 'exceptions 2',
+ ... 'up',
+ ... 'down',
+ ... 'exceptions 3',
+ ... 'up',
+ ... 'down',
+ ... 'exceptions 4',
+ ... 'up',
+ ... 'down',
+ ... 'exit',
+ ... ]):
+ ... try:
+ ... test_function()
+ ... except ValueError:
+ ... print('Correctly reraised.')
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[0]>(9)main()
+ -> raise ValueError("With Cause, and cause has context") from ex
+ (Pdb) exceptions
+ 0 TypeError('Context of the cause')
+ 1 ValueError('Root Cause')
+ > 2 ValueError('With Cause, and cause has context')
+ (Pdb) exceptions 2
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[0]>(9)main()
+ -> raise ValueError("With Cause, and cause has context") from ex
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[1]>(5)test_function()
+ -> main()
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[0]>(9)main()
+ -> raise ValueError("With Cause, and cause has context") from ex
+ (Pdb) exceptions 3
+ *** No exception with that number
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[1]>(5)test_function()
+ -> main()
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[0]>(9)main()
+ -> raise ValueError("With Cause, and cause has context") from ex
+ (Pdb) exceptions 4
+ *** No exception with that number
+ (Pdb) up
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[1]>(5)test_function()
+ -> main()
+ (Pdb) down
+ > <doctest test.test_pdb.test_post_mortem_context_of_the_cause[0]>(9)main()
+ -> raise ValueError("With Cause, and cause has context") from ex
+ (Pdb) exit
+ """
+
+
+def test_post_mortem_from_none():
+ """Test post mortem traceback debugging of chained exception
+
+ In particular that cause from None (which sets __supress_context__ to True)
+ does not show context.
+
+
+ >>> def main():
+ ... try:
+ ... raise TypeError('Context of the cause')
+ ... except Exception as e1:
+ ... raise ValueError("With Cause, and cause has context") from None
+
+ >>> def test_function():
+ ... import pdb;
+ ... instance = pdb.Pdb(nosigint=True, readrc=False)
+ ... try:
+ ... main()
+ ... except Exception as e:
+ ... # same as pdb.post_mortem(e), but with custom pdb instance.
+ ... instance.reset()
+ ... instance.interaction(None, e)
+
+ >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ ... 'exceptions',
+ ... 'exit',
+ ... ]):
+ ... try:
+ ... test_function()
+ ... except ValueError:
+ ... print('Correctly reraised.')
+ > <doctest test.test_pdb.test_post_mortem_from_none[0]>(5)main()
+ -> raise ValueError("With Cause, and cause has context") from None
+ (Pdb) exceptions
+ > 0 ValueError('With Cause, and cause has context')
+ (Pdb) exit
+ """
+
+
+def test_post_mortem_complex():
+ """Test post mortem traceback debugging of chained exception
+
+ Test with simple and complex cycles, exception groups,...
+
+ >>> def make_ex_with_stack(type_, *content, from_=None):
+ ... try:
+ ... raise type_(*content) from from_
+ ... except Exception as out:
+ ... return out
+ ...
+
+ >>> def cycle():
+ ... try:
+ ... raise ValueError("Cycle Leaf")
+ ... except Exception as e:
+ ... raise e from e
+ ...
+
+ >>> def tri_cycle():
+ ... a = make_ex_with_stack(ValueError, "Cycle1")
+ ... b = make_ex_with_stack(ValueError, "Cycle2")
+ ... c = make_ex_with_stack(ValueError, "Cycle3")
+ ...
+ ... a.__cause__ = b
+ ... b.__cause__ = c
+ ...
+ ... raise c from a
+ ...
+
+ >>> def cause():
+ ... try:
+ ... raise ValueError("Cause Leaf")
+ ... except Exception as e:
+ ... raise e
+ ...
+
+ >>> def context(n=10):
+ ... try:
+ ... raise ValueError(f"Context Leaf {n}")
+ ... except Exception as e:
+ ... if n == 0:
+ ... raise ValueError(f"With Context {n}") from e
+ ... else:
+ ... context(n - 1)
+ ...
+
+ >>> def main():
+ ... try:
+ ... cycle()
+ ... except Exception as e1:
+ ... try:
+ ... tri_cycle()
+ ... except Exception as e2:
+ ... ex = e2
+ ... raise ValueError("With Context and With Cause") from ex
+
+
+ >>> def test_function():
+ ... import pdb;
+ ... instance = pdb.Pdb(nosigint=True, readrc=False)
+ ... try:
+ ... main()
+ ... except Exception as e:
+ ... # same as pdb.post_mortem(e), but with custom pdb instance.
+ ... instance.reset()
+ ... instance.interaction(None, e)
+
+ >>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+ ... ["exceptions",
+ ... "exceptions 0",
+ ... "exceptions 1",
+ ... "exceptions 2",
+ ... "exceptions 3",
+ ... "exit"],
+ ... ):
+ ... try:
+ ... test_function()
+ ... except ValueError:
+ ... print('Correctly reraised.')
+ > <doctest test.test_pdb.test_post_mortem_complex[5]>(9)main()
+ -> raise ValueError("With Context and With Cause") from ex
+ (Pdb) exceptions
+ 0 ValueError('Cycle2')
+ 1 ValueError('Cycle1')
+ 2 ValueError('Cycle3')
+ > 3 ValueError('With Context and With Cause')
+ (Pdb) exceptions 0
+ > <doctest test.test_pdb.test_post_mortem_complex[0]>(3)make_ex_with_stack()
+ -> raise type_(*content) from from_
+ (Pdb) exceptions 1
+ > <doctest test.test_pdb.test_post_mortem_complex[0]>(3)make_ex_with_stack()
+ -> raise type_(*content) from from_
+ (Pdb) exceptions 2
+ > <doctest test.test_pdb.test_post_mortem_complex[0]>(3)make_ex_with_stack()
+ -> raise type_(*content) from from_
+ (Pdb) exceptions 3
+ > <doctest test.test_pdb.test_post_mortem_complex[5]>(9)main()
+ -> raise ValueError("With Context and With Cause") from ex
+ (Pdb) exit
+ """
+
+
def test_post_mortem():
"""Test post mortem traceback debugging.