gh-106670: Fix Pdb handling of chained Exceptions with no stacks. (#108865)

This commit is contained in:
Matthias Bussonnier 2023-09-06 11:41:56 +02:00 committed by GitHub
parent 44892b1c52
commit 5f3433f210
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 108 additions and 30 deletions

View File

@ -494,6 +494,8 @@ class Pdb(bdb.Bdb, cmd.Cmd):
Pdb._previous_sigint_handler = None Pdb._previous_sigint_handler = None
_chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc) _chained_exceptions, tb = self._get_tb_and_exceptions(tb_or_exc)
if isinstance(tb_or_exc, BaseException):
assert tb is not None, "main exception must have a traceback"
with self._hold_exceptions(_chained_exceptions): with self._hold_exceptions(_chained_exceptions):
if self.setup(frame, tb): if self.setup(frame, tb):
# no interaction desired at this time (happens if .pdbrc contains # no interaction desired at this time (happens if .pdbrc contains
@ -1169,7 +1171,12 @@ class Pdb(bdb.Bdb, cmd.Cmd):
rep = repr(exc) rep = repr(exc)
if len(rep) > 80: if len(rep) > 80:
rep = rep[:77] + "..." rep = rep[:77] + "..."
self.message(f"{prompt} {ix:>3} {rep}") indicator = (
" -"
if self._chained_exceptions[ix].__traceback__ is None
else f"{ix:>3}"
)
self.message(f"{prompt} {indicator} {rep}")
else: else:
try: try:
number = int(arg) number = int(arg)
@ -1177,6 +1184,10 @@ class Pdb(bdb.Bdb, cmd.Cmd):
self.error("Argument must be an integer") self.error("Argument must be an integer")
return return
if 0 <= number < len(self._chained_exceptions): if 0 <= number < len(self._chained_exceptions):
if self._chained_exceptions[number].__traceback__ is None:
self.error("This exception does not have a traceback, cannot jump to it")
return
self._chained_exception_index = number self._chained_exception_index = number
self.setup(None, self._chained_exceptions[number].__traceback__) self.setup(None, self._chained_exceptions[number].__traceback__)
self.print_stack_entry(self.stack[self.curindex]) self.print_stack_entry(self.stack[self.curindex])
@ -2010,19 +2021,27 @@ def post_mortem(t=None):
If `t` is an exception object, the `exceptions` command makes it possible to If `t` is an exception object, the `exceptions` command makes it possible to
list and inspect its chained exceptions (if any). list and inspect its chained exceptions (if any).
""" """
return _post_mortem(t, Pdb())
def _post_mortem(t, pdb_instance):
"""
Private version of post_mortem, which allow to pass a pdb instance
for testing purposes.
"""
# handling the default # handling the default
if t is None: if t is None:
exc = sys.exception() exc = sys.exception()
if exc is not None: if exc is not None:
t = exc.__traceback__ t = exc.__traceback__
if t is None: if t is None or (isinstance(t, BaseException) and t.__traceback__ is None):
raise ValueError("A valid traceback must be passed if no " raise ValueError("A valid traceback must be passed if no "
"exception is being handled") "exception is being handled")
p = Pdb() pdb_instance.reset()
p.reset() pdb_instance.interaction(None, t)
p.interaction(None, t)
def pm(): def pm():
"""Enter post-mortem debugging of the traceback found in sys.last_exc.""" """Enter post-mortem debugging of the traceback found in sys.last_exc."""

View File

@ -848,9 +848,7 @@ def test_post_mortem_chained():
... try: ... try:
... test_function_reraise() ... test_function_reraise()
... except Exception as e: ... except Exception as e:
... # same as pdb.post_mortem(e), but with custom pdb instance. ... pdb._post_mortem(e, instance)
... instance.reset()
... instance.interaction(None, e)
>>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... 'exceptions', ... 'exceptions',
@ -907,11 +905,18 @@ def test_post_mortem_chained():
def test_post_mortem_cause_no_context(): def test_post_mortem_cause_no_context():
"""Test post mortem traceback debugging of chained exception """Test post mortem traceback debugging of chained exception
>>> def make_exc_with_stack(type_, *content, from_=None):
... try:
... raise type_(*content) from from_
... except Exception as out:
... return out
...
>>> def main(): >>> def main():
... try: ... try:
... raise ValueError('Context Not Shown') ... raise ValueError('Context Not Shown')
... except Exception as e1: ... except Exception as e1:
... raise ValueError("With Cause") from TypeError('The Cause') ... raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause')
>>> def test_function(): >>> def test_function():
... import pdb; ... import pdb;
@ -919,12 +924,11 @@ def test_post_mortem_cause_no_context():
... try: ... try:
... main() ... main()
... except Exception as e: ... except Exception as e:
... # same as pdb.post_mortem(e), but with custom pdb instance. ... pdb._post_mortem(e, instance)
... instance.reset()
... instance.interaction(None, e)
>>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... 'exceptions', ... 'exceptions',
... 'exceptions 0',
... 'exceptions 1', ... 'exceptions 1',
... 'up', ... 'up',
... 'down', ... 'down',
@ -934,20 +938,23 @@ def test_post_mortem_cause_no_context():
... test_function() ... test_function()
... except ValueError: ... except ValueError:
... print('Ok.') ... print('Ok.')
> <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(5)main() > <doctest test.test_pdb.test_post_mortem_cause_no_context[1]>(5)main()
-> raise ValueError("With Cause") from TypeError('The Cause') -> raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause')
(Pdb) exceptions (Pdb) exceptions
0 TypeError('The Cause') 0 TypeError('The Cause')
> 1 ValueError('With Cause') > 1 ValueError('With Cause')
(Pdb) exceptions 0
> <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(3)make_exc_with_stack()
-> raise type_(*content) from from_
(Pdb) exceptions 1 (Pdb) exceptions 1
> <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(5)main() > <doctest test.test_pdb.test_post_mortem_cause_no_context[1]>(5)main()
-> raise ValueError("With Cause") from TypeError('The Cause') -> raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause')
(Pdb) up (Pdb) up
> <doctest test.test_pdb.test_post_mortem_cause_no_context[1]>(5)test_function() > <doctest test.test_pdb.test_post_mortem_cause_no_context[2]>(5)test_function()
-> main() -> main()
(Pdb) down (Pdb) down
> <doctest test.test_pdb.test_post_mortem_cause_no_context[0]>(5)main() > <doctest test.test_pdb.test_post_mortem_cause_no_context[1]>(5)main()
-> raise ValueError("With Cause") from TypeError('The Cause') -> raise ValueError("With Cause") from make_exc_with_stack(TypeError,'The Cause')
(Pdb) exit""" (Pdb) exit"""
@ -971,9 +978,7 @@ def test_post_mortem_context_of_the_cause():
... try: ... try:
... main() ... main()
... except Exception as e: ... except Exception as e:
... # same as pdb.post_mortem(e), but with custom pdb instance. ... pdb._post_mortem(e, instance)
... instance.reset()
... instance.interaction(None, e)
>>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... 'exceptions', ... 'exceptions',
@ -1046,9 +1051,7 @@ def test_post_mortem_from_none():
... try: ... try:
... main() ... main()
... except Exception as e: ... except Exception as e:
... # same as pdb.post_mortem(e), but with custom pdb instance. ... pdb._post_mortem(e, instance)
... instance.reset()
... instance.interaction(None, e)
>>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> with PdbTestInput([ # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... 'exceptions', ... 'exceptions',
@ -1066,6 +1069,64 @@ def test_post_mortem_from_none():
""" """
def test_post_mortem_from_no_stack():
"""Test post mortem traceback debugging of chained exception
especially when one exception has no stack.
>>> def main():
... raise Exception() from Exception()
>>> def test_function():
... import pdb;
... instance = pdb.Pdb(nosigint=True, readrc=False)
... try:
... main()
... except Exception as e:
... pdb._post_mortem(e, instance)
>>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... ["exceptions",
... "exceptions 0",
... "exit"],
... ):
... try:
... test_function()
... except ValueError:
... print('Correctly reraised.')
> <doctest test.test_pdb.test_post_mortem_from_no_stack[0]>(2)main()
-> raise Exception() from Exception()
(Pdb) exceptions
- Exception()
> 1 Exception()
(Pdb) exceptions 0
*** This exception does not have a traceback, cannot jump to it
(Pdb) exit
"""
def test_post_mortem_single_no_stack():
"""Test post mortem called when origin exception has no stack
>>> def test_function():
... import pdb;
... instance = pdb.Pdb(nosigint=True, readrc=False)
... import sys
... sys.last_exc = Exception()
... pdb._post_mortem(sys.last_exc, instance)
>>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... []
... ):
... try:
... test_function()
... except ValueError as e:
... print(e)
A valid traceback must be passed if no exception is being handled
"""
def test_post_mortem_complex(): def test_post_mortem_complex():
"""Test post mortem traceback debugging of chained exception """Test post mortem traceback debugging of chained exception
@ -1130,9 +1191,7 @@ def test_post_mortem_complex():
... try: ... try:
... main() ... main()
... except Exception as e: ... except Exception as e:
... # same as pdb.post_mortem(e), but with custom pdb instance. ... pdb._post_mortem(e, instance)
... instance.reset()
... instance.interaction(None, e)
>>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> with PdbTestInput( # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
... ["exceptions", ... ["exceptions",