Skip to content

Commit e29a6e0

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 e29a6e0

File tree

5 files changed

+46
-34
lines changed

5 files changed

+46
-34
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: 31 additions & 18 deletions
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
@@ -598,9 +598,11 @@ def errisinstance(
598598
"""
599599
return isinstance(self.value, exc)
600600

601-
def _getreprcrash(self) -> "ReprFileLocation":
601+
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
602602
exconly = self.exconly(tryshort=True)
603603
entry = self.traceback.getcrashentry()
604+
if entry is None:
605+
return None
604606
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
605607
return ReprFileLocation(path, lineno + 1, exconly)
606608

@@ -647,7 +649,9 @@ def getrepr(
647649
return ReprExceptionInfo(
648650
reprtraceback=ReprTracebackNative(
649651
traceback.format_exception(
650-
self.type, self.value, self.traceback[0]._rawentry
652+
self.type,
653+
self.value,
654+
self.traceback[0]._rawentry if self.traceback else None,
651655
)
652656
),
653657
reprcrash=self._getreprcrash(),
@@ -803,12 +807,16 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]:
803807

804808
def repr_traceback_entry(
805809
self,
806-
entry: TracebackEntry,
810+
entry: Optional[TracebackEntry],
807811
excinfo: Optional[ExceptionInfo[BaseException]] = None,
808812
) -> "ReprEntry":
809813
lines: List[str] = []
810-
style = entry._repr_style if entry._repr_style is not None else self.style
811-
if style in ("short", "long"):
814+
style = (
815+
entry._repr_style
816+
if entry is not None and entry._repr_style is not None
817+
else self.style
818+
)
819+
if style in ("short", "long") and entry is not None:
812820
source = self._getentrysource(entry)
813821
if source is None:
814822
source = Source("???")
@@ -857,17 +865,21 @@ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTracebac
857865
else:
858866
extraline = None
859867

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

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)
879+
entries = [
880+
self.repr_traceback_entry(entry, excinfo if last == entry else None)
881+
for entry in traceback
882+
]
871883
return ReprTraceback(entries, extraline, style=self.style)
872884

873885
def _truncate_recursive_traceback(
@@ -924,6 +936,7 @@ def repr_excinfo(
924936
seen: Set[int] = set()
925937
while e is not None and id(e) not in seen:
926938
seen.add(id(e))
939+
927940
if excinfo_:
928941
# Fall back to native traceback as a temporary workaround until
929942
# full support for exception groups added to ExceptionInfo.
@@ -950,8 +963,8 @@ def repr_excinfo(
950963
traceback.format_exception(type(e), e, None)
951964
)
952965
reprcrash = None
953-
954966
repr_chain += [(reprtraceback, reprcrash, descr)]
967+
955968
if e.__cause__ is not None and self.chain:
956969
e = e.__cause__
957970
excinfo_ = (
@@ -1042,7 +1055,7 @@ def toterminal(self, tw: TerminalWriter) -> None:
10421055
@dataclasses.dataclass(eq=False)
10431056
class ReprExceptionInfo(ExceptionRepr):
10441057
reprtraceback: "ReprTraceback"
1045-
reprcrash: "ReprFileLocation"
1058+
reprcrash: Optional["ReprFileLocation"]
10461059

10471060
def toterminal(self, tw: TerminalWriter) -> None:
10481061
self.reprtraceback.toterminal(tw)
@@ -1147,8 +1160,8 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None:
11471160

11481161
def toterminal(self, tw: TerminalWriter) -> None:
11491162
if self.style == "short":
1150-
assert self.reprfileloc is not None
1151-
self.reprfileloc.toterminal(tw)
1163+
if self.reprfileloc:
1164+
self.reprfileloc.toterminal(tw)
11521165
self._write_entry_lines(tw)
11531166
if self.reprlocals:
11541167
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: 4 additions & 0 deletions
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

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)