Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Aron Coyle
Aron Curzon
Arthur Richard
Ashish Kurmi
Ashley Whetter
Aviral Verma
Aviv Palivoda
Babak Keyvani
Expand Down
1 change: 1 addition & 0 deletions changelog/12707.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Exception chains can be navigated when dropped into Pdb in Python 3.13+.
34 changes: 24 additions & 10 deletions src/_pytest/debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ def pytest_exception_interact(
_enter_pdb(node, call.excinfo, report)

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


class PdbTrace:
Expand Down Expand Up @@ -354,32 +354,46 @@ def _enter_pdb(
tw.sep(">", "traceback")
rep.toterminal(tw)
tw.sep(">", "entering PDB")
tb = _postmortem_traceback(excinfo)
tb_or_exc = _postmortem_exc_or_tb(excinfo)
rep._pdbshown = True # type: ignore[attr-defined]
post_mortem(tb)
post_mortem(tb_or_exc)
return rep


def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.TracebackType:
def _postmortem_exc_or_tb(
excinfo: ExceptionInfo[BaseException],
) -> types.TracebackType | BaseException:
from doctest import UnexpectedException

get_exc = sys.version_info >= (3, 13)
if isinstance(excinfo.value, UnexpectedException):
# A doctest.UnexpectedException is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.exc_info[2]
underlying_exc = excinfo.value
if get_exc:
return underlying_exc.exc_info[1]

return underlying_exc.exc_info[2]
elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead:
assert excinfo.value.cause.__traceback__ is not None
return excinfo.value.cause.__traceback__
cause = excinfo.value.cause
if get_exc:
return cause

assert cause.__traceback__ is not None
return cause.__traceback__
else:
assert excinfo._excinfo is not None
if get_exc:
return excinfo._excinfo[1]

return excinfo._excinfo[2]


def post_mortem(t: types.TracebackType) -> None:
def post_mortem(tb_or_exc: types.TracebackType | BaseException) -> None:
p = pytestPDB._init_pdb("post_mortem")
p.reset()
p.interaction(None, t)
p.interaction(None, tb_or_exc)
if p.quitting:
outcomes.exit("Quitting debugger")
38 changes: 37 additions & 1 deletion testing/test_debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ def test_func():
)
assert rep.failed
assert len(pdblist) == 1
tb = _pytest._code.Traceback(pdblist[0][0])
if sys.version_info < (3, 13):
tb = _pytest._code.Traceback(pdblist[0][0])
else:
tb = _pytest._code.Traceback(pdblist[0][0].__traceback__)
assert tb[-1].name == "test_func"

def test_pdb_on_xfail(self, pytester: Pytester, pdblist) -> None:
Expand Down Expand Up @@ -921,6 +924,39 @@ def test_foo():
child.expect("custom set_trace>")
self.flush(child)

@pytest.mark.skipif(
sys.version_info < (3, 13),
reason="Navigating exception chains was introduced in 3.13",
)
def test_pdb_exception_chain_navigation(self, pytester: Pytester) -> None:
p1 = pytester.makepyfile(
"""
def inner_raise():
is_inner = True
raise RuntimeError("Woops")

def outer_raise():
is_inner = False
try:
inner_raise()
except RuntimeError:
raise RuntimeError("Woopsie")

def test_1():
outer_raise()
assert True
"""
)
child = pytester.spawn_pytest(f"--pdb {p1}")
child.expect("Pdb")
child.sendline("is_inner")
child.expect_exact("False")
child.sendline("exceptions 0")
child.sendline("is_inner")
child.expect_exact("True")
child.sendeof()
self.flush(child)


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