|
11 | 11 | import contextlib |
12 | 12 | import os |
13 | 13 | import sys |
| 14 | +import textwrap |
14 | 15 | import traceback |
15 | 16 | from inspect import getframeinfo |
16 | 17 | from pathlib import Path |
17 | | -from typing import Dict |
| 18 | +from typing import Dict, NamedTuple, Optional, Type |
18 | 19 |
|
19 | 20 | import hypothesis |
20 | 21 | from hypothesis.errors import ( |
@@ -105,32 +106,46 @@ def get_trimmed_traceback(exception=None): |
105 | 106 | return tb |
106 | 107 |
|
107 | 108 |
|
108 | | -def get_interesting_origin(exception): |
| 109 | +class InterestingOrigin(NamedTuple): |
109 | 110 | # The `interesting_origin` is how Hypothesis distinguishes between multiple |
110 | 111 | # failures, for reporting and also to replay from the example database (even |
111 | 112 | # if report_multiple_bugs=False). We traditionally use the exception type and |
112 | 113 | # location, but have extracted this logic in order to see through `except ...:` |
113 | 114 | # blocks and understand the __cause__ (`raise x from y`) or __context__ that |
114 | 115 | # 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: Optional[str] |
| 118 | + lineno: Optional[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": |
117 | 134 | 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)) |
130 | 146 | if isinstance(exception, BaseExceptionGroup) |
131 | | - else [] |
132 | | - ), |
133 | | - ) |
| 147 | + else (), |
| 148 | + ) |
134 | 149 |
|
135 | 150 |
|
136 | 151 | current_pytest_item = DynamicVariable(None) |
|
0 commit comments