Skip to content

Commit 2958a45

Browse files
committed
Fix for cyclic exception context
1 parent 7fdad0b commit 2958a45

File tree

3 files changed

+37
-6
lines changed

3 files changed

+37
-6
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
RELEASE_TYPE: patch
22

3-
This patch updates our vendored `list of top-level domains <https://www.iana.org/domains/root/db>`__,
4-
which is used by the provisional :func:`~hypothesis.provisional.domains` strategy.
3+
This patch fixes an internal error when the ``__context__``
4+
attribute of a raised exception leads to a cycle (:issue:`4115`).

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
import sys
1414
import textwrap
1515
import traceback
16+
from functools import partial
1617
from inspect import getframeinfo
1718
from pathlib import Path
18-
from typing import Dict, NamedTuple, Optional, Type
19+
from typing import Dict, NamedTuple, Optional, Tuple, Type
1920

2021
import hypothesis
2122
from hypothesis.errors import _Trimmable
@@ -107,20 +108,27 @@ def __str__(self) -> str:
107108
return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"
108109

109110
@classmethod
110-
def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin":
111+
def from_exception(
112+
cls, exception: BaseException, /, seen: Tuple[BaseException, ...] = ()
113+
) -> "InterestingOrigin":
111114
filename, lineno = None, None
112115
if tb := get_trimmed_traceback(exception):
113116
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
117+
seen = (*seen, exception)
118+
make = partial(cls.from_exception, seen=seen)
119+
context: "InterestingOrigin | tuple[()]" = ()
120+
if exception.__context__ is not None and exception.__context__ not in seen:
121+
context = make(exception.__context__)
114122
return cls(
115123
type(exception),
116124
filename,
117125
lineno,
118126
# Note that if __cause__ is set it is always equal to __context__, explicitly
119127
# to support introspection when debugging, so we can use that unconditionally.
120-
cls.from_exception(exception.__context__) if exception.__context__ else (),
128+
context,
121129
# We distinguish exception groups by the inner exceptions, as for __context__
122130
(
123-
tuple(map(cls.from_exception, exception.exceptions))
131+
tuple(make(exc) for exc in exception.exceptions if exc not in seen)
124132
if isinstance(exception, BaseExceptionGroup)
125133
else ()
126134
),

hypothesis-python/tests/cover/test_escalation.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,26 @@ def test_handles_groups():
7070
assert "ExceptionGroup at " in str(origin)
7171
assert "child exception" in str(origin)
7272
assert "ValueError at " in str(origin)
73+
74+
75+
def make_exceptions_with_cycles():
76+
err = ValueError()
77+
err.__context__ = err
78+
yield err
79+
80+
err = TypeError()
81+
err.__context__ = BaseExceptionGroup("msg", [err])
82+
yield err
83+
84+
inner = LookupError()
85+
err = BaseExceptionGroup("msg", [inner])
86+
inner.__context__ = err
87+
yield err
88+
89+
inner = OSError()
90+
yield BaseExceptionGroup("msg", [inner, inner, inner])
91+
92+
93+
@pytest.mark.parametrize("err", list(make_exceptions_with_cycles()))
94+
def test_handles_cycles(err):
95+
esc.InterestingOrigin.from_exception(err)

0 commit comments

Comments
 (0)