Skip to content

Commit 3cc88d9

Browse files
authored
Exception chains can be navigated when dropped into Pdb in Python 3.13+ (#12708)
Closes #12707
1 parent 57cccf7 commit 3cc88d9

File tree

4 files changed

+63
-11
lines changed

4 files changed

+63
-11
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Aron Coyle
5151
Aron Curzon
5252
Arthur Richard
5353
Ashish Kurmi
54+
Ashley Whetter
5455
Aviral Verma
5556
Aviv Palivoda
5657
Babak Keyvani

changelog/12707.improvement.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Exception chains can be navigated when dropped into Pdb in Python 3.13+.

src/_pytest/debugging.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,8 @@ def pytest_exception_interact(
292292
_enter_pdb(node, call.excinfo, report)
293293

294294
def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
295-
tb = _postmortem_traceback(excinfo)
296-
post_mortem(tb)
295+
exc_or_tb = _postmortem_exc_or_tb(excinfo)
296+
post_mortem(exc_or_tb)
297297

298298

299299
class PdbTrace:
@@ -354,32 +354,46 @@ def _enter_pdb(
354354
tw.sep(">", "traceback")
355355
rep.toterminal(tw)
356356
tw.sep(">", "entering PDB")
357-
tb = _postmortem_traceback(excinfo)
357+
tb_or_exc = _postmortem_exc_or_tb(excinfo)
358358
rep._pdbshown = True # type: ignore[attr-defined]
359-
post_mortem(tb)
359+
post_mortem(tb_or_exc)
360360
return rep
361361

362362

363-
def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
363+
def _postmortem_exc_or_tb(
364+
excinfo: ExceptionInfo[BaseException],
365+
) -> types.TracebackType | BaseException:
364366
from doctest import UnexpectedException
365367

368+
get_exc = sys.version_info >= (3, 13)
366369
if isinstance(excinfo.value, UnexpectedException):
367370
# A doctest.UnexpectedException is not useful for post_mortem.
368371
# Use the underlying exception instead:
369-
return excinfo.value.exc_info[2]
372+
underlying_exc = excinfo.value
373+
if get_exc:
374+
return underlying_exc.exc_info[1]
375+
376+
return underlying_exc.exc_info[2]
370377
elif isinstance(excinfo.value, ConftestImportFailure):
371378
# A config.ConftestImportFailure is not useful for post_mortem.
372379
# Use the underlying exception instead:
373-
assert excinfo.value.cause.__traceback__ is not None
374-
return excinfo.value.cause.__traceback__
380+
cause = excinfo.value.cause
381+
if get_exc:
382+
return cause
383+
384+
assert cause.__traceback__ is not None
385+
return cause.__traceback__
375386
else:
376387
assert excinfo._excinfo is not None
388+
if get_exc:
389+
return excinfo._excinfo[1]
390+
377391
return excinfo._excinfo[2]
378392

379393

380-
def post_mortem(t: types.TracebackType) -> None:
394+
def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None:
381395
p = pytestPDB._init_pdb("post_mortem")
382396
p.reset()
383-
p.interaction(None, t)
397+
p.interaction(None, tb_or_exc)
384398
if p.quitting:
385399
outcomes.exit("Quitting debugger")

testing/test_debugging.py

+37-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ def test_func():
103103
)
104104
assert rep.failed
105105
assert len(pdblist) == 1
106-
tb = _pytest._code.Traceback(pdblist[0][0])
106+
if sys.version_info < (3, 13):
107+
tb = _pytest._code.Traceback(pdblist[0][0])
108+
else:
109+
tb = _pytest._code.Traceback(pdblist[0][0].__traceback__)
107110
assert tb[-1].name == "test_func"
108111

109112
def test_pdb_on_xfail(self, pytester: Pytester, pdblist) -> None:
@@ -921,6 +924,39 @@ def test_foo():
921924
child.expect("custom set_trace>")
922925
self.flush(child)
923926

927+
@pytest.mark.skipif(
928+
sys.version_info < (3, 13),
929+
reason="Navigating exception chains was introduced in 3.13",
930+
)
931+
def test_pdb_exception_chain_navigation(self, pytester: Pytester) -> None:
932+
p1 = pytester.makepyfile(
933+
"""
934+
def inner_raise():
935+
is_inner = True
936+
raise RuntimeError("Woops")
937+
938+
def outer_raise():
939+
is_inner = False
940+
try:
941+
inner_raise()
942+
except RuntimeError:
943+
raise RuntimeError("Woopsie")
944+
945+
def test_1():
946+
outer_raise()
947+
assert True
948+
"""
949+
)
950+
child = pytester.spawn_pytest(f"--pdb {p1}")
951+
child.expect("Pdb")
952+
child.sendline("is_inner")
953+
child.expect_exact("False")
954+
child.sendline("exceptions 0")
955+
child.sendline("is_inner")
956+
child.expect_exact("True")
957+
child.sendeof()
958+
self.flush(child)
959+
924960

925961
class TestDebuggingBreakpoints:
926962
@pytest.mark.parametrize("arg", ["--pdb", ""])

0 commit comments

Comments
 (0)