diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..590b76ffda --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,5 @@ +RELEASE_TYPE: minor + +This preserves the type annotations of functions passed to :func:`hypothesis.strategies.composite` and :func:`hypothesis.strategies.functions` by using :obj:`python:typing.ParamSpec`. + +This improves the ability of static type-checkers to check test code that uses Hypothesis, and improves auto-completion in IDEs. \ No newline at end of file diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index 6c3f838d5b..0dd11e3042 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -951,9 +951,7 @@ def fuzz_one_input( def given( *_given_arguments: Union[SearchStrategy, InferType], **_given_kwargs: Union[SearchStrategy, InferType], -) -> Callable[ - [Callable[..., Union[None, Coroutine[Any, Any, None]]]], Callable[..., None] -]: +) -> Callable[[Callable[..., Union[None, Coroutine[Any, Any, None]]]], Callable[..., None]]: """A decorator for turning a test function that accepts arguments into a randomized test. diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py index 7530ec62d7..4e2225fdef 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py @@ -43,6 +43,7 @@ from uuid import UUID import attr +from typing_extensions import Concatenate from hypothesis.control import cleanup, note from hypothesis.errors import InvalidArgument, ResolutionFailed @@ -94,6 +95,7 @@ from hypothesis.strategies._internal.shared import SharedStrategy from hypothesis.strategies._internal.strategies import ( Ex, + P, SampledFromStrategy, T, one_of, @@ -1451,7 +1453,9 @@ def __call__(self, strategy: SearchStrategy[Ex], label: object = None) -> Ex: @cacheable -def composite(f: Callable[..., Ex]) -> Callable[..., SearchStrategy[Ex]]: +def composite( + f: Callable[Concatenate[DrawFn, P], Ex] +) -> Callable[P, SearchStrategy[Ex]]: """Defines a strategy that is built out of potentially arbitrarily many other strategies. @@ -1850,10 +1854,10 @@ def emails() -> SearchStrategy[str]: @defines_strategy() def functions( *, - like: Callable[..., Any] = lambda: None, + like: Callable[P, Any] = lambda: None, returns: Optional[SearchStrategy[Any]] = None, pure: bool = False, -) -> SearchStrategy[Callable[..., Any]]: +) -> SearchStrategy[Callable[P, Any]]: # The proper type signature of `functions()` would have T instead of Any, but mypy # disallows default args for generics: https://github.com/python/mypy/issues/3737 """functions(*, like=lambda: None, returns=none(), pure=False) diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py index 680e6b58ce..c3a50ef1fe 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py @@ -24,6 +24,8 @@ overload, ) +from typing_extensions import ParamSpec + from hypothesis._settings import HealthCheck, Phase, Verbosity, settings from hypothesis.control import _current_build_context, assume from hypothesis.errors import ( @@ -54,6 +56,7 @@ T3 = TypeVar("T3") T4 = TypeVar("T4") T5 = TypeVar("T5") +P = ParamSpec("P") calculating = UniqueIdentifier("calculating") diff --git a/whole-repo-tests/test_type_hints.py b/whole-repo-tests/test_type_hints.py index fedf21ad71..7fd318cb77 100644 --- a/whole-repo-tests/test_type_hints.py +++ b/whole-repo-tests/test_type_hints.py @@ -142,6 +142,32 @@ def test_drawfn_type_tracing(tmpdir): assert got == "str" +def test_composite_type_tracing(tmpdir): + f = tmpdir.join("check_mypy_on_st_composite.py") + f.write( + "from hypothesis.strategies import composite, DrawFn\n" + "@composite\n" + "def comp(draw: DrawFn, x: int) -> int:\n" + " return x\n" + "reveal_type(comp)\n" + ) + got = get_mypy_analysed_type(str(f.realpath()), ...) + assert got == "def (x: int) -> int" + + +def test_functions_type_tracing(tmpdir): + f = tmpdir.join("check_mypy_on_st_functions.py") + f.write( + "from hypothesis.strategies import functions\n" + "def like(x: int, y: str) -> str:\n" + " return str(x) + y\n" + "st = functions(like)\n" + "reveal_type(st)\n" + ) + got = get_mypy_analysed_type(str(f.realpath()), ...) + assert got == "SearchStrategy[Callable[[int, str], str]]" + + def test_settings_preserves_type(tmpdir): f = tmpdir.join("check_mypy_on_settings.py") f.write(