Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -346,6 +346,7 @@ Pavel Karateev
Pavel Zhukov
Paweł Adamczak
Pedro Algarvio
Peter Gessler
Petter Strandmark
Philipp Loose
Pierre Sassoulas
Expand Down
1 change: 1 addition & 0 deletions changelog/13380.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :class:`ExceptionGroup` traceback filtering to exclude pytest internals.
63 changes: 41 additions & 22 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
from pathlib import Path
import re
import sys
import traceback
from traceback import extract_tb
from traceback import format_exception
from traceback import format_exception_only
from traceback import FrameSummary
from types import CodeType
from types import FrameType
from types import TracebackType
Expand All @@ -28,6 +30,7 @@
from typing import Literal
from typing import overload
from typing import SupportsIndex
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union

Expand Down Expand Up @@ -208,10 +211,10 @@ def with_repr_style(
def lineno(self) -> int:
return self._rawentry.tb_lineno - 1

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

# Column and end line numbers introduced in python 3.11
Expand Down Expand Up @@ -694,8 +697,7 @@ def getrepr(
showlocals: bool = False,
style: TracebackStyle = "long",
abspath: bool = False,
tbfilter: bool
| Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True,
tbfilter: TracebackFilter = True,
funcargs: bool = False,
truncate_locals: bool = True,
truncate_args: bool = True,
Expand Down Expand Up @@ -742,7 +744,7 @@ def getrepr(
if style == "native":
return ReprExceptionInfo(
reprtraceback=ReprTracebackNative(
traceback.format_exception(
format_exception(
self.type,
self.value,
self.traceback[0]._rawentry if self.traceback else None,
Expand Down Expand Up @@ -851,6 +853,17 @@ def group_contains(
return self._group_contains(self.value, expected_exception, match, depth)


if TYPE_CHECKING:
from typing_extensions import TypeAlias

# Type alias for the `tbfilter` setting:
# bool: If True, it should be filtered using Traceback.filter()
# callable: A callable that takes an ExceptionInfo and returns the filtered traceback.
TracebackFilter: TypeAlias = Union[
bool, Callable[[ExceptionInfo[BaseException]], Traceback]
]


@dataclasses.dataclass
class FormattedExcinfo:
"""Presenting information about failing Functions and Generators."""
Expand All @@ -862,7 +875,7 @@ class FormattedExcinfo:
showlocals: bool = False
style: TracebackStyle = "long"
abspath: bool = True
tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True
tbfilter: TracebackFilter = True
funcargs: bool = False
truncate_locals: bool = True
truncate_args: bool = True
Expand Down Expand Up @@ -1100,11 +1113,7 @@ def _makepath(self, path: Path | str) -> str:
return str(path)

def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback:
traceback = excinfo.traceback
if callable(self.tbfilter):
traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)

if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback)
Expand Down Expand Up @@ -1178,14 +1187,14 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR
# Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo.
# See https://github.com/pytest-dev/pytest/issues/9159
reprtraceback: ReprTraceback | ReprTracebackNative
if isinstance(e, BaseExceptionGroup):
reprtraceback: ReprTracebackNative | ReprTraceback = (
ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
excinfo_.value,
excinfo_.traceback[0]._rawentry,
)
traceback = filter_excinfo_traceback(self.tbfilter, excinfo)
reprtraceback = ReprTracebackNative(
format_exception(
type(excinfo.value),
excinfo.value,
traceback[0]._rawentry,
)
)
else:
Expand All @@ -1194,9 +1203,7 @@ 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(
traceback.format_exception(type(e), e, None)
)
reprtraceback = ReprTracebackNative(format_exception(type(e), e, None))
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]

Expand Down Expand Up @@ -1545,3 +1552,15 @@ def filter_traceback(entry: TracebackEntry) -> bool:
return False

return True


def filter_excinfo_traceback(
tbfilter: TracebackFilter, excinfo: ExceptionInfo[BaseException]
) -> Traceback:
"""Filter the exception traceback in ``excinfo`` according to ``tbfilter``."""
if callable(tbfilter):
return tbfilter(excinfo)
elif tbfilter:
return excinfo.traceback.filter(excinfo)
else:
return excinfo.traceback
3 changes: 3 additions & 0 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,9 @@ def test():
rf"FAILED test_excgroup.py::test - {pre_catch}BaseExceptionGroup: Oops \(2.*"
)
result.stdout.re_match_lines(match_lines)
# Check for traceback filtering of pytest internals.
result.stdout.no_fnmatch_line("*, line *, in pytest_pyfunc_call")
result.stdout.no_fnmatch_line("*, line *, in pytest_runtest_call")


@pytest.mark.skipif(
Expand Down