Skip to content

Commit 7e93d83

Browse files
Fix ExceptionGroup traceback filtering of pytest internals (#13380)
* init * + author * fix * Simplify the code a bit * Small refactor * Import functions from `traceback` directly, to allow free use of `traceback` as a variable name. * Extract `_filtered_traceback` into a function. * Inline `_repr_exception_group_traceback` given it is used only in one place. * Make a type alias for the type of `tbfilter`. * action rerun * + comment * fix docs * Revert "fix docs" This reverts commit fc505cb. * fix --------- Co-authored-by: Bruno Oliveira <[email protected]>
1 parent 61c204a commit 7e93d83

File tree

4 files changed

+47
-22
lines changed

4 files changed

+47
-22
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ Pavel Karateev
346346
Pavel Zhukov
347347
Paweł Adamczak
348348
Pedro Algarvio
349+
Peter Gessler
349350
Petter Strandmark
350351
Philipp Loose
351352
Pierre Sassoulas

changelog/13380.improvement.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix :class:`ExceptionGroup` traceback filtering to exclude pytest internals.

src/_pytest/_code/code.py

+42-22
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
from pathlib import Path
1616
import re
1717
import sys
18-
import traceback
18+
from traceback import extract_tb
19+
from traceback import format_exception
1920
from traceback import format_exception_only
21+
from traceback import FrameSummary
2022
from types import CodeType
2123
from types import FrameType
2224
from types import TracebackType
@@ -28,6 +30,7 @@
2830
from typing import Literal
2931
from typing import overload
3032
from typing import SupportsIndex
33+
from typing import TYPE_CHECKING
3134
from typing import TypeVar
3235
from typing import Union
3336

@@ -208,10 +211,10 @@ def with_repr_style(
208211
def lineno(self) -> int:
209212
return self._rawentry.tb_lineno - 1
210213

211-
def get_python_framesummary(self) -> traceback.FrameSummary:
214+
def get_python_framesummary(self) -> FrameSummary:
212215
# Python's built-in traceback module implements all the nitty gritty
213216
# details to get column numbers of out frames.
214-
stack_summary = traceback.extract_tb(self._rawentry, limit=1)
217+
stack_summary = extract_tb(self._rawentry, limit=1)
215218
return stack_summary[0]
216219

217220
# Column and end line numbers introduced in python 3.11
@@ -694,8 +697,7 @@ def getrepr(
694697
showlocals: bool = False,
695698
style: TracebackStyle = "long",
696699
abspath: bool = False,
697-
tbfilter: bool
698-
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
700+
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True,
699701
funcargs: bool = False,
700702
truncate_locals: bool = True,
701703
truncate_args: bool = True,
@@ -742,7 +744,7 @@ def getrepr(
742744
if style == "native":
743745
return ReprExceptionInfo(
744746
reprtraceback=ReprTracebackNative(
745-
traceback.format_exception(
747+
format_exception(
746748
self.type,
747749
self.value,
748750
self.traceback[0]._rawentry if self.traceback else None,
@@ -851,6 +853,17 @@ def group_contains(
851853
return self._group_contains(self.value, expected_exception, match, depth)
852854

853855

856+
if TYPE_CHECKING:
857+
from typing_extensions import TypeAlias
858+
859+
# Type alias for the `tbfilter` setting:
860+
# bool: If True, it should be filtered using Traceback.filter()
861+
# callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
862+
TracebackFilter: TypeAlias = Union[
863+
bool, Callable[[ExceptionInfo[BaseException]], Traceback]
864+
]
865+
866+
854867
@dataclasses.dataclass
855868
class FormattedExcinfo:
856869
"""Presenting information about failing Functions and Generators."""
@@ -862,7 +875,7 @@ class FormattedExcinfo:
862875
showlocals: bool = False
863876
style: TracebackStyle = "long"
864877
abspath: bool = True
865-
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
878+
tbfilter: TracebackFilter = True
866879
funcargs: bool = False
867880
truncate_locals: bool = True
868881
truncate_args: bool = True
@@ -1100,11 +1113,7 @@ def _makepath(self, path: Path | str) -> str:
11001113
return str(path)
11011114

11021115
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
1103-
traceback = excinfo.traceback
1104-
if callable(self.tbfilter):
1105-
traceback = self.tbfilter(excinfo)
1106-
elif self.tbfilter:
1107-
traceback = traceback.filter(excinfo)
1116+
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
11081117

11091118
if isinstance(excinfo.value, RecursionError):
11101119
traceback, extraline = self._truncate_recursive_traceback(traceback)
@@ -1178,14 +1187,15 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
11781187
# Fall back to native traceback as a temporary workaround until
11791188
# full support for exception groups added to ExceptionInfo.
11801189
# See https://github.com/pytest-dev/pytest/issues/9159
1190+
reprtraceback: ReprTraceback | ReprTracebackNative
11811191
if isinstance(e, BaseExceptionGroup):
1182-
reprtraceback: ReprTracebackNative | ReprTraceback = (
1183-
ReprTracebackNative(
1184-
traceback.format_exception(
1185-
type(excinfo_.value),
1186-
excinfo_.value,
1187-
excinfo_.traceback[0]._rawentry,
1188-
)
1192+
# don't filter any sub-exceptions since they shouldn't have any internal frames
1193+
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
1194+
reprtraceback = ReprTracebackNative(
1195+
format_exception(
1196+
type(excinfo.value),
1197+
excinfo.value,
1198+
traceback[0]._rawentry,
11891199
)
11901200
)
11911201
else:
@@ -1194,9 +1204,7 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
11941204
else:
11951205
# Fallback to native repr if the exception doesn't have a traceback:
11961206
# ExceptionInfo objects require a full traceback to work.
1197-
reprtraceback = ReprTracebackNative(
1198-
traceback.format_exception(type(e), e, None)
1199-
)
1207+
reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
12001208
reprcrash = None
12011209
repr_chain += [(reprtraceback, reprcrash, descr)]
12021210

@@ -1545,3 +1553,15 @@ def filter_traceback(entry: TracebackEntry) -> bool:
15451553
return False
15461554

15471555
return True
1556+
1557+
1558+
def filter_excinfo_traceback(
1559+
tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
1560+
) -> Traceback:
1561+
"""Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
1562+
if callable(tbfilter):
1563+
return tbfilter(excinfo)
1564+
elif tbfilter:
1565+
return excinfo.traceback.filter(excinfo)
1566+
else:
1567+
return excinfo.traceback

testing/code/test_excinfo.py

+3
Original file line numberDiff line numberDiff line change
@@ -1797,6 +1797,9 @@ def test():
17971797
rf"FAILED test_excgroup.py::test - {pre_catch}BaseExceptionGroup: Oops \(2.*"
17981798
)
17991799
result.stdout.re_match_lines(match_lines)
1800+
# Check for traceback filtering of pytest internals.
1801+
result.stdout.no_fnmatch_line("*, line *, in pytest_pyfunc_call")
1802+
result.stdout.no_fnmatch_line("*, line *, in pytest_runtest_call")
18001803

18011804

18021805
@pytest.mark.skipif(

0 commit comments

Comments
 (0)