diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 5148dcdbeb4..9b051332baf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -635,7 +635,9 @@ def getrepr( showlocals: bool = False, style: "_TracebackStyle" = "long", abspath: bool = False, - tbfilter: bool = True, + tbfilter: Union[ + bool, Callable[["ExceptionInfo[BaseException]"], Traceback] + ] = True, funcargs: bool = False, truncate_locals: bool = True, chain: bool = True, @@ -652,9 +654,15 @@ def getrepr( :param bool abspath: If paths should be changed to absolute or left unchanged. - :param bool tbfilter: - Hide entries that contain a local variable ``__tracebackhide__==True``. - Ignored if ``style=="native"``. + :param tbfilter: + A filter for traceback entries. + + * If false, don't hide any entries. + * If true, hide internal entries and entries that contain a local + variable ``__tracebackhide__ = True``. + * If a callable, delegates the filtering to the callable. + + Ignored if ``style`` is ``"native"``. :param bool funcargs: Show fixtures ("funcargs" for legacy purposes) per traceback entry. @@ -719,7 +727,7 @@ class FormattedExcinfo: showlocals: bool = False style: "_TracebackStyle" = "long" abspath: bool = True - tbfilter: bool = True + tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True funcargs: bool = False truncate_locals: bool = True chain: bool = True @@ -881,7 +889,9 @@ def _makepath(self, path: Union[Path, str]) -> str: def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": traceback = excinfo.traceback - if self.tbfilter: + if callable(self.tbfilter): + traceback = self.tbfilter(excinfo) + elif self.tbfilter: traceback = traceback.filter(excinfo) if isinstance(excinfo.value, RecursionError): diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 1a1a47a28d4..dbd6b0a4273 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -450,10 +450,13 @@ def _repr_failure_py( style = "value" if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() + + tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] if self.config.getoption("fulltrace", False): style = "long" + tbfilter = False else: - excinfo.traceback = self._traceback_filter(excinfo) + tbfilter = self._traceback_filter if style == "auto": style = "long" # XXX should excinfo.getrepr record all data and toterminal() process it? @@ -484,7 +487,7 @@ def _repr_failure_py( abspath=abspath, showlocals=self.config.getoption("showlocals", False), style=style, - tbfilter=False, # pruned already, or in --fulltrace mode. + tbfilter=tbfilter, truncate_locals=truncate_locals, ) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index bda6cd4cdd0..88aa5f0f154 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1603,3 +1603,48 @@ def test(): result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"]) if tbstyle not in ("line", "native"): result.stdout.fnmatch_lines(["All traceback entries are hidden.*"]) + + +def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None: + """Hidden entries of chained exceptions are not shown (#1904).""" + p = pytester.makepyfile( + """ + def g1(): + __tracebackhide__ = True + str.does_not_exist + + def f3(): + __tracebackhide__ = True + 1 / 0 + + def f2(): + try: + f3() + except Exception: + g1() + + def f1(): + __tracebackhide__ = True + f2() + + def test(): + f1() + """ + ) + result = pytester.runpytest(str(p), "--tb=short") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*.py:11: in f2", + " f3()", + "E ZeroDivisionError: division by zero", + "", + "During handling of the above exception, another exception occurred:", + "*.py:20: in test", + " f1()", + "*.py:13: in f2", + " g1()", + "E AttributeError:*'does_not_exist'", + ], + consecutive=True, + )