Skip to content

Commit 8a67dc7

Browse files
committed
refactor interesting_origin
1 parent 3962442 commit 8a67dc7

3 files changed

Lines changed: 55 additions & 22 deletions

File tree

hypothesis-python/src/hypothesis/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@
8080
from hypothesis.internal.conjecture.shrinker import sort_key
8181
from hypothesis.internal.entropy import deterministic_PRNG
8282
from hypothesis.internal.escalation import (
83+
InterestingOrigin,
8384
current_pytest_item,
8485
escalate_hypothesis_internal_error,
8586
format_exception,
86-
get_interesting_origin,
8787
get_trimmed_traceback,
8888
)
8989
from hypothesis.internal.healthcheck import fail_health_check
@@ -970,7 +970,7 @@ def _execute_once_for_engine(self, data):
970970

971971
self.failed_normally = True
972972

973-
interesting_origin = get_interesting_origin(e)
973+
interesting_origin = InterestingOrigin.from_exception(e)
974974
if trace: # pragma: no cover
975975
# Trace collection is explicitly disabled under coverage.
976976
self.explain_traces[interesting_origin].add(trace)

hypothesis-python/src/hypothesis/internal/escalation.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
import contextlib
1212
import os
1313
import sys
14+
import textwrap
1415
import traceback
1516
from inspect import getframeinfo
1617
from pathlib import Path
17-
from typing import Dict
18+
from typing import Dict, NamedTuple, Type
1819

1920
import hypothesis
2021
from hypothesis.errors import (
@@ -105,32 +106,46 @@ def get_trimmed_traceback(exception=None):
105106
return tb
106107

107108

108-
def get_interesting_origin(exception):
109+
class InterestingOrigin(NamedTuple):
109110
# The `interesting_origin` is how Hypothesis distinguishes between multiple
110111
# failures, for reporting and also to replay from the example database (even
111112
# if report_multiple_bugs=False). We traditionally use the exception type and
112113
# location, but have extracted this logic in order to see through `except ...:`
113114
# blocks and understand the __cause__ (`raise x from y`) or __context__ that
114115
# first raised an exception as well as PEP-654 exception groups.
115-
tb = get_trimmed_traceback(exception)
116-
if tb is None:
116+
exc_type: Type[BaseException]
117+
filename: str
118+
lineno: int
119+
context: "InterestingOrigin | tuple[()]"
120+
group_elems: "tuple[InterestingOrigin, ...]"
121+
122+
def __str__(self) -> str:
123+
ctx = ""
124+
if self.context:
125+
ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ")
126+
group = ""
127+
if self.group_elems:
128+
chunks = "\n ".join(str(x) for x in self.group_elems)
129+
group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ")
130+
return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"
131+
132+
@classmethod
133+
def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin":
117134
filename, lineno = None, None
118-
else:
119-
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
120-
return (
121-
type(exception),
122-
filename,
123-
lineno,
124-
# Note that if __cause__ is set it is always equal to __context__, explicitly
125-
# to support introspection when debugging, so we can use that unconditionally.
126-
get_interesting_origin(exception.__context__) if exception.__context__ else (),
127-
# We distinguish exception groups by the inner exceptions, as for __context__
128-
tuple(
129-
map(get_interesting_origin, exception.exceptions)
135+
if tb := get_trimmed_traceback(exception):
136+
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
137+
return cls(
138+
type(exception),
139+
filename,
140+
lineno,
141+
# Note that if __cause__ is set it is always equal to __context__, explicitly
142+
# to support introspection when debugging, so we can use that unconditionally.
143+
cls.from_exception(exception.__context__) if exception.__context__ else (),
144+
# We distinguish exception groups by the inner exceptions, as for __context__
145+
tuple(map(cls.from_exception, exception.exceptions))
130146
if isinstance(exception, BaseExceptionGroup)
131-
else []
132-
),
133-
)
147+
else (),
148+
)
134149

135150

136151
current_pytest_item = DynamicVariable(None)

hypothesis-python/tests/cover/test_escalation.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,22 @@ def test_errors_attribute_error():
7878

7979

8080
def test_handles_null_traceback():
81-
esc.get_interesting_origin(Exception())
81+
esc.InterestingOrigin.from_exception(Exception())
82+
83+
84+
def test_handles_context():
85+
e = ValueError()
86+
e.__context__ = KeyError()
87+
origin = esc.InterestingOrigin.from_exception(e)
88+
assert "ValueError at " in str(origin)
89+
assert " context: " in str(origin)
90+
assert "KeyError at " in str(origin)
91+
92+
93+
def test_handles_groups():
94+
origin = esc.InterestingOrigin.from_exception(
95+
BaseExceptionGroup("message", [ValueError("msg2")])
96+
)
97+
assert "ExceptionGroup at " in str(origin)
98+
assert "child exception" in str(origin)
99+
assert "ValueError at " in str(origin)

0 commit comments

Comments
 (0)