From 2886fc64ba5869afe4935f6313977f29f32a6efb Mon Sep 17 00:00:00 2001 From: SemTiOne Date: Sat, 25 Jul 2026 10:24:00 +0700 Subject: [PATCH] Fix duplicated exception chain output for exceptions without a traceback Co-authored-by: Claude --- AUTHORS | 1 + changelog/8321.bugfix.rst | 1 + src/_pytest/_code/code.py | 4 +++- testing/code/test_excinfo.py | 39 ++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 changelog/8321.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 229e4315078..74346c234b1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -439,6 +439,7 @@ Sankt Petersbug Saravanan Padmanaban Sean Malloy Segev Finer +SemTiOne Serhii Mozghovyi Seth Junot Shantanu Jain diff --git a/changelog/8321.bugfix.rst b/changelog/8321.bugfix.rst new file mode 100644 index 00000000000..7a91fa6aaba --- /dev/null +++ b/changelog/8321.bugfix.rst @@ -0,0 +1 @@ +Fixed exception chains being duplicated in the traceback output when an exception in the chain (``__cause__``/``__context__``) has no traceback of its own, and that exception is itself linked to further exceptions. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 3c453b15dd7..e37a1324c67 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1226,7 +1226,9 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR else: # Fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work. - reprtraceback = ReprTracebackNative(format_exception(type(e), e, None)) + reprtraceback = ReprTracebackNative( + format_exception(type(e), e, None, chain=False) + ) reprcrash = None repr_chain.append((reprtraceback, reprcrash, description)) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 883a7c5f9b0..ee1b9d6fa2f 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1592,6 +1592,45 @@ def g(): ] ) + def test_exc_chain_repr_without_traceback_multiple_links(self) -> None: + """ + Exceptions without a traceback that are themselves part of a longer + chain must not have their remaining chain printed twice: once by + Python's own ``traceback.format_exception`` (used as a fallback when + an exception has no ``__traceback__``) and once more by pytest's own + chain-walking loop (#8321). + """ + exc1 = ValueError("abcd") + exc2 = IndexError("efgh") + exc3 = KeyError("ijkl") + exc4 = RuntimeError("mnop") + exc1.__cause__ = exc2 + exc2.__context__ = exc3 + exc3.__cause__ = exc4 + + try: + raise exc1 + except ValueError: + excinfo = ExceptionInfo.from_current() + + r = excinfo.getrepr() + file = io.StringIO() + tw = TerminalWriter(file=file) + tw.hasmarkup = False + r.toterminal(tw) + + output = file.getvalue() + for message in ( + "ValueError: abcd", + "IndexError: efgh", + "KeyError: 'ijkl'", + "RuntimeError: mnop", + ): + assert output.count(message) == 1, ( + f"{message!r} should appear exactly once in the output, " + f"got {output.count(message)} occurrences:\n{output}" + ) + def test_exc_chain_repr_cycle(self, importasmod, tw_mock): __tracebackhide__ = True mod = importasmod(