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/14036.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``__tracebackhide__`` is now respected in ``ExceptionGroup`` tracebacks, both for the group itself and for its sub-exceptions.
46 changes: 40 additions & 6 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from traceback import format_exception
from traceback import format_exception_only
from traceback import FrameSummary
from traceback import StackSummary
from traceback import TracebackException
from types import CodeType
from types import FrameType
from types import TracebackType
Expand Down Expand Up @@ -1204,19 +1206,16 @@ 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)
extraline = (
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
if not traceback
else None
)
tb_exc = TracebackException.from_exception(excinfo.value)
_filter_tracebackexception(tb_exc, excinfo.value, self.tbfilter)
reprtraceback = ReprTracebackNative(
format_exception(
type(excinfo.value),
excinfo.value,
traceback[0]._rawentry if traceback else None,
),
list(tb_exc.format()),
extraline=extraline,
)

Expand Down Expand Up @@ -1630,3 +1629,38 @@ def filter_excinfo_traceback(
return excinfo.traceback.filter(excinfo)
else:
return excinfo.traceback


def _filter_tracebackexception(
tb_exc: TracebackException,
e: BaseException,
tbfilter: TracebackFilter,
) -> None:
"""Filter a ``TracebackException`` in-place, respecting ``__tracebackhide__``.

This is used to filter native-style tracebacks (currently the only style
used for ``BaseExceptionGroup``) without mutating the original exception
objects. It recurses into exception group sub-exceptions and into
``__cause__`` / ``__context__`` chains.

Frames are matched by ``(filename, lineno)``: ``TracebackEntry._rawentry.tb_lineno``
is 1-based absolute, matching ``FrameSummary.lineno``.
"""
if e.__traceback__ is not None:
excinfo = ExceptionInfo.from_exception(e)
filtered = filter_excinfo_traceback(tbfilter, excinfo)
kept = {
(str(entry.frame.code.path), entry._rawentry.tb_lineno)
for entry in filtered
}
tb_exc.stack = StackSummary.from_list(
[fs for fs in tb_exc.stack if (fs.filename, fs.lineno) in kept]
)
if isinstance(e, BaseExceptionGroup):
sub_tb_excs = getattr(tb_exc, "exceptions", None) or []
for sub_tb_exc, sub_e in zip(sub_tb_excs, e.exceptions, strict=True):
_filter_tracebackexception(sub_tb_exc, sub_e, tbfilter)
if tb_exc.__cause__ is not None and e.__cause__ is not None:
_filter_tracebackexception(tb_exc.__cause__, e.__cause__, tbfilter)
if tb_exc.__context__ is not None and e.__context__ is not None:
_filter_tracebackexception(tb_exc.__context__, e.__context__, tbfilter)
53 changes: 53 additions & 0 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2011,6 +2011,59 @@ def test():
)


def test_tracebackhide_in_exceptiongroup_is_respected(pytester: Pytester) -> None:
"""ExceptionGroup tracebacks respect __tracebackhide__ (#14036)."""
p = pytester.makepyfile(
"""
import sys
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup

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(
[
"*ExceptionGroup: blah (2 sub-exceptions)*",
"*ZeroDivisionError: division by zero*",
"*AttributeError*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
Loading