Skip to content
Open
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 @@ -439,6 +439,7 @@ Sankt Petersbug
Saravanan Padmanaban
Sean Malloy
Segev Finer
SemTiOne
Serhii Mozghovyi
Seth Junot
Shantanu Jain
Expand Down
1 change: 1 addition & 0 deletions changelog/8321.bugfix.rst
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 3 additions & 1 deletion src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
39 changes: 39 additions & 0 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down