Skip to content

Commit

Permalink
Fix for cyclic exception context
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Sep 29, 2024
1 parent 7fdad0b commit 2958a45
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 6 deletions.
4 changes: 2 additions & 2 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
RELEASE_TYPE: patch

This patch updates our vendored `list of top-level domains <https://www.iana.org/domains/root/db>`__,
which is used by the provisional :func:`~hypothesis.provisional.domains` strategy.
This patch fixes an internal error when the ``__context__``
attribute of a raised exception leads to a cycle (:issue:`4115`).
16 changes: 12 additions & 4 deletions hypothesis-python/src/hypothesis/internal/escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@
import sys
import textwrap
import traceback
from functools import partial
from inspect import getframeinfo
from pathlib import Path
from typing import Dict, NamedTuple, Optional, Type
from typing import Dict, NamedTuple, Optional, Tuple, Type

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

@classmethod
def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin":
def from_exception(
cls, exception: BaseException, /, seen: Tuple[BaseException, ...] = ()
) -> "InterestingOrigin":
filename, lineno = None, None
if tb := get_trimmed_traceback(exception):
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
seen = (*seen, exception)
make = partial(cls.from_exception, seen=seen)
context: "InterestingOrigin | tuple[()]" = ()
if exception.__context__ is not None and exception.__context__ not in seen:
context = make(exception.__context__)
return cls(
type(exception),
filename,
lineno,
# Note that if __cause__ is set it is always equal to __context__, explicitly
# to support introspection when debugging, so we can use that unconditionally.
cls.from_exception(exception.__context__) if exception.__context__ else (),
context,
# We distinguish exception groups by the inner exceptions, as for __context__
(
tuple(map(cls.from_exception, exception.exceptions))
tuple(make(exc) for exc in exception.exceptions if exc not in seen)
if isinstance(exception, BaseExceptionGroup)
else ()
),
Expand Down
23 changes: 23 additions & 0 deletions hypothesis-python/tests/cover/test_escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,26 @@ def test_handles_groups():
assert "ExceptionGroup at " in str(origin)
assert "child exception" in str(origin)
assert "ValueError at " in str(origin)


def make_exceptions_with_cycles():
err = ValueError()
err.__context__ = err
yield err

err = TypeError()
err.__context__ = BaseExceptionGroup("msg", [err])
yield err

inner = LookupError()
err = BaseExceptionGroup("msg", [inner])
inner.__context__ = err
yield err

inner = OSError()
yield BaseExceptionGroup("msg", [inner, inner, inner])


@pytest.mark.parametrize("err", list(make_exceptions_with_cycles()))
def test_handles_cycles(err):
esc.InterestingOrigin.from_exception(err)

0 comments on commit 2958a45

Please sign in to comment.