From b68123b6b1534643910c32bab236b386a197fdf7 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 23 Dec 2024 19:21:20 +1100 Subject: [PATCH] teach from_type to build with posonly args --- hypothesis-python/RELEASE.rst | 5 +++ .../hypothesis/strategies/_internal/core.py | 32 +++++++++++++++---- hypothesis-python/tests/cover/test_lookup.py | 13 ++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..1fa78ad520 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,5 @@ +RELEASE_TYPE: minor + +:func:`~hypothesis.strategies.from_type` can now handle constructors with +required positional-only arguments if they have type annotations. Previously, +we only passed arguments by keyword. diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py index 6133231ba6..96be9f676b 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py @@ -1447,17 +1447,35 @@ def _get_typeddict_qualifiers(key, annotation_type): params = get_signature(thing).parameters except Exception: params = {} # type: ignore + + posonly_args = [] kwargs = {} for k, p in params.items(): if ( - k in hints + p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY) + and k in hints and k != "return" - and p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) ): - kwargs[k] = from_type_guarded(hints[k]) - if p.default is not Parameter.empty and kwargs[k] is not ...: - kwargs[k] = just(p.default) | kwargs[k] - if params and not kwargs and not issubclass(thing, BaseException): + ps = from_type_guarded(hints[k]) + if p.default is not Parameter.empty and ps is not ...: + ps = just(p.default) | ps + if p.kind is Parameter.POSITIONAL_ONLY: + # builds() doesn't infer strategies for positional args, so: + if ps is ...: # pragma: no cover # rather fiddly to test + if p.default is Parameter.empty: + raise ResolutionFailed( + f"Could not resolve {thing!r} to a strategy; " + "consider using register_type_strategy" + ) + ps = just(p.default) + posonly_args.append(ps) + else: + kwargs[k] = ps + if ( + params + and not (posonly_args or kwargs) + and not issubclass(thing, BaseException) + ): from_type_repr = repr_call(from_type, (thing,), {}) builds_repr = repr_call(builds, (thing,), {}) warnings.warn( @@ -1468,7 +1486,7 @@ def _get_typeddict_qualifiers(key, annotation_type): SmallSearchSpaceWarning, stacklevel=2, ) - return builds(thing, **kwargs) + return builds(thing, *posonly_args, **kwargs) # And if it's an abstract type, we'll resolve to a union of subclasses instead. subclasses = thing.__subclasses__() if not subclasses: diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py index 9994c02ed3..23c18162e4 100644 --- a/hypothesis-python/tests/cover/test_lookup.py +++ b/hypothesis-python/tests/cover/test_lookup.py @@ -1224,3 +1224,16 @@ def resolve_custom_strategy_for_b(thing): assert_all_examples(st.from_type(A), lambda example: type(example) == C) assert_all_examples(st.from_type(B), lambda example: example is sentinel) assert_all_examples(st.from_type(C), lambda example: type(example) == C) + + +class CustomInteger(int): + def __init__(self, value: int, /) -> None: + if not isinstance(value, int): + raise TypeError + + +@given(...) +def test_from_type_resolves_required_posonly_args(n: CustomInteger): + # st.builds() does not infer for positional arguments, but st.from_type() + # does. See e.g. https://stackoverflow.com/q/79199376/ for motivation. + assert isinstance(n, CustomInteger)