From d03f09c68fe28b211a802ca7ed8d25f9e8efcc4e Mon Sep 17 00:00:00 2001 From: Amine-LG Date: Wed, 26 Aug 2026 13:54:09 +0100 Subject: [PATCH] Fix __tracebackhide__ for same-location ExceptionGroup frames --- AUTHORS | 1 + changelog/14940.bugfix.rst | 1 + src/_pytest/_code/code.py | 15 +++++----- testing/code/test_excinfo.py | 53 ++++++++++++++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 changelog/14940.bugfix.rst diff --git a/AUTHORS b/AUTHORS index d1a2d3e7911..15f73ddb171 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,6 +28,7 @@ algojogacor Alice Purcell Allan Feldman Aly Sivji +Amine-LG Amir Elkess Ammar Askar Anatoly Bubenkoff diff --git a/changelog/14940.bugfix.rst b/changelog/14940.bugfix.rst new file mode 100644 index 00000000000..bd0839ed2fb --- /dev/null +++ b/changelog/14940.bugfix.rst @@ -0,0 +1 @@ +Fixed ``__tracebackhide__`` not hiding frames that share a source location in ``ExceptionGroup`` tracebacks. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e7712c48bf4..630fc93688a 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1658,18 +1658,19 @@ def _filter_tracebackexception( 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``. + Frames are filtered based on the identity of their corresponding raw + traceback entries. """ 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 - } + kept = {id(entry._rawentry) for entry in filtered} tb_exc.stack = StackSummary.from_list( - [fs for fs in tb_exc.stack if (fs.filename, fs.lineno) in kept] + [ + fs + for entry, fs in zip(excinfo.traceback, tb_exc.stack, strict=False) + if id(entry._rawentry) in kept + ] ) if isinstance(e, BaseExceptionGroup): sub_tb_excs = getattr(tb_exc, "exceptions", None) or [] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index ec9f584dfba..56fa6f6cfab 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -2220,6 +2220,59 @@ def test(): result.stdout.no_fnmatch_line("*in g1*") +def test_tracebackhide_in_exceptiongroup_distinguishes_same_line_frames( + pytester: Pytester, +) -> None: + p = pytester.makepyfile( + """ + import sys + if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + + def fail(number): + __tracebackhide__ = number == 1 + if number == 0: + raise ValueError("boom") + fail(number - 1) + + def test(): + try: + fail(2) + except ValueError as error: + raise ExceptionGroup("failure", [error]) from None + """ + ) + result = pytester.runpytest(str(p), "--tb=short") + assert result.ret == 1 + assert result.stdout.str().count("fail(number - 1)") == 1 + + +def test_tracebackhide_in_exceptiongroup_with_tracebacklimit( + pytester: Pytester, +) -> None: + p = pytester.makepyfile( + """ + import sys + if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + + def fail(): + __tracebackhide__ = True + raise ValueError("boom") + + def test(monkeypatch): + monkeypatch.setattr(sys, "tracebacklimit", 1, raising=False) + try: + fail() + except ValueError as error: + raise ExceptionGroup("failure", [error]) from None + """ + ) + result = pytester.runpytest(str(p), "--tb=short") + assert result.ret == 1 + result.stdout.fnmatch_lines(["*ValueError: boom*"]) + + def add_note(err: BaseException, msg: str) -> None: """Adds a note to an exception inplace.""" if sys.version_info < (3, 11):