Skip to content

Commit 60d070e

Browse files
authored
Merge pull request #4211 from Zac-HD/from-type-posonly
Teach from_type to build with posonly args
2 parents 218897e + b68123b commit 60d070e

File tree

3 files changed

+43
-7
lines changed

3 files changed

+43
-7
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RELEASE_TYPE: minor
2+
3+
:func:`~hypothesis.strategies.from_type` can now handle constructors with
4+
required positional-only arguments if they have type annotations. Previously,
5+
we only passed arguments by keyword.

hypothesis-python/src/hypothesis/strategies/_internal/core.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,17 +1447,35 @@ def _get_typeddict_qualifiers(key, annotation_type):
14471447
params = get_signature(thing).parameters
14481448
except Exception:
14491449
params = {} # type: ignore
1450+
1451+
posonly_args = []
14501452
kwargs = {}
14511453
for k, p in params.items():
14521454
if (
1453-
k in hints
1455+
p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD, p.KEYWORD_ONLY)
1456+
and k in hints
14541457
and k != "return"
1455-
and p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
14561458
):
1457-
kwargs[k] = from_type_guarded(hints[k])
1458-
if p.default is not Parameter.empty and kwargs[k] is not ...:
1459-
kwargs[k] = just(p.default) | kwargs[k]
1460-
if params and not kwargs and not issubclass(thing, BaseException):
1459+
ps = from_type_guarded(hints[k])
1460+
if p.default is not Parameter.empty and ps is not ...:
1461+
ps = just(p.default) | ps
1462+
if p.kind is Parameter.POSITIONAL_ONLY:
1463+
# builds() doesn't infer strategies for positional args, so:
1464+
if ps is ...: # pragma: no cover # rather fiddly to test
1465+
if p.default is Parameter.empty:
1466+
raise ResolutionFailed(
1467+
f"Could not resolve {thing!r} to a strategy; "
1468+
"consider using register_type_strategy"
1469+
)
1470+
ps = just(p.default)
1471+
posonly_args.append(ps)
1472+
else:
1473+
kwargs[k] = ps
1474+
if (
1475+
params
1476+
and not (posonly_args or kwargs)
1477+
and not issubclass(thing, BaseException)
1478+
):
14611479
from_type_repr = repr_call(from_type, (thing,), {})
14621480
builds_repr = repr_call(builds, (thing,), {})
14631481
warnings.warn(
@@ -1468,7 +1486,7 @@ def _get_typeddict_qualifiers(key, annotation_type):
14681486
SmallSearchSpaceWarning,
14691487
stacklevel=2,
14701488
)
1471-
return builds(thing, **kwargs)
1489+
return builds(thing, *posonly_args, **kwargs)
14721490
# And if it's an abstract type, we'll resolve to a union of subclasses instead.
14731491
subclasses = thing.__subclasses__()
14741492
if not subclasses:

hypothesis-python/tests/cover/test_lookup.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,3 +1224,16 @@ def resolve_custom_strategy_for_b(thing):
12241224
assert_all_examples(st.from_type(A), lambda example: type(example) == C)
12251225
assert_all_examples(st.from_type(B), lambda example: example is sentinel)
12261226
assert_all_examples(st.from_type(C), lambda example: type(example) == C)
1227+
1228+
1229+
class CustomInteger(int):
1230+
def __init__(self, value: int, /) -> None:
1231+
if not isinstance(value, int):
1232+
raise TypeError
1233+
1234+
1235+
@given(...)
1236+
def test_from_type_resolves_required_posonly_args(n: CustomInteger):
1237+
# st.builds() does not infer for positional arguments, but st.from_type()
1238+
# does. See e.g. https://stackoverflow.com/q/79199376/ for motivation.
1239+
assert isinstance(n, CustomInteger)

0 commit comments

Comments
 (0)