Repository: HypothesisWorks/hypothesis
Pull request: WIP: Smarter return type for st.functions()
@Zac-HD I got started on making the changes. Just wanted to check, if I am making the code changes in the right direction. 

Disclaimer - A few test cases are failing. I will fix them.

(edit for magic comment: resolves #2425)

Problem statement:
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.

Hints from issue 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!

Commit message: Added release notes, and test to validate new changes

Patch to review:
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index c5b10ddda9..522aaa9bc3 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -185,6 +185,7 @@ their individual contributions.
 
 * `Adam Johnson <https://github.com/adamchainz>`_
 * `Adam Sven Johnson <https://www.github.com/pkqk>`_
+* `Akash Suresh <https://www.github.com/akash-suresh>`_ (akashsuresh36@gmail.com)
 * `Alex Gaynor <https://github.com/alex>`_
 * `Alex Stapleton <https://www.github.com/public>`_
 * `Alex Willmer <https://github.com/moreati>`_ (alex@moreati.org.uk)
diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst
new file mode 100644
index 0000000000..333af902e5
--- /dev/null
+++ b/hypothesis-python/RELEASE.rst
@@ -0,0 +1,5 @@
+RELEASE_TYPE: minor
+
+:func:`~hypothesis.strategies.functions` can now infer the appropriate ``returns``
+strategy if you pass a ``like`` function with a return-type annotation.  Before,
+omitting the ``returns`` argument would generate functions that always returned None.
\ No newline at end of file
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
index 90e0e4ddac..09119555b4 100644
--- 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)
 
 
diff --git a/hypothesis-python/tests/cover/test_functions.py b/hypothesis-python/tests/cover/test_functions.py
index d858221968..8ab21ba67b 100644
--- a/hypothesis-python/tests/cover/test_functions.py
+++ b/hypothesis-python/tests/cover/test_functions.py
@@ -75,12 +75,22 @@ 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 test_smart_returns():
+    def t() -> str:
+        return "string"
+
+    f = functions(like=t, returns=None).example()
+    assert f.__annotations__["return"] is str
+
+
 def test_functions_valid_within_given_invalid_outside():
     cache = [None]
 
@@ -99,7 +109,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):


Code review comments:
diff_hunk: @@ -75,12 +75,22 @@ 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 test_smart_returns():
+    def t() -> str:
+        return "string"
+
+    f = functions(like=t, returns=None).example()
+    assert f.__annotations__["return"] is str
path: hypothesis-python/tests/cover/test_functions.py
text: @user2:
```suggestion
def func_returns_str() -> str:
    return "a string"


@user1(functions(like=func_returns_str))
def test_functions_strategy_return_type_inference(f):
    result = f()
    assume(result != "a string")
    assert isinstance(result, str)
```

Last suggestion - this test is a bit more direct in checking the behaviour we care about, i.e. what the generated functions actually return.

Then time to merge!

@author:
Done 👍