Skip to content

Commit

Permalink
Merge pull request #4211 from Zac-HD/from-type-posonly
Browse files Browse the repository at this point in the history
Teach from_type to build with posonly args
  • Loading branch information
Zac-HD authored Dec 23, 2024
2 parents 218897e + b68123b commit 60d070e
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 7 deletions.
5 changes: 5 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 25 additions & 7 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions hypothesis-python/tests/cover/test_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 60d070e

Please sign in to comment.