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
64 changes: 57 additions & 7 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1187,15 +1187,65 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
# See https://github.com/pytest-dev/pytest/issues/9159
reprtraceback: ReprTraceback | ReprTracebackNative
if isinstance(e, BaseExceptionGroup):
# don't filter any sub-exceptions since they shouldn't have any internal frames
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
reprtraceback = ReprTracebackNative(
format_exception(
type(excinfo.value),
excinfo.value,
traceback[0]._rawentry if traceback else None,

patched: list[tuple[BaseException, TracebackType | None]] = []

def patch_group(group: BaseExceptionGroup[BaseException]) -> None:
for sub in group.exceptions:
if isinstance(sub, BaseExceptionGroup):
patch_group(sub)
continue
patched.append((sub, sub.__traceback__))
try:
sub_excinfo = ExceptionInfo.from_exception(sub)
except Exception:
sub.__traceback__ = None
continue
sub_tb = filter_excinfo_traceback(
self.tbfilter, sub_excinfo
)
if sub_tb:
# Ensure the last frame's tb_next is None
sub_tb[-1]._rawentry.tb_next = None
# Link the filtered frames together
for i in range(len(sub_tb) - 1):
sub_tb[i]._rawentry.tb_next = sub_tb[
i + 1
]._rawentry
sub.__traceback__ = sub_tb[0]._rawentry
else:
sub.__traceback__ = None

old_group_tb = e.__traceback__
try:
# Build a filtered traceback chain for the group
if traceback:
# First, ensure the last frame's tb_next is None to prevent
# format_exception from walking into hidden frames
traceback[-1]._rawentry.tb_next = None
# Then link the filtered frames together
for i in range(len(traceback) - 1):
traceback[i]._rawentry.tb_next = traceback[
i + 1
]._rawentry
e.__traceback__ = traceback[0]._rawentry
else:
e.__traceback__ = None

patch_group(e)
reprtraceback = ReprTracebackNative(
format_exception(
type(excinfo.value),
excinfo.value,
e.__traceback__,
)
)
)
finally:
e.__traceback__ = old_group_tb
for sub, old_tb in patched:
sub.__traceback__ = old_tb

if not traceback:
reprtraceback.extraline = (
"All traceback entries are hidden. "
Expand Down
53 changes: 53 additions & 0 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,59 @@ def test():
)


def test_tracebackhide_in_exceptiongroup_is_respected(pytester: Pytester) -> None:
"""Regression test for issue #14036."""
p = pytester.makepyfile(
"""
def g1():
__tracebackhide__ = True
str.does_not_exist
def f3():
__tracebackhide__ = True
1 / 0
def f2():
__tracebackhide__ = True
exc = None
try:
f3()
except Exception as e:
exc = e
exc2 = None
try:
g1()
except Exception as e:
exc2 = e
raise ExceptionGroup("blah", [exc, exc2])
def f1():
__tracebackhide__ = True
f2()
def test():
f1()
"""
)
result = pytester.runpytest(str(p), "--tb=short")
assert result.ret == 1
result.stdout.fnmatch_lines(
[
"*in test*",
"*f1()*",
"*ExceptionGroup: blah (2 sub-exceptions)*",
"*ZeroDivisionError: division by zero*",
"*AttributeError: type object 'str' has no attribute 'does_not_exist'*",
]
)
result.stdout.no_fnmatch_line("*in f1*")
result.stdout.no_fnmatch_line("*in f2*")
result.stdout.no_fnmatch_line("*in f3*")
result.stdout.no_fnmatch_line("*in g1*")


def add_note(err: BaseException, msg: str) -> None:
"""Adds a note to an exception inplace."""
if sys.version_info < (3, 11):
Expand Down