Correctly handle tracebackhide for chained exceptions (#10772)

This commit is contained in:
Felix Hofstätter 2023-03-15 12:10:25 +01:00 committed by GitHub
parent eada68b2b3
commit 431ec6d34e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 50 additions and 14 deletions

View File

@ -128,6 +128,7 @@ Erik M. Bray
Evan Kepner Evan Kepner
Fabien Zarifian Fabien Zarifian
Fabio Zadrozny Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen Felix Nieuwenhuizen
Feng Ma Feng Ma
Florian Bruhin Florian Bruhin

View File

@ -0,0 +1 @@
Correctly handle ``__tracebackhide__`` for chained exceptions.

View File

@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]):
""" """
return Traceback(filter(fn, self), self._excinfo) return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> TracebackEntry: def getcrashentry(self) -> Optional[TracebackEntry]:
"""Return last non-hidden traceback entry that lead to the exception of a traceback.""" """Return last non-hidden traceback entry that lead to the exception of a traceback."""
for i in range(-1, -len(self) - 1, -1): for i in range(-1, -len(self) - 1, -1):
entry = self[i] entry = self[i]
if not entry.ishidden(): if not entry.ishidden():
return entry return entry
return self[-1] return None
def recursionindex(self) -> Optional[int]: def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if """Return the index of the frame/TracebackEntry where recursion originates if
@ -602,11 +602,13 @@ class ExceptionInfo(Generic[E]):
""" """
return isinstance(self.value, exc) return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation": def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True) exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry() entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno if entry:
return ReprFileLocation(path, lineno + 1, exconly) path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr( def getrepr(
self, self,
@ -942,9 +944,14 @@ class FormattedExcinfo:
) )
else: else:
reprtraceback = self.repr_traceback(excinfo_) reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None # 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)]
else: else:
# Fallback to native repr if the exception doesn't have a traceback: # Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work. # ExceptionInfo objects require a full traceback to work.
@ -952,8 +959,8 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None) traceback.format_exception(type(e), e, None)
) )
reprcrash = None reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]
repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain: if e.__cause__ is not None and self.chain:
e = e.__cause__ e = e.__cause__
excinfo_ = ( excinfo_ = (
@ -1044,7 +1051,7 @@ class ExceptionChainRepr(ExceptionRepr):
@dataclasses.dataclass(eq=False) @dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback" reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation" reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw) self.reprtraceback.toterminal(tw)

View File

@ -347,6 +347,10 @@ class TestReport(BaseReport):
elif isinstance(excinfo.value, skip.Exception): elif isinstance(excinfo.value, skip.Exception):
outcome = "skipped" outcome = "skipped"
r = excinfo._getreprcrash() 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: if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2] path, line = item.reportinfo()[:2]
assert line is not None assert line is not None

View File

@ -294,6 +294,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f) excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback tb = excinfo.traceback
entry = tb.getcrashentry() entry = tb.getcrashentry()
assert entry is not None
co = _pytest._code.Code.from_function(h) co = _pytest._code.Code.from_function(h)
assert entry.frame.code.path == co.path assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 1 assert entry.lineno == co.firstlineno + 1
@ -311,10 +312,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f) excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback tb = excinfo.traceback
entry = tb.getcrashentry() entry = tb.getcrashentry()
co = _pytest._code.Code.from_function(g) assert entry is None
assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 2
assert entry.frame.code.name == "g"
def test_excinfo_exconly(): def test_excinfo_exconly():

View File

@ -0,0 +1,25 @@
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