Skip to content

Commit 4e0f392

Browse files
authored
Merge pull request #3247 from HypothesisWorks/enable-ellipses
2 parents f2a3966 + 12073e7 commit 4e0f392

20 files changed

+198
-96
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
RELEASE_TYPE: minor
2+
3+
This release changes the implementation of :const:`~hypothesis.infer` to be an alias
4+
for :obj:`python:Ellipsis`. E.g. ``@given(a=infer)`` is now equivalent to ``@given(a=...)``. Furthermore, ``@given(...)`` can now be specified so that
5+
:func:`@given <hypothesis.given>` will infer the strategies for *all* arguments of the
6+
decorated function based on its annotations.

hypothesis-python/docs/details.rst

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ limits.
561561
If there are required arguments with type annotations and
562562
no strategy was passed to :func:`~hypothesis.strategies.builds`,
563563
:func:`~hypothesis.strategies.from_type` is used to fill them in.
564-
You can also pass the special value :const:`hypothesis.infer` as a keyword
564+
You can also pass the value ``...`` (``Ellipsis``) as a keyword
565565
argument, to force this inference for arguments with a default value.
566566

567567
.. code-block:: pycon
@@ -576,21 +576,38 @@ argument, to force this inference for arguments with a default value.
576576

577577
:func:`@given <hypothesis.given>` does not perform any implicit inference
578578
for required arguments, as this would break compatibility with pytest fixtures.
579-
:const:`~hypothesis.infer` can be used as a keyword argument to explicitly
580-
fill in an argument from its type annotation.
579+
``...`` (:obj:`python:Ellipsis`), can be used as a keyword argument to explicitly fill
580+
in an argument from its type annotation. You can also use the ``hypothesis.infer``
581+
alias if writing a literal ``...`` seems too weird.
581582

582583
.. code:: python
583584
584-
@given(a=infer)
585+
@given(a=...) # or @given(a=infer)
585586
def test(a: int):
586587
pass
587588
588589
589590
# is equivalent to
590-
@given(a=integers())
591+
@given(a=from_type(int))
591592
def test(a):
592593
pass
593594
595+
596+
``@given(...)`` can also be specified to fill all arguments from their type annotations.
597+
598+
.. code:: python
599+
600+
@given(...)
601+
def test(a: int, b: str):
602+
pass
603+
604+
605+
# is equivalent to
606+
@given(a=..., b=...)
607+
def test(a, b):
608+
pass
609+
610+
594611
~~~~~~~~~~~
595612
Limitations
596613
~~~~~~~~~~~

hypothesis-python/src/hypothesis/core.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import zlib
2323
from collections import defaultdict
2424
from io import StringIO
25+
from itertools import chain
2526
from random import Random
2627
from typing import (
2728
Any,
@@ -106,10 +107,17 @@
106107
MappedSearchStrategy,
107108
SearchStrategy,
108109
)
109-
from hypothesis.utils.conventions import InferType, infer
110+
from hypothesis.utils.conventions import infer
110111
from hypothesis.vendor.pretty import RepresentationPrinter
111112
from hypothesis.version import __version__
112113

114+
if sys.version_info >= (3, 10): # pragma: no cover
115+
from types import EllipsisType as InferType
116+
117+
else:
118+
InferType = type(Ellipsis)
119+
120+
113121
TestFunc = TypeVar("TestFunc", bound=Callable)
114122

115123

@@ -275,8 +283,9 @@ def wrapped_test(*arguments, **kwargs):
275283

276284
if infer in given_arguments:
277285
return invalid(
278-
"infer was passed as a positional argument to @given, "
279-
"but may only be passed as a keyword argument"
286+
"... was passed as a positional argument to @given, "
287+
"but may only be passed as a keyword argument or as "
288+
"the sole argument of @given"
280289
)
281290

282291
if given_arguments and given_kwargs:
@@ -983,6 +992,14 @@ def run_test_as_given(test):
983992

984993
original_argspec = getfullargspec(test)
985994

995+
if given_arguments == (Ellipsis,) and not given_kwargs:
996+
# user indicated that they want to infer all arguments
997+
given_kwargs.update(
998+
(name, Ellipsis)
999+
for name in chain(original_argspec.args, original_argspec.kwonlyargs)
1000+
)
1001+
given_arguments = ()
1002+
9861003
check_invalid = is_invalid_test(
9871004
test, original_argspec, given_arguments, given_kwargs
9881005
)
@@ -1016,7 +1033,7 @@ def run_test_as_given(test):
10161033
def wrapped_test(*arguments, **kwargs):
10171034
__tracebackhide__ = True
10181035
raise InvalidArgument(
1019-
f"passed {name}=infer for {test.__name__}, but {name} has "
1036+
f"passed {name}=... for {test.__name__}, but {name} has "
10201037
"no type annotation"
10211038
)
10221039

hypothesis-python/src/hypothesis/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class ResolutionFailed(InvalidArgument):
7979
8080
Type inference is best-effort, so this only happens when an
8181
annotation exists but could not be resolved for a required argument
82-
to the target of ``builds()``, or where the user passed ``infer``.
82+
to the target of ``builds()``, or where the user passed ``...``.
8383
"""
8484

8585

hypothesis-python/src/hypothesis/extra/django/_fields.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ def from_field(field: F) -> st.SearchStrategy[Union[F, None]]:
290290
291291
This function is used by :func:`~hypothesis.extra.django.from_form` and
292292
:func:`~hypothesis.extra.django.from_model` for any fields that require
293-
a value, or for which you passed :obj:`hypothesis.infer`.
293+
a value, or for which you passed ``...`` (:obj:`python:Ellipsis`) to infer
294+
a strategy from an annotation.
294295
295296
It's pretty similar to the core :func:`~hypothesis.strategies.from_type`
296297
function, with a subtle but important difference: ``from_field`` takes a

hypothesis-python/src/hypothesis/extra/django/_impl.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,13 @@
2424
from hypothesis.extra.django._fields import from_field
2525
from hypothesis.internal.reflection import define_function_signature_from_signature
2626
from hypothesis.strategies._internal.utils import defines_strategy
27-
from hypothesis.utils.conventions import InferType, infer
27+
from hypothesis.utils.conventions import infer
28+
29+
if sys.version_info >= (3, 10): # pragma: no cover
30+
from types import EllipsisType as InferType
31+
32+
else:
33+
InferType = type(Ellipsis)
2834

2935

3036
class HypothesisTestCase:
@@ -84,7 +90,7 @@ def from_model(
8490
shop_strategy = from_model(Shop, company=from_model(Company))
8591
8692
Like for :func:`~hypothesis.strategies.builds`, you can pass
87-
:obj:`~hypothesis.infer` as a keyword argument to infer a strategy for
93+
``...`` (:obj:`python:Ellipsis`) as a keyword argument to infer a strategy for
8894
a field which has a default value instead of using the default.
8995
"""
9096
if len(model) == 1:
@@ -171,7 +177,7 @@ def from_form(
171177
shop_strategy = from_form(Shop, form_kwargs={"company_id": 5})
172178
173179
Like for :func:`~hypothesis.strategies.builds`, you can pass
174-
:obj:`~hypothesis.infer` as a keyword argument to infer a strategy for
180+
``...`` (:obj:`python:Ellipsis`) as a keyword argument to infer a strategy for
175181
a field which has a default value instead of using the default.
176182
"""
177183
# currently unsupported:

hypothesis-python/src/hypothesis/extra/ghostwriter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,14 @@
112112
SampledFromStrategy,
113113
)
114114
from hypothesis.strategies._internal.types import _global_type_lookup, is_generic_type
115-
from hypothesis.utils.conventions import InferType, infer
115+
from hypothesis.utils.conventions import infer
116+
117+
if sys.version_info >= (3, 10): # pragma: no cover
118+
from types import EllipsisType as InferType
119+
120+
else:
121+
InferType = type(Ellipsis)
122+
116123

117124
IMPORT_SECTION = """
118125
# This test code was written by the `hypothesis.extra.ghostwriter` module

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def from_attrs(target, args, kwargs, to_infer):
3636
def from_attrs_attribute(attrib, target):
3737
"""Infer a strategy from the metadata on an attr.Attribute object."""
3838
# Try inferring from the default argument. Note that this will only help if
39-
# the user passed `infer` to builds() for this attribute, but in that case
39+
# the user passed `...` to builds() for this attribute, but in that case
4040
# we use it as the minimal example.
4141
default = st.nothing()
4242
if isinstance(attrib.default, attr.Factory):

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,14 @@
104104
TextStrategy,
105105
)
106106
from hypothesis.strategies._internal.utils import cacheable, defines_strategy
107-
from hypothesis.utils.conventions import InferType, infer, not_set
107+
from hypothesis.utils.conventions import infer, not_set
108+
109+
if sys.version_info >= (3, 10): # pragma: no cover
110+
from types import EllipsisType as InferType
111+
112+
else:
113+
InferType = type(Ellipsis)
114+
108115

109116
try:
110117
from typing import Protocol
@@ -841,9 +848,9 @@ def builds(
841848
842849
If the callable has type annotations, they will be used to infer a strategy
843850
for required arguments that were not passed to builds. You can also tell
844-
builds to infer a strategy for an optional argument by passing the special
845-
value :const:`hypothesis.infer` as a keyword argument to
846-
builds, instead of a strategy for that argument to the callable.
851+
builds to infer a strategy for an optional argument by passing ``...``
852+
(:obj:`python:Ellipsis`) as a keyword argument to builds, instead of a strategy for
853+
that argument to the callable.
847854
848855
If the callable is a class defined with :pypi:`attrs`, missing required
849856
arguments will be inferred from the attribute on a best-effort basis,
@@ -868,7 +875,7 @@ def builds(
868875
if infer in args:
869876
# Avoid an implementation nightmare juggling tuples and worse things
870877
raise InvalidArgument(
871-
"infer was passed as a positional argument to "
878+
"... was passed as a positional argument to "
872879
"builds(), but is only allowed as a keyword arg"
873880
)
874881
required = required_args(target, args, kwargs)
@@ -884,7 +891,8 @@ def builds(
884891
if to_infer - set(hints):
885892
badargs = ", ".join(sorted(to_infer - set(hints)))
886893
raise InvalidArgument(
887-
f"passed infer for {badargs}, but there is no type annotation"
894+
f"passed ... for {badargs}, but we cannot infer a strategy "
895+
"because these arguments have no type annotation"
888896
)
889897
infer_for = {k: v for k, v in hints.items() if k in (required | to_infer)}
890898
if infer_for:
@@ -1864,7 +1872,7 @@ def functions(
18641872
) -> SearchStrategy[Callable[..., Any]]:
18651873
# The proper type signature of `functions()` would have T instead of Any, but mypy
18661874
# disallows default args for generics: https://github.com/python/mypy/issues/3737
1867-
"""functions(*, like=lambda: None, returns=infer, pure=False)
1875+
"""functions(*, like=lambda: None, returns=..., pure=False)
18681876
18691877
A strategy for functions, which can be used in callbacks.
18701878

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
import zoneinfo
3030
except ImportError:
3131
try:
32-
from backports import zoneinfo
32+
from backports import zoneinfo # type: ignore
3333
except ImportError:
3434
# We raise an error recommending `pip install hypothesis[zoneinfo]`
3535
# when timezones() or timezone_keys() strategies are actually used.
36-
zoneinfo = None
36+
zoneinfo = None # type: ignore
3737

3838
DATENAMES = ("year", "month", "day")
3939
TIMENAMES = ("hour", "minute", "second", "microsecond")

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@
3939
from hypothesis.strategies._internal.lazy import unwrap_strategies
4040
from hypothesis.strategies._internal.strategies import OneOfStrategy
4141

42+
UnionType: typing.Any
4243
try:
4344
# The type of PEP-604 unions (`int | str`), added in Python 3.10
44-
from types import UnionType # type: ignore
45+
from types import UnionType
4546
except ImportError:
4647
UnionType = ()
4748

@@ -65,7 +66,7 @@
6566

6667
TypeAliasTypes: tuple = ()
6768
try:
68-
TypeAliasTypes += (typing.TypeAlias,) # type: ignore
69+
TypeAliasTypes += (typing.TypeAlias,)
6970
except AttributeError:
7071
pass # Is missing for `python<3.10`
7172
try:
@@ -81,7 +82,7 @@
8182

8283
FinalTypes: tuple = ()
8384
try:
84-
FinalTypes += (typing.Final,) # type: ignore
85+
FinalTypes += (typing.Final,)
8586
except AttributeError: # pragma: no cover
8687
pass # Is missing for `python<3.8`
8788
try:
@@ -590,7 +591,7 @@ def _networks(bits):
590591
}
591592
)
592593
if hasattr(typing, "SupportsIndex"): # pragma: no branch # new in Python 3.8
593-
_global_type_lookup[typing.SupportsIndex] = st.integers() | st.booleans() # type: ignore
594+
_global_type_lookup[typing.SupportsIndex] = st.integers() | st.booleans()
594595

595596

596597
def register(type_, fallback=None, *, module=typing):

hypothesis-python/src/hypothesis/utils/conventions.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,5 @@ def __repr__(self):
1919
return self.identifier
2020

2121

22-
class InferType(UniqueIdentifier):
23-
"""We have a subclass for `infer` so we can type-hint public APIs."""
24-
25-
26-
infer = InferType("infer")
22+
infer = ...
2723
not_set = UniqueIdentifier("not_set")

hypothesis-python/tests/cover/test_attrs_inference.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import attr
1414
import pytest
1515

16-
from hypothesis import given, infer, strategies as st
16+
from hypothesis import given, strategies as st
1717
from hypothesis.errors import ResolutionFailed
1818

1919

@@ -70,7 +70,7 @@ class UnhelpfulConverter:
7070
a = attr.ib(converter=lambda x: x)
7171

7272

73-
@given(st.builds(Inferrables, has_default=infer, has_default_factory=infer))
73+
@given(st.builds(Inferrables, has_default=..., has_default_factory=...))
7474
def test_attrs_inference_builds(c):
7575
pass
7676

@@ -88,4 +88,4 @@ def test_cannot_infer(c):
8888

8989
def test_cannot_infer_takes_self():
9090
with pytest.raises(ResolutionFailed):
91-
st.builds(Inferrables, has_default_factory_takes_self=infer).example()
91+
st.builds(Inferrables, has_default_factory_takes_self=...).example()

hypothesis-python/tests/cover/test_functions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import pytest
1414

15-
from hypothesis import assume, given, infer
15+
from hypothesis import assume, given
1616
from hypothesis.errors import InvalidArgument, InvalidState
1717
from hypothesis.strategies import booleans, functions, integers
1818

@@ -112,7 +112,7 @@ def test_can_call_default_like_arg():
112112
# branch for calling it otherwise and alternative workarounds are worse.
113113
defaults = getfullargspec(functions).kwonlydefaults
114114
assert defaults["like"]() is None
115-
assert defaults["returns"] is infer
115+
assert defaults["returns"] is ...
116116

117117

118118
def func(arg, *, kwonly_arg):

0 commit comments

Comments
 (0)