Skip to content

Commit 91f7722

Browse files
committed
Distinguish exceptiongroup contents
1 parent 822235e commit 91f7722

File tree

4 files changed

+38
-13
lines changed

4 files changed

+38
-13
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RELEASE_TYPE: minor
2+
3+
When distinguishing multiple errors, Hypothesis now looks at the inner
4+
exceptions of :pep:`654` ``ExceptionGroup``\ s.

hypothesis-python/src/hypothesis/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@
7474
from hypothesis.internal.conjecture.shrinker import sort_key
7575
from hypothesis.internal.entropy import deterministic_PRNG
7676
from hypothesis.internal.escalation import (
77+
InterestingOrigin,
7778
escalate_hypothesis_internal_error,
7879
format_exception,
79-
get_interesting_origin,
8080
get_trimmed_traceback,
8181
)
8282
from hypothesis.internal.healthcheck import fail_health_check
@@ -742,7 +742,7 @@ def _execute_once_for_engine(self, data):
742742

743743
self.failed_normally = True
744744

745-
interesting_origin = get_interesting_origin(e)
745+
interesting_origin = InterestingOrigin.from_exception(e)
746746
if trace: # pragma: no cover
747747
# Trace collection is explicitly disabled under coverage.
748748
self.explain_traces[interesting_origin].add(trace)

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
import sys
1515
import typing
1616

17+
try:
18+
BaseExceptionGroup = BaseExceptionGroup
19+
except NameError: # pragma: no cover
20+
try:
21+
from exceptiongroup import BaseExceptionGroup
22+
except ImportError:
23+
BaseExceptionGroup = () # valid in isinstance and except clauses!
24+
1725
PYPY = platform.python_implementation() == "PyPy"
1826
WINDOWS = platform.system() == "Windows"
1927

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

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import traceback
1515
from inspect import getframeinfo
1616
from pathlib import Path
17-
from typing import Dict
17+
from typing import Dict, NamedTuple, Optional, Tuple, Type
1818

1919
import hypothesis
2020
from hypothesis.errors import (
@@ -24,6 +24,7 @@
2424
UnsatisfiedAssumption,
2525
_Trimmable,
2626
)
27+
from hypothesis.internal.compat import BaseExceptionGroup
2728
from hypothesis.utils.dynamicvariables import DynamicVariable
2829

2930

@@ -102,23 +103,35 @@ def get_trimmed_traceback(exception=None):
102103
return tb
103104

104105

105-
def get_interesting_origin(exception):
106+
class InterestingOrigin(NamedTuple):
106107
# The `interesting_origin` is how Hypothesis distinguishes between multiple
107108
# failures, for reporting and also to replay from the example database (even
108109
# if report_multiple_bugs=False). We traditionally use the exception type and
109110
# location, but have extracted this logic in order to see through `except ...:`
110111
# blocks and understand the __cause__ (`raise x from y`) or __context__ that
111-
# first raised an exception.
112-
tb = get_trimmed_traceback(exception)
113-
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
114-
return (
115-
type(exception),
116-
filename,
117-
lineno,
112+
# first raised an exception as well as PEP-654 exception groups.
113+
type_: Type[BaseException]
114+
filename: str
115+
lineno: int
116+
context: "Optional[InterestingOrigin]"
117+
exceptiongroup_contents: "Optional[Tuple[InterestingOrigin, ...]]"
118+
119+
@classmethod
120+
def from_exception(cls, exception: BaseException) -> "InterestingOrigin":
121+
tb = get_trimmed_traceback(exception)
122+
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
118123
# Note that if __cause__ is set it is always equal to __context__, explicitly
119124
# to support introspection when debugging, so we can use that unconditionally.
120-
get_interesting_origin(exception.__context__) if exception.__context__ else (),
121-
)
125+
chained_from = exception.__context__
126+
return cls(
127+
type(exception),
128+
filename,
129+
lineno,
130+
cls.from_exception(chained_from) if chained_from else None,
131+
tuple(map(cls.from_exception, exception.exceptions))
132+
if isinstance(exception, BaseExceptionGroup)
133+
else None,
134+
)
122135

123136

124137
current_pytest_item = DynamicVariable(None)

0 commit comments

Comments
 (0)