Repository: HypothesisWorks/hypothesis

Issue:
Type annotations used for `functions` strategy
When you accept a function as a parameter to another function, if you're using `mypy` and `typing` you may well have that method described already using `Callable` etc. In this case, it's redundant to create a lambda function for the `like` attribute of `hypothesis.strategies.functions`. It would be nice to be able to reuse these objects in hypothesis.


Discussion:
It's not quite clear to me what you're asking for.  If you mean "`st.functions(like=Callable[[int, int], int])` should work", you're better off using `st.from_type()`.

If you mean "`st.functions()` ought to infer a strategy for `returns` from the return-type annotation on the `like` function", that sounds pretty good to me!  I'd be happy to help out if you'd like to open a PR: `hypothesis.internal.compat.get_type_hints(func).get("return", type(None))` and `st.from_type()` should get you started :smile: 
Hey @Zac-HD ! I would like to pick this up. But I would need some guidance on how to get this done. 😄
Sure!  I'll keep the tips a little vague, because figuring out how to do it is part of the fun.  Getting stuck isn't fun for long though, so if you're getting frustrated just push what you have up and ask me for more detailed advice.

1. Find the definition of the `functions()` strategy.
2. We're going to change the default value of the `returns` argument to `None` (instead of `st.none()`).
3. If a strategy is passed, same logic as now.
4. If `returns is None`, we want to get the return-type annotation of the `like` argument using our `get_type_hints` function.
5. Use `st.from_type()` to get the strategy, defaulting to `st.none()` if there is no return type hint (again, the current behaviour)
6. Write some tests in `tests/cover/test_functions.py` to check that it's working.
7. Write release notes in a new file `hypothesis-python/RELEASE.rst` for a minor release.  Check some of the other PRs for examples of this.
8. Add your name to the list of contributors in `CONTRIBUTING.rst`

and that's it!

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py
--- a/hypothesis-python/src/hypothesis/internal/compat.py
+++ b/hypothesis-python/src/hypothesis/internal/compat.py
@@ -108,13 +108,18 @@ def get_type_hints(thing):
             thing = thing.__init__  # type: ignore
         try:
             spec = inspect.getfullargspec(thing)
-            return {
+            hints = {
                 k: v
                 for k, v in spec.annotations.items()
                 if k in (spec.args + spec.kwonlyargs) and isinstance(v, type)
             }
         except TypeError:
-            return {}
+            hints = {}
+        try:
+            hints["return"] = typing.get_type_hints(thing)["return"]
+        except Exception:
+            pass
+        return hints
 
 
 else:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -2109,7 +2109,7 @@ def emails() -> SearchStrategy[str]:
 @defines_strategy
 @deprecated_posargs
 def functions(
-    *, like: Callable[..., Any] = lambda: None, returns: SearchStrategy[Any] = none()
+    *, like: Callable[..., Any] = lambda: None, returns: SearchStrategy[Any] = None
 ) -> SearchStrategy[Callable[..., 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
@@ -2128,12 +2128,17 @@ def functions(
     Generated functions can only be called within the scope of the ``@given``
     which created them.  This strategy does not support ``.example()``.
     """
-    check_type(SearchStrategy, returns)
     if not callable(like):
         raise InvalidArgument(
             "The first argument to functions() must be a callable to imitate, "
             "but got non-callable like=%r" % (nicerepr(like),)
         )
+
+    if returns is None:
+        hints = get_type_hints(like)
+        returns = from_type(hints.get("return", type(None)))
+
+    check_type(SearchStrategy, returns)
     return FunctionStrategy(like, returns)
 
 


Test patch:
diff --git a/hypothesis-python/tests/cover/test_functions.py b/hypothesis-python/tests/cover/test_functions.py
--- a/hypothesis-python/tests/cover/test_functions.py
+++ b/hypothesis-python/tests/cover/test_functions.py
@@ -17,7 +17,7 @@
 
 import pytest
 
-from hypothesis import given
+from hypothesis import assume, given
 from hypothesis.errors import InvalidArgument, InvalidState
 from hypothesis.strategies import booleans, functions
 
@@ -75,12 +75,25 @@ def test_functions_lambda_with_arg(f):
     assert isinstance(f(1), bool)
 
 
-@pytest.mark.parametrize("like,returns", [(None, booleans()), (lambda: None, None)])
+@pytest.mark.parametrize(
+    "like,returns", [(None, booleans()), (lambda: None, "not a strategy")]
+)
 def test_invalid_arguments(like, returns):
     with pytest.raises(InvalidArgument):
         functions(like=like, returns=returns).example()
 
 
+def func_returns_str() -> str:
+    return "a string"
+
+
+@given(functions(like=func_returns_str))
+def test_functions_strategy_return_type_inference(f):
+    result = f()
+    assume(result != "a string")
+    assert isinstance(result, str)
+
+
 def test_functions_valid_within_given_invalid_outside():
     cache = [None]
 
@@ -99,7 +112,7 @@ def test_can_call_default_like_arg():
     # branch for calling it otherwise and alternative workarounds are worse.
     defaults = getfullargspec(functions).kwonlydefaults
     assert defaults["like"]() is None
-    assert defaults["returns"].example() is None
+    assert defaults["returns"] is None
 
 
 def func(arg, *, kwonly_arg):