diff --git a/AUTHORS b/AUTHORS index 8103a1d52a5..91a474c0dd9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -51,6 +51,7 @@ Aron Coyle Aron Curzon Arthur Richard Ashish Kurmi +Ashley Whetter Aviral Verma Aviv Palivoda Babak Keyvani diff --git a/changelog/12707.improvement.rst b/changelog/12707.improvement.rst new file mode 100644 index 00000000000..4684b6561c8 --- /dev/null +++ b/changelog/12707.improvement.rst @@ -0,0 +1 @@ +Exception chains can be navigated when dropped into Pdb in Python 3.13+. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3e1463fff26..8c88c02b8d6 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -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: @@ -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") diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 37032f92354..d86c9018b80 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -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: @@ -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", ""])