diff --git a/AUTHORS b/AUTHORS index 1aa5265e62e..a4c7f856884 100644 --- a/AUTHORS +++ b/AUTHORS @@ -128,7 +128,6 @@ Erik M. Bray Evan Kepner Fabien Zarifian Fabio Zadrozny -Felix Hofstätter Felix Nieuwenhuizen Feng Ma Florian Bruhin diff --git a/changelog/10903.bugfix.rst b/changelog/10903.bugfix.rst new file mode 100644 index 00000000000..34fbcd00221 --- /dev/null +++ b/changelog/10903.bugfix.rst @@ -0,0 +1,2 @@ +Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden. +This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index e57e4fd72a4..5a2a26b87f4 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -82,6 +82,7 @@ Bug Fixes - `#1904 `_: Correctly handle ``__tracebackhide__`` for chained exceptions. + NOTE: This change was reverted in version 7.3.1. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 032b83beb02..9024cc0133f 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -411,13 +411,13 @@ def filter( """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self) -> Optional[TracebackEntry]: + def getcrashentry(self) -> TracebackEntry: """Return last non-hidden traceback entry that lead to the exception of a traceback.""" for i in range(-1, -len(self) - 1, -1): entry = self[i] if not entry.ishidden(): return entry - return None + return self[-1] def recursionindex(self) -> Optional[int]: """Return the index of the frame/TracebackEntry where recursion originates if @@ -602,13 +602,11 @@ def errisinstance( """ return isinstance(self.value, exc) - def _getreprcrash(self) -> Optional["ReprFileLocation"]: + def _getreprcrash(self) -> "ReprFileLocation": exconly = self.exconly(tryshort=True) entry = self.traceback.getcrashentry() - if entry: - path, lineno = entry.frame.code.raw.co_filename, entry.lineno - return ReprFileLocation(path, lineno + 1, exconly) - return None + path, lineno = entry.frame.code.raw.co_filename, entry.lineno + return ReprFileLocation(path, lineno + 1, exconly) def getrepr( self, @@ -946,14 +944,9 @@ def repr_excinfo( ) else: reprtraceback = self.repr_traceback(excinfo_) - - # will be None if all traceback entries are hidden - reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash() - if reprcrash: - if self.style == "value": - repr_chain += [(reprtraceback, None, descr)] - else: - repr_chain += [(reprtraceback, reprcrash, descr)] + reprcrash: Optional[ReprFileLocation] = ( + excinfo_._getreprcrash() if self.style != "value" else None + ) else: # Fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work. @@ -961,8 +954,8 @@ def repr_excinfo( traceback.format_exception(type(e), e, None) ) reprcrash = None - repr_chain += [(reprtraceback, reprcrash, descr)] + repr_chain += [(reprtraceback, reprcrash, descr)] if e.__cause__ is not None and self.chain: e = e.__cause__ excinfo_ = ( @@ -1053,7 +1046,7 @@ def toterminal(self, tw: TerminalWriter) -> None: @dataclasses.dataclass(eq=False) class ReprExceptionInfo(ExceptionRepr): reprtraceback: "ReprTraceback" - reprcrash: Optional["ReprFileLocation"] + reprcrash: "ReprFileLocation" def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index c0a76f92b59..2e36514eae5 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -347,10 +347,6 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() - if r is None: - raise ValueError( - "There should always be a traceback entry for skipping a test." - ) if excinfo.value._use_item_location: path, line = item.reportinfo()[:2] assert line is not None diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 918c972762b..957ac6fc5ae 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -294,7 +294,6 @@ def f(): excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback entry = tb.getcrashentry() - assert entry is not None co = _pytest._code.Code.from_function(h) assert entry.frame.code.path == co.path assert entry.lineno == co.firstlineno + 1 @@ -312,7 +311,10 @@ def f(): excinfo = pytest.raises(ValueError, f) tb = excinfo.traceback entry = tb.getcrashentry() - assert entry is None + co = _pytest._code.Code.from_function(g) + assert entry.frame.code.path == co.path + assert entry.lineno == co.firstlineno + 2 + assert entry.frame.code.name == "g" def test_excinfo_exconly(): @@ -1573,3 +1575,20 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None: # with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it pytest.importorskip("exceptiongroup") _exceptiongroup_common(pytester, outer_chain, inner_chain, native=False) + + +def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None: + """Regression test for #10903. + + We're not really sure what should be *displayed* here, so this test + just verified that at least it doesn't crash. + """ + pytester.makepyfile( + """ + def test(): + __tracebackhide__ = True + 1 / 0 + """ + ) + result = pytester.runpytest() + assert result.ret == 1 diff --git a/testing/test_tracebackhide.py b/testing/test_tracebackhide.py deleted file mode 100644 index 88f9c4fc00e..00000000000 --- a/testing/test_tracebackhide.py +++ /dev/null @@ -1,25 +0,0 @@ -def test_tbh_chained(testdir): - """Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904).""" - p = testdir.makepyfile( - """ - import pytest - - def f1(): - __tracebackhide__ = True - try: - return f1.meh - except AttributeError: - pytest.fail("fail") - - @pytest.fixture - def fix(): - f1() - - - def test(fix): - pass - """ - ) - result = testdir.runpytest(str(p)) - assert "'function' object has no attribute 'meh'" not in result.stdout.str() - assert result.ret == 1