From 9611aff38678badcbae5c11f80846d527cca29e3 Mon Sep 17 00:00:00 2001 From: agent3 Date: Tue, 16 Dec 2025 01:10:27 +0000 Subject: [PATCH 1/2] Fix ExceptionGroup to respect __tracebackhide__ (fixes #14036) --- src/_pytest/_code/code.py | 58 +++++++++++++++++++++++++++++++----- testing/code/test_excinfo.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 4cf99a77340..f95a73407d5 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1187,15 +1187,59 @@ 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) - reprtraceback = ReprTracebackNative( - format_exception( - type(excinfo.value), - excinfo.value, - traceback[0]._rawentry if traceback else None, + + patched: list[tuple[BaseException, TracebackType | None]] = [] + + def patch_group(group: BaseExceptionGroup[BaseException]) -> None: + for sub in group.exceptions: + if isinstance(sub, BaseExceptionGroup): + patch_group(sub) + continue + patched.append((sub, sub.__traceback__)) + try: + sub_excinfo = ExceptionInfo.from_exception(sub) + except Exception: + sub.__traceback__ = None + continue + sub_tb = filter_excinfo_traceback(self.tbfilter, sub_excinfo) + if sub_tb: + # Ensure the last frame's tb_next is None + sub_tb[-1]._rawentry.tb_next = None + # Link the filtered frames together + for i in range(len(sub_tb) - 1): + sub_tb[i]._rawentry.tb_next = sub_tb[i + 1]._rawentry + sub.__traceback__ = sub_tb[0]._rawentry + else: + sub.__traceback__ = None + + old_group_tb = e.__traceback__ + try: + # Build a filtered traceback chain for the group + if traceback: + # First, ensure the last frame's tb_next is None to prevent + # format_exception from walking into hidden frames + traceback[-1]._rawentry.tb_next = None + # Then link the filtered frames together + for i in range(len(traceback) - 1): + traceback[i]._rawentry.tb_next = traceback[i + 1]._rawentry + e.__traceback__ = traceback[0]._rawentry + else: + e.__traceback__ = None + + patch_group(e) + reprtraceback = ReprTracebackNative( + format_exception( + type(excinfo.value), + excinfo.value, + e.__traceback__, + ) ) - ) + finally: + e.__traceback__ = old_group_tb + for sub, old_tb in patched: + sub.__traceback__ = old_tb + if not traceback: reprtraceback.extraline = ( "All traceback entries are hidden. " diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 70499fec893..39bef9269d3 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1963,6 +1963,59 @@ def test(): ) +def test_tracebackhide_in_exceptiongroup_is_respected(pytester: Pytester) -> None: + """Regression test for issue #14036.""" + p = pytester.makepyfile( + """ + 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( + [ + "*in test*", + "*f1()*", + "*ExceptionGroup: blah (2 sub-exceptions)*", + "*ZeroDivisionError: division by zero*", + "*AttributeError: type object 'str' has no attribute '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): From 99b354238ca186604bf79919553ea9102bb18f2a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:11:43 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/_code/code.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index f95a73407d5..7f85bc8093e 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1202,13 +1202,17 @@ def patch_group(group: BaseExceptionGroup[BaseException]) -> None: except Exception: sub.__traceback__ = None continue - sub_tb = filter_excinfo_traceback(self.tbfilter, sub_excinfo) + sub_tb = filter_excinfo_traceback( + self.tbfilter, sub_excinfo + ) if sub_tb: # Ensure the last frame's tb_next is None sub_tb[-1]._rawentry.tb_next = None # Link the filtered frames together for i in range(len(sub_tb) - 1): - sub_tb[i]._rawentry.tb_next = sub_tb[i + 1]._rawentry + sub_tb[i]._rawentry.tb_next = sub_tb[ + i + 1 + ]._rawentry sub.__traceback__ = sub_tb[0]._rawentry else: sub.__traceback__ = None @@ -1222,11 +1226,13 @@ def patch_group(group: BaseExceptionGroup[BaseException]) -> None: traceback[-1]._rawentry.tb_next = None # Then link the filtered frames together for i in range(len(traceback) - 1): - traceback[i]._rawentry.tb_next = traceback[i + 1]._rawentry + traceback[i]._rawentry.tb_next = traceback[ + i + 1 + ]._rawentry e.__traceback__ = traceback[0]._rawentry else: e.__traceback__ = None - + patch_group(e) reprtraceback = ReprTracebackNative( format_exception(