diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..4c7dc4fd4a --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,19 @@ +RELEASE_TYPE: minor + +This release teaches :func:`~hypothesis.strategies.builds` to use +:func:`~hypothesis.strategies.deferred` when resolving unrecognised type hints, +so that you can conveniently register strategies for recursive types +with constraints on some arguments (:issue:`3026`): + +.. code-block:: python + + class RecursiveClass: + def __init__(self, value: int, next_node: typing.Optional["SomeClass"]): + assert value > 0 + self.value = value + self.next_node = next_node + + st.register_type_strategy( + RecursiveClass, + st.builds(RecursiveClass, value=st.integers(min_value=1)) + ) diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py index 4f64ff75fe..c48eea72f1 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py @@ -886,8 +886,21 @@ def builds( raise InvalidArgument( f"passed infer for {badargs}, but there is no type annotation" ) - for kw in set(hints) & (required | to_infer): - kwargs[kw] = from_type(hints[kw]) + infer_for = {k: v for k, v in hints.items() if k in (required | to_infer)} + if infer_for: + from hypothesis.strategies._internal.types import _global_type_lookup + + for kw, t in infer_for.items(): + if ( + getattr(t, "__module__", None) in ("builtins", "typing") + or t in _global_type_lookup + ): + kwargs[kw] = from_type(t) + else: + # We defer resolution of these type annotations so that the obvious + # approach to registering recursive types just works. See + # https://github.com/HypothesisWorks/hypothesis/issues/3026 + kwargs[kw] = deferred(lambda t=t: from_type(t)) return BuildsStrategy(target, args, kwargs) diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py index 7a03b612a2..c7248f8155 100644 --- a/hypothesis-python/tests/cover/test_lookup.py +++ b/hypothesis-python/tests/cover/test_lookup.py @@ -547,6 +547,24 @@ def test_resolving_recursive_type(): assert isinstance(st.builds(Tree).example(), Tree) +class SomeClass: + def __init__(self, value: int, next_node: typing.Optional["SomeClass"]) -> None: + assert value > 0 + self.value = value + self.next_node = next_node + + def __repr__(self) -> str: + return f"SomeClass({self.value}, next_node={self.next_node})" + + +def test_resolving_recursive_type_with_registered_constraint(): + with temp_registered( + SomeClass, st.builds(SomeClass, value=st.integers(min_value=1)) + ): + find_any(st.from_type(SomeClass), lambda s: s.next_node is None) + find_any(st.from_type(SomeClass), lambda s: s.next_node is not None) + + @given(from_type(typing.Tuple[()])) def test_resolves_empty_Tuple_issue_1583_regression(ex): # See e.g. https://github.com/python/mypy/commit/71332d58