From 0e97dd1d0091f1fb75b7e5726b808c0ddedef128 Mon Sep 17 00:00:00 2001 From: ShubhamDesai <42180509+ShubhamDesai@users.noreply.github.com> Date: Fri, 9 May 2025 19:53:07 -0400 Subject: [PATCH 1/4] init --- src/_pytest/_code/code.py | 46 +++++++++++++++++++++--------------- testing/code/test_excinfo.py | 5 +++- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 2c872df3008..2faed344182 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1099,35 +1099,51 @@ def _makepath(self, path: Path | str) -> str: return np return str(path) - def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: - traceback = excinfo.traceback + def _filtered_traceback(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: + traceback_ = excinfo.traceback if callable(self.tbfilter): - traceback = self.tbfilter(excinfo) + traceback_ = self.tbfilter(excinfo) elif self.tbfilter: - traceback = traceback.filter(excinfo) + traceback_ = traceback_.filter(excinfo) + return traceback_ + + def _repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: + traceback_ = self._filtered_traceback(excinfo) if isinstance(excinfo.value, RecursionError): - traceback, extraline = self._truncate_recursive_traceback(traceback) + traceback_, extraline = self._truncate_recursive_traceback(traceback_) else: extraline = None - if not traceback: + if not traceback_: if extraline is None: extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." entries = [self.repr_traceback_entry(None, excinfo)] return ReprTraceback(entries, extraline, style=self.style) - last = traceback[-1] + last = traceback_[-1] if self.style == "value": entries = [self.repr_traceback_entry(last, excinfo)] return ReprTraceback(entries, None, style=self.style) entries = [ self.repr_traceback_entry(entry, excinfo if last == entry else None) - for entry in traceback + for entry in traceback_ ] return ReprTraceback(entries, extraline, style=self.style) + def _repr_exception_group_traceback( + self, excinfo: ExceptionInfo[BaseExceptionGroup] + ) -> ReprTracebackNative: + traceback_ = self._filtered_traceback(excinfo) + return ReprTracebackNative( + traceback.format_exception( + type(excinfo.value), + excinfo.value, + traceback_[0]._rawentry, + ) + ) + def _truncate_recursive_traceback( self, traceback: Traceback ) -> tuple[Traceback, str | None]: @@ -1179,17 +1195,9 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR # full support for exception groups added to ExceptionInfo. # See https://github.com/pytest-dev/pytest/issues/9159 if isinstance(e, BaseExceptionGroup): - reprtraceback: ReprTracebackNative | ReprTraceback = ( - ReprTracebackNative( - traceback.format_exception( - type(excinfo_.value), - excinfo_.value, - excinfo_.traceback[0]._rawentry, - ) - ) - ) + reprtraceback = self._repr_exception_group_traceback(excinfo_) else: - reprtraceback = self.repr_traceback(excinfo_) + reprtraceback = self._repr_traceback(excinfo_) reprcrash = excinfo_._getreprcrash() else: # Fallback to native repr if the exception doesn't have a traceback: @@ -1544,4 +1552,4 @@ def filter_traceback(entry: TracebackEntry) -> bool: if _PYTEST_DIR in parents: return False - return True + return True \ No newline at end of file diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 89088576980..48c4bd57aad 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -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( @@ -2001,4 +2004,4 @@ def test_check_error_notes_failure( with pytest.raises(AssertionError): with pytest.raises(type(error), match=match): - raise error + raise error \ No newline at end of file From eb62583a4cbbf6c55f4506701a8fb6baf0e70456 Mon Sep 17 00:00:00 2001 From: ShubhamDesai <42180509+ShubhamDesai@users.noreply.github.com> Date: Fri, 9 May 2025 20:37:39 -0400 Subject: [PATCH 2/4] + author --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 95e6b13f11e..d94c403a124 100644 --- a/AUTHORS +++ b/AUTHORS @@ -345,6 +345,7 @@ Pavel Karateev Pavel Zhukov Paweł Adamczak Pedro Algarvio +Peter Gessler Petter Strandmark Philipp Loose Pierre Sassoulas From 6e2cde9ddb63c0f326cf1dc9bd15443917b92c5b Mon Sep 17 00:00:00 2001 From: ShubhamDesai <42180509+ShubhamDesai@users.noreply.github.com> Date: Fri, 9 May 2025 21:37:14 -0400 Subject: [PATCH 3/4] fix --- changelog/13380.bugfix.rst | 1 + src/_pytest/_code/code.py | 10 ++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 changelog/13380.bugfix.rst diff --git a/changelog/13380.bugfix.rst b/changelog/13380.bugfix.rst new file mode 100644 index 00000000000..970e6168254 --- /dev/null +++ b/changelog/13380.bugfix.rst @@ -0,0 +1 @@ +Fix :class:`ExceptionGroup` traceback filtering to exclude pytest internals. \ No newline at end of file diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 2faed344182..e1098ac7c19 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1107,7 +1107,7 @@ def _filtered_traceback(self, excinfo: ExceptionInfo[BaseException]) -> Tracebac traceback_ = traceback_.filter(excinfo) return traceback_ - def _repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: + def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: traceback_ = self._filtered_traceback(excinfo) if isinstance(excinfo.value, RecursionError): @@ -1133,7 +1133,7 @@ def _repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTracebac return ReprTraceback(entries, extraline, style=self.style) def _repr_exception_group_traceback( - self, excinfo: ExceptionInfo[BaseExceptionGroup] + self, excinfo: ExceptionInfo[BaseException] ) -> ReprTracebackNative: traceback_ = self._filtered_traceback(excinfo) return ReprTracebackNative( @@ -1195,9 +1195,11 @@ def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainR # full support for exception groups added to ExceptionInfo. # See https://github.com/pytest-dev/pytest/issues/9159 if isinstance(e, BaseExceptionGroup): - reprtraceback = self._repr_exception_group_traceback(excinfo_) + reprtraceback: ReprTracebackNative | ReprTraceback = ( + self._repr_exception_group_traceback(excinfo_) + ) else: - reprtraceback = self._repr_traceback(excinfo_) + reprtraceback = self.repr_traceback(excinfo_) reprcrash = excinfo_._getreprcrash() else: # Fallback to native repr if the exception doesn't have a traceback: From e9553e275846db9ac69b1caca007b793479f9c7e Mon Sep 17 00:00:00 2001 From: ShubhamDesai <42180509+ShubhamDesai@users.noreply.github.com> Date: Fri, 9 May 2025 22:39:57 -0400 Subject: [PATCH 4/4] Small refactor --- changelog/13380.improvement.rst | 1 + src/_pytest/_code/code.py | 85 ++++++++++++++++++--------------- testing/code/test_excinfo.py | 2 +- 3 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 changelog/13380.improvement.rst diff --git a/changelog/13380.improvement.rst b/changelog/13380.improvement.rst new file mode 100644 index 00000000000..970e6168254 --- /dev/null +++ b/changelog/13380.improvement.rst @@ -0,0 +1 @@ +Fix :class:`ExceptionGroup` traceback filtering to exclude pytest internals. \ No newline at end of file diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e1098ac7c19..bca18ff733e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -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 @@ -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 @@ -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 @@ -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, @@ -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, @@ -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.""" @@ -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 @@ -1099,51 +1112,31 @@ def _makepath(self, path: Path | str) -> str: return np return str(path) - def _filtered_traceback(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: - traceback_ = excinfo.traceback - if callable(self.tbfilter): - traceback_ = self.tbfilter(excinfo) - elif self.tbfilter: - traceback_ = traceback_.filter(excinfo) - return traceback_ - def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: - traceback_ = self._filtered_traceback(excinfo) + traceback = filter_excinfo_traceback(self.tbfilter, excinfo) if isinstance(excinfo.value, RecursionError): - traceback_, extraline = self._truncate_recursive_traceback(traceback_) + traceback, extraline = self._truncate_recursive_traceback(traceback) else: extraline = None - if not traceback_: + if not traceback: if extraline is None: extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames." entries = [self.repr_traceback_entry(None, excinfo)] return ReprTraceback(entries, extraline, style=self.style) - last = traceback_[-1] + last = traceback[-1] if self.style == "value": entries = [self.repr_traceback_entry(last, excinfo)] return ReprTraceback(entries, None, style=self.style) entries = [ self.repr_traceback_entry(entry, excinfo if last == entry else None) - for entry in traceback_ + for entry in traceback ] return ReprTraceback(entries, extraline, style=self.style) - def _repr_exception_group_traceback( - self, excinfo: ExceptionInfo[BaseException] - ) -> ReprTracebackNative: - traceback_ = self._filtered_traceback(excinfo) - return ReprTracebackNative( - traceback.format_exception( - type(excinfo.value), - excinfo.value, - traceback_[0]._rawentry, - ) - ) - def _truncate_recursive_traceback( self, traceback: Traceback ) -> tuple[Traceback, str | None]: @@ -1194,9 +1187,15 @@ 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 = ( - self._repr_exception_group_traceback(excinfo_) + traceback = filter_excinfo_traceback(self.tbfilter, excinfo) + reprtraceback = ReprTracebackNative( + format_exception( + type(excinfo.value), + excinfo.value, + traceback[0]._rawentry, + ) ) else: reprtraceback = self.repr_traceback(excinfo_) @@ -1204,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)] @@ -1554,4 +1551,16 @@ def filter_traceback(entry: TracebackEntry) -> bool: if _PYTEST_DIR in parents: return False - return True \ No newline at end of file + 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 \ No newline at end of file diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 48c4bd57aad..bafc1f0a7cd 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1797,7 +1797,7 @@ 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 + # 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")