Skip to content

Commit b893d2a

Browse files
authored
Merge pull request #10907 from bluetech/empty-traceback
code: handle repr'ing empty tracebacks gracefully
2 parents 5d13853 + e3b1799 commit b893d2a

File tree

5 files changed

+47
-35
lines changed

5 files changed

+47
-35
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
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

src/_pytest/_code/code.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,14 @@ def filter(
411411
"""
412412
return Traceback(filter(fn, self), self._excinfo)
413413

414-
def getcrashentry(self) -> TracebackEntry:
415-
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
414+
def getcrashentry(self) -> Optional[TracebackEntry]:
415+
"""Return last non-hidden traceback entry that lead to the exception of
416+
a traceback, or None if all hidden."""
416417
for i in range(-1, -len(self) - 1, -1):
417418
entry = self[i]
418419
if not entry.ishidden():
419420
return entry
420-
return self[-1]
421+
return None
421422

422423
def recursionindex(self) -> Optional[int]:
423424
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -621,9 +622,11 @@ def errisinstance(
621622
"""
622623
return isinstance(self.value, exc)
623624

624-
def _getreprcrash(self) -> "ReprFileLocation":
625+
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
625626
exconly = self.exconly(tryshort=True)
626627
entry = self.traceback.getcrashentry()
628+
if entry is None:
629+
return None
627630
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
628631
return ReprFileLocation(path, lineno + 1, exconly)
629632

@@ -670,7 +673,9 @@ def getrepr(
670673
return ReprExceptionInfo(
671674
reprtraceback=ReprTracebackNative(
672675
traceback.format_exception(
673-
self.type, self.value, self.traceback[0]._rawentry
676+
self.type,
677+
self.value,
678+
self.traceback[0]._rawentry if self.traceback else None,
674679
)
675680
),
676681
reprcrash=self._getreprcrash(),
@@ -826,12 +831,16 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
826831

827832
def repr_traceback_entry(
828833
self,
829-
entry: TracebackEntry,
834+
entry: Optional[TracebackEntry],
830835
excinfo: Optional[ExceptionInfo[BaseException]] = None,
831836
) -> "ReprEntry":
832837
lines: List[str] = []
833-
style = entry._repr_style if entry._repr_style is not None else self.style
834-
if style in ("short", "long"):
838+
style = (
839+
entry._repr_style
840+
if entry is not None and entry._repr_style is not None
841+
else self.style
842+
)
843+
if style in ("short", "long") and entry is not None:
835844
source = self._getentrysource(entry)
836845
if source is None:
837846
source = Source("???")
@@ -880,17 +889,21 @@ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTracebac
880889
else:
881890
extraline = None
882891

892+
if not traceback:
893+
if extraline is None:
894+
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
895+
entries = [self.repr_traceback_entry(None, excinfo)]
896+
return ReprTraceback(entries, extraline, style=self.style)
897+
883898
last = traceback[-1]
884-
entries = []
885899
if self.style == "value":
886-
reprentry = self.repr_traceback_entry(last, excinfo)
887-
entries.append(reprentry)
900+
entries = [self.repr_traceback_entry(last, excinfo)]
888901
return ReprTraceback(entries, None, style=self.style)
889902

890-
for index, entry in enumerate(traceback):
891-
einfo = (last == entry) and excinfo or None
892-
reprentry = self.repr_traceback_entry(entry, einfo)
893-
entries.append(reprentry)
903+
entries = [
904+
self.repr_traceback_entry(entry, excinfo if last == entry else None)
905+
for entry in traceback
906+
]
894907
return ReprTraceback(entries, extraline, style=self.style)
895908

896909
def _truncate_recursive_traceback(
@@ -947,6 +960,7 @@ def repr_excinfo(
947960
seen: Set[int] = set()
948961
while e is not None and id(e) not in seen:
949962
seen.add(id(e))
963+
950964
if excinfo_:
951965
# Fall back to native traceback as a temporary workaround until
952966
# full support for exception groups added to ExceptionInfo.
@@ -973,8 +987,8 @@ def repr_excinfo(
973987
traceback.format_exception(type(e), e, None)
974988
)
975989
reprcrash = None
976-
977990
repr_chain += [(reprtraceback, reprcrash, descr)]
991+
978992
if e.__cause__ is not None and self.chain:
979993
e = e.__cause__
980994
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
@@ -1057,7 +1071,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10571071
@dataclasses.dataclass(eq=False)
10581072
class ReprExceptionInfo(ExceptionRepr):
10591073
reprtraceback: "ReprTraceback"
1060-
reprcrash: "ReprFileLocation"
1074+
reprcrash: Optional["ReprFileLocation"]
10611075

10621076
def toterminal(self, tw: TerminalWriter) -> None:
10631077
self.reprtraceback.toterminal(tw)
@@ -1162,8 +1176,8 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None:
11621176

11631177
def toterminal(self, tw: TerminalWriter) -> None:
11641178
if self.style == "short":
1165-
assert self.reprfileloc is not None
1166-
self.reprfileloc.toterminal(tw)
1179+
if self.reprfileloc:
1180+
self.reprfileloc.toterminal(tw)
11671181
self._write_entry_lines(tw)
11681182
if self.reprlocals:
11691183
self.reprlocals.toterminal(tw, indent=" " * 8)

src/_pytest/nodes.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,7 @@ def _repr_failure_py(
452452
if self.config.getoption("fulltrace", False):
453453
style = "long"
454454
else:
455-
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
456455
self._prunetraceback(excinfo)
457-
if len(excinfo.traceback) == 0:
458-
excinfo.traceback = tb
459456
if style == "auto":
460457
style = "long"
461458
# XXX should excinfo.getrepr record all data and toterminal() process it?

src/_pytest/reports.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ 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+
assert (
351+
r is not None
352+
), "There should always be a traceback entry for skipping a test."
350353
if excinfo.value._use_item_location:
351354
path, line = item.reportinfo()[:2]
352355
assert line is not None

testing/code/test_excinfo.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def f():
308308
excinfo = pytest.raises(ValueError, f)
309309
tb = excinfo.traceback
310310
entry = tb.getcrashentry()
311+
assert entry is not None
311312
co = _pytest._code.Code.from_function(h)
312313
assert entry.frame.code.path == co.path
313314
assert entry.lineno == co.firstlineno + 1
@@ -323,12 +324,7 @@ def f():
323324
g()
324325

325326
excinfo = pytest.raises(ValueError, f)
326-
tb = excinfo.traceback
327-
entry = tb.getcrashentry()
328-
co = _pytest._code.Code.from_function(g)
329-
assert entry.frame.code.path == co.path
330-
assert entry.lineno == co.firstlineno + 2
331-
assert entry.frame.code.name == "g"
327+
assert excinfo.traceback.getcrashentry() is None
332328

333329

334330
def test_excinfo_exconly():
@@ -1591,18 +1587,19 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
15911587
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
15921588

15931589

1594-
def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None:
1595-
"""Regression test for #10903.
1596-
1597-
We're not really sure what should be *displayed* here, so this test
1598-
just verified that at least it doesn't crash.
1599-
"""
1590+
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
1591+
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
1592+
"""Regression test for #10903."""
16001593
pytester.makepyfile(
16011594
"""
16021595
def test():
16031596
__tracebackhide__ = True
16041597
1 / 0
16051598
"""
16061599
)
1607-
result = pytester.runpytest()
1600+
result = pytester.runpytest("--tb", tbstyle)
16081601
assert result.ret == 1
1602+
if tbstyle != "line":
1603+
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
1604+
if tbstyle not in ("line", "native"):
1605+
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])

0 commit comments

Comments
 (0)