Skip to content

Commit e3b1799

Browse files
bluetechFelhof
andcommitted
code: handle repr'ing empty tracebacks gracefully
By "empty traceback" I mean a traceback all of whose entries have been filtered/cut/pruned out. Currently, if an empty traceback needs to be repr'ed, the last entry before the filtering is used instead (added in accd962). Showing a hidden frame is not so good IMO. This commit does the following instead: 1. Shows details of the exception. 2. Shows a message about how the full trace can be seen. Example: ``` _____________ test _____________ E ZeroDivisionError: division by zero All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames. ``` Also handles `--tb=native`, though there the `--full-trace` bit is not shown. This commit contains some pieces from 431ec6d (which has been reverted). Helps towards fixing issue # 1904. Co-authored-by: Felix Hofstätter <[email protected]>
1 parent eff54ae commit e3b1799

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
@@ -598,9 +599,11 @@ def errisinstance(
598599
"""
599600
return isinstance(self.value, exc)
600601

601-
def _getreprcrash(self) -> "ReprFileLocation":
602+
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
602603
exconly = self.exconly(tryshort=True)
603604
entry = self.traceback.getcrashentry()
605+
if entry is None:
606+
return None
604607
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
605608
return ReprFileLocation(path, lineno + 1, exconly)
606609

@@ -647,7 +650,9 @@ def getrepr(
647650
return ReprExceptionInfo(
648651
reprtraceback=ReprTracebackNative(
649652
traceback.format_exception(
650-
self.type, self.value, self.traceback[0]._rawentry
653+
self.type,
654+
self.value,
655+
self.traceback[0]._rawentry if self.traceback else None,
651656
)
652657
),
653658
reprcrash=self._getreprcrash(),
@@ -803,12 +808,16 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
803808

804809
def repr_traceback_entry(
805810
self,
806-
entry: TracebackEntry,
811+
entry: Optional[TracebackEntry],
807812
excinfo: Optional[ExceptionInfo[BaseException]] = None,
808813
) -> "ReprEntry":
809814
lines: List[str] = []
810-
style = entry._repr_style if entry._repr_style is not None else self.style
811-
if style in ("short", "long"):
815+
style = (
816+
entry._repr_style
817+
if entry is not None and entry._repr_style is not None
818+
else self.style
819+
)
820+
if style in ("short", "long") and entry is not None:
812821
source = self._getentrysource(entry)
813822
if source is None:
814823
source = Source("???")
@@ -857,17 +866,21 @@ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTracebac
857866
else:
858867
extraline = None
859868

869+
if not traceback:
870+
if extraline is None:
871+
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
872+
entries = [self.repr_traceback_entry(None, excinfo)]
873+
return ReprTraceback(entries, extraline, style=self.style)
874+
860875
last = traceback[-1]
861-
entries = []
862876
if self.style == "value":
863-
reprentry = self.repr_traceback_entry(last, excinfo)
864-
entries.append(reprentry)
877+
entries = [self.repr_traceback_entry(last, excinfo)]
865878
return ReprTraceback(entries, None, style=self.style)
866879

867-
for index, entry in enumerate(traceback):
868-
einfo = (last == entry) and excinfo or None
869-
reprentry = self.repr_traceback_entry(entry, einfo)
870-
entries.append(reprentry)
880+
entries = [
881+
self.repr_traceback_entry(entry, excinfo if last == entry else None)
882+
for entry in traceback
883+
]
871884
return ReprTraceback(entries, extraline, style=self.style)
872885

873886
def _truncate_recursive_traceback(
@@ -924,6 +937,7 @@ def repr_excinfo(
924937
seen: Set[int] = set()
925938
while e is not None and id(e) not in seen:
926939
seen.add(id(e))
940+
927941
if excinfo_:
928942
# Fall back to native traceback as a temporary workaround until
929943
# full support for exception groups added to ExceptionInfo.
@@ -950,8 +964,8 @@ def repr_excinfo(
950964
traceback.format_exception(type(e), e, None)
951965
)
952966
reprcrash = None
953-
954967
repr_chain += [(reprtraceback, reprcrash, descr)]
968+
955969
if e.__cause__ is not None and self.chain:
956970
e = e.__cause__
957971
excinfo_ = (
@@ -1042,7 +1056,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10421056
@dataclasses.dataclass(eq=False)
10431057
class ReprExceptionInfo(ExceptionRepr):
10441058
reprtraceback: "ReprTraceback"
1045-
reprcrash: "ReprFileLocation"
1059+
reprcrash: Optional["ReprFileLocation"]
10461060

10471061
def toterminal(self, tw: TerminalWriter) -> None:
10481062
self.reprtraceback.toterminal(tw)
@@ -1147,8 +1161,8 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None:
11471161

11481162
def toterminal(self, tw: TerminalWriter) -> None:
11491163
if self.style == "short":
1150-
assert self.reprfileloc is not None
1151-
self.reprfileloc.toterminal(tw)
1164+
if self.reprfileloc:
1165+
self.reprfileloc.toterminal(tw)
11521166
self._write_entry_lines(tw)
11531167
if self.reprlocals:
11541168
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
@@ -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
@@ -309,12 +310,7 @@ def f():
309310
g()
310311

311312
excinfo = pytest.raises(ValueError, f)
312-
tb = excinfo.traceback
313-
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"
313+
assert excinfo.traceback.getcrashentry() is None
318314

319315

320316
def test_excinfo_exconly():
@@ -1577,18 +1573,19 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
15771573
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
15781574

15791575

1580-
def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None:
1581-
"""Regression test for #10903.
1582-
1583-
We're not really sure what should be *displayed* here, so this test
1584-
just verified that at least it doesn't crash.
1585-
"""
1576+
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
1577+
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
1578+
"""Regression test for #10903."""
15861579
pytester.makepyfile(
15871580
"""
15881581
def test():
15891582
__tracebackhide__ = True
15901583
1 / 0
15911584
"""
15921585
)
1593-
result = pytester.runpytest()
1586+
result = pytester.runpytest("--tb", tbstyle)
15941587
assert result.ret == 1
1588+
if tbstyle != "line":
1589+
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
1590+
if tbstyle not in ("line", "native"):
1591+
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])

0 commit comments

Comments
 (0)