Skip to content

Commit 431ec6d

Browse files
authored
Correctly handle tracebackhide for chained exceptions (#10772)
1 parent eada68b commit 431ec6d

File tree

6 files changed

+50
-14
lines changed

6 files changed

+50
-14
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Erik M. Bray
128128
Evan Kepner
129129
Fabien Zarifian
130130
Fabio Zadrozny
131+
Felix Hofstätter
131132
Felix Nieuwenhuizen
132133
Feng Ma
133134
Florian Bruhin

changelog/1904.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Correctly handle ``__tracebackhide__`` for chained exceptions.

src/_pytest/_code/code.py

+17-10
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,13 @@ def filter(
411411
"""
412412
return Traceback(filter(fn, self), self._excinfo)
413413

414-
def getcrashentry(self) -> TracebackEntry:
414+
def getcrashentry(self) -> Optional[TracebackEntry]:
415415
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
416416
for i in range(-1, -len(self) - 1, -1):
417417
entry = self[i]
418418
if not entry.ishidden():
419419
return entry
420-
return self[-1]
420+
return None
421421

422422
def recursionindex(self) -> Optional[int]:
423423
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -602,11 +602,13 @@ def errisinstance(
602602
"""
603603
return isinstance(self.value, exc)
604604

605-
def _getreprcrash(self) -> "ReprFileLocation":
605+
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
606606
exconly = self.exconly(tryshort=True)
607607
entry = self.traceback.getcrashentry()
608-
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
609-
return ReprFileLocation(path, lineno + 1, exconly)
608+
if entry:
609+
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
610+
return ReprFileLocation(path, lineno + 1, exconly)
611+
return None
610612

611613
def getrepr(
612614
self,
@@ -942,18 +944,23 @@ def repr_excinfo(
942944
)
943945
else:
944946
reprtraceback = self.repr_traceback(excinfo_)
945-
reprcrash: Optional[ReprFileLocation] = (
946-
excinfo_._getreprcrash() if self.style != "value" else None
947-
)
947+
948+
# will be None if all traceback entries are hidden
949+
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
950+
if reprcrash:
951+
if self.style == "value":
952+
repr_chain += [(reprtraceback, None, descr)]
953+
else:
954+
repr_chain += [(reprtraceback, reprcrash, descr)]
948955
else:
949956
# Fallback to native repr if the exception doesn't have a traceback:
950957
# ExceptionInfo objects require a full traceback to work.
951958
reprtraceback = ReprTracebackNative(
952959
traceback.format_exception(type(e), e, None)
953960
)
954961
reprcrash = None
962+
repr_chain += [(reprtraceback, reprcrash, descr)]
955963

956-
repr_chain += [(reprtraceback, reprcrash, descr)]
957964
if e.__cause__ is not None and self.chain:
958965
e = e.__cause__
959966
excinfo_ = (
@@ -1044,7 +1051,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10441051
@dataclasses.dataclass(eq=False)
10451052
class ReprExceptionInfo(ExceptionRepr):
10461053
reprtraceback: "ReprTraceback"
1047-
reprcrash: "ReprFileLocation"
1054+
reprcrash: Optional["ReprFileLocation"]
10481055

10491056
def toterminal(self, tw: TerminalWriter) -> None:
10501057
self.reprtraceback.toterminal(tw)

src/_pytest/reports.py

+4
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport":
347347
elif isinstance(excinfo.value, skip.Exception):
348348
outcome = "skipped"
349349
r = excinfo._getreprcrash()
350+
if r is None:
351+
raise ValueError(
352+
"There should always be a traceback entry for skipping a test."
353+
)
350354
if excinfo.value._use_item_location:
351355
path, line = item.reportinfo()[:2]
352356
assert line is not None

testing/code/test_excinfo.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ def f():
294294
excinfo = pytest.raises(ValueError, f)
295295
tb = excinfo.traceback
296296
entry = tb.getcrashentry()
297+
assert entry is not None
297298
co = _pytest._code.Code.from_function(h)
298299
assert entry.frame.code.path == co.path
299300
assert entry.lineno == co.firstlineno + 1
@@ -311,10 +312,7 @@ def f():
311312
excinfo = pytest.raises(ValueError, f)
312313
tb = excinfo.traceback
313314
entry = tb.getcrashentry()
314-
co = _pytest._code.Code.from_function(g)
315-
assert entry.frame.code.path == co.path
316-
assert entry.lineno == co.firstlineno + 2
317-
assert entry.frame.code.name == "g"
315+
assert entry is None
318316

319317

320318
def test_excinfo_exconly():

testing/test_tracebackhide.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
def test_tbh_chained(testdir):
2+
"""Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904)."""
3+
p = testdir.makepyfile(
4+
"""
5+
import pytest
6+
7+
def f1():
8+
__tracebackhide__ = True
9+
try:
10+
return f1.meh
11+
except AttributeError:
12+
pytest.fail("fail")
13+
14+
@pytest.fixture
15+
def fix():
16+
f1()
17+
18+
19+
def test(fix):
20+
pass
21+
"""
22+
)
23+
result = testdir.runpytest(str(p))
24+
assert "'function' object has no attribute 'meh'" not in result.stdout.str()
25+
assert result.ret == 1

0 commit comments

Comments
 (0)