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 changelog/14828.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Exception groups raised from another exception now print their cause chain only once in the traceback output.
4 changes: 3 additions & 1 deletion src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,9 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
reprcrash = None
repr_chain.append((reprtraceback, reprcrash, description))

if e.__cause__ is not None and self.chain:
if isinstance(e, BaseExceptionGroup) and self.chain:
e = None
elif e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
description = "The above exception was the direct cause of the following exception:"
Expand Down
76 changes: 62 additions & 14 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,66 @@ def g():
]
)

def _render_output(self, excinfo: ExceptionInfo[BaseException]) -> str:
r = excinfo.getrepr()
file = io.StringIO()
tw = TerminalWriter(file=file)
tw.hasmarkup = False
r.toterminal(tw)
return file.getvalue()

def test_exc_chain_repr_exception_group_with_cause(self) -> None:
"""An exception group raised from another exception must not print that
exception's cause chain twice."""
try:
try:
raise RuntimeError("original cause")
except RuntimeError as exc:
raise ExceptionGroup("group", [ValueError("inner")]) from exc
except ExceptionGroup:
excinfo = ExceptionInfo.from_current()

output = self._render_output(excinfo)
assert output.count("RuntimeError: original cause") == 1
assert output.count("ExceptionGroup: group") == 1
assert output.count("ValueError: inner") == 1
assert output.count("The above exception was the direct cause") == 1

def test_exc_chain_repr_exception_group_with_context(self) -> None:
"""An exception group raised during handling must not print the
implicit context chain twice."""
exc1 = RuntimeError("implicit context")
try:
raise exc1
except RuntimeError as exc:
group = ExceptionGroup("group", [ValueError("inner")])
group.__context__ = exc
try:
raise group
except ExceptionGroup:
excinfo = ExceptionInfo.from_current()

output = self._render_output(excinfo)
assert output.count("RuntimeError: implicit context") == 1
assert output.count("During handling of the above exception") == 1

def test_exc_chain_repr_nested_exception_group_with_cause(self) -> None:
"""A nested exception group raised from another exception must not
print the cause chain twice."""
try:
try:
raise RuntimeError("root cause")
except RuntimeError as exc:
raise ExceptionGroup(
"outer", [ExceptionGroup("inner", [ValueError("v")])]
) from exc
except ExceptionGroup:
excinfo = ExceptionInfo.from_current()

output = self._render_output(excinfo)
assert output.count("RuntimeError: root cause") == 1
assert output.count("The above exception was the direct cause") == 1

def test_exc_chain_repr_without_traceback_multiple_links(self) -> None:
"""
Exceptions without a traceback that are themselves part of a longer
Expand All @@ -1613,13 +1673,7 @@ def test_exc_chain_repr_without_traceback_multiple_links(self) -> None:
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()
output = self._render_output(excinfo)
for message in (
"ValueError: abcd",
"IndexError: efgh",
Expand Down Expand Up @@ -1648,13 +1702,7 @@ def test_exc_chain_repr_mixed_traceback(self) -> None:
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()
output = self._render_output(excinfo)
assert output.count("ValueError: outer without traceback") == 1
assert output.count("RuntimeError: inner with traceback") == 1
assert output.count("The above exception was the direct cause") == 1
Expand Down
2 changes: 1 addition & 1 deletion testing/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,7 +1609,7 @@ def test(self):
result.stdout.fnmatch_lines(
[
"* ERROR at setup of MyTestCase.test *",
"E * Exception: fail 0",
"*Exception: fail 0",
]
)

Expand Down