|
14 | 14 | import traceback |
15 | 15 | from inspect import getframeinfo |
16 | 16 | from pathlib import Path |
17 | | -from typing import Dict |
| 17 | +from typing import Dict, NamedTuple, Optional, Tuple, Type |
18 | 18 |
|
19 | 19 | import hypothesis |
20 | 20 | from hypothesis.errors import ( |
|
24 | 24 | UnsatisfiedAssumption, |
25 | 25 | _Trimmable, |
26 | 26 | ) |
| 27 | +from hypothesis.internal.compat import BaseExceptionGroup |
27 | 28 | from hypothesis.utils.dynamicvariables import DynamicVariable |
28 | 29 |
|
29 | 30 |
|
@@ -102,23 +103,35 @@ def get_trimmed_traceback(exception=None): |
102 | 103 | return tb |
103 | 104 |
|
104 | 105 |
|
105 | | -def get_interesting_origin(exception): |
| 106 | +class InterestingOrigin(NamedTuple): |
106 | 107 | # The `interesting_origin` is how Hypothesis distinguishes between multiple |
107 | 108 | # failures, for reporting and also to replay from the example database (even |
108 | 109 | # if report_multiple_bugs=False). We traditionally use the exception type and |
109 | 110 | # location, but have extracted this logic in order to see through `except ...:` |
110 | 111 | # 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] |
118 | 123 | # Note that if __cause__ is set it is always equal to __context__, explicitly |
119 | 124 | # 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 | + ) |
122 | 135 |
|
123 | 136 |
|
124 | 137 | current_pytest_item = DynamicVariable(None) |
|
0 commit comments