Repository: HypothesisWorks/hypothesis

Issue:
`st.builds()` works incorrectly with annotated `__init__`
The simplest reproduction is:

```python
from typing import Generic, TypeVar

from hypothesis import given
from hypothesis import strategies as st

_ValueType = TypeVar('_ValueType')


class Wrapper(Generic[_ValueType]):
    _inner_value: _ValueType

    def __init__(self, inner_value: _ValueType) -> None:
        self._inner_value = inner_value


@given(st.builds(Wrapper))
def test_wrapper(wrapper):
    assert isinstance(wrapper, Wrapper)

```

Outputs:

```
====================================== FAILURES ======================================
____________________________________ test_wrapper ____________________________________

    @given(st.builds(Wrapper))
>   def test_wrapper(wrapper):

ex2.py:24: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.7/site-packages/hypothesis/core.py:684: in _execute_once_for_engine
    escalate_hypothesis_internal_error()
.venv/lib/python3.7/site-packages/hypothesis/core.py:655: in _execute_once_for_engine
    result = self.execute_once(data)
.venv/lib/python3.7/site-packages/hypothesis/core.py:610: in execute_once
    result = self.test_runner(data, run)
.venv/lib/python3.7/site-packages/hypothesis/executors.py:52: in default_new_style_executor
    return function(data)
.venv/lib/python3.7/site-packages/hypothesis/core.py:550: in run
    args, kwargs = data.draw(self.search_strategy)
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:889: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:650: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:150: in do_draw
    return data.draw(self.wrapped_strategy)
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:650: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:150: in do_draw
    return data.draw(self.wrapped_strategy)
.venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:650: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

value = ((), {})

>       lambda value: target(*value[0], **value[1])  # type: ignore
    )
E   TypeError: __init__() missing 1 required positional argument: 'inner_value'

.venv/lib/python3.7/site-packages/hypothesis/strategies/_internal/core.py:1313: TypeError
```

Why does this happen?

My debugging session shows that:
- `required` [here](https://github.com/HypothesisWorks/hypothesis/blob/d1efdd688d889931427afa0defdc4abdbfc95c1a/hypothesis-python/src/hypothesis/strategies/_internal/core.py#L1282) is equal to `{'inner_value'}`
- But, since [here we fetch annotations](https://github.com/HypothesisWorks/hypothesis/blob/d1efdd688d889931427afa0defdc4abdbfc95c1a/hypothesis-python/src/hypothesis/strategies/_internal/core.py#L1291) from the object itself, not its `__init__` method. It results in `{'_inner_value': ~_ValueType}`
- And since these two sets do not match, the [resulting set here](https://github.com/HypothesisWorks/hypothesis/blob/d1efdd688d889931427afa0defdc4abdbfc95c1a/hypothesis-python/src/hypothesis/strategies/_internal/core.py#L1297) is empty
- And that's why we have this error 😢 

I can fix it! The only question I have is: 
Is it ok to chage how we get annotation from `target` if we are dealing with `__init__`?


Discussion:
Ok, this happens because of this line: https://github.com/HypothesisWorks/hypothesis/blob/hypothesis-python-5.31.0/hypothesis-python/src/hypothesis/internal/compat.py#L157-L158 Introduced in `5.31.0`

At this point `hints = {'_inner_value': ~_ValueType}` and `__init__` is not checked.

Related #2388 

I am not sure how to fix this 🤔 
Actually, no. It was working the same way even before `5.31.0`.

```python
        try:
            hints = typing.get_type_hints(thing)
        except (TypeError, NameError):
            hints = {}
        if hints or not inspect.isclass(thing):
            return hints
        try:
            return typing.get_type_hints(thing.__init__)
        except (TypeError, NameError, AttributeError):
            return {}
```

Here, `if hints` always evaluates to true for classes that have at least one annotation available. Mine has.
So, `__init__` is never checked.

Proposed solution:
- during the `st.builds` we can tell which mode we are using right now: object, object with `__init__` or function, method
- this way we can get annotations differently for different types

So, I can rewrite some parts of `st.builds` to handle this correctly. With backward compatibility. 
An alternative would be to try each method of getting hints in order of preference (`typing.get_type_hints()`, `__signature__`, `__init__`) and keep the additional hints from each.  This conceptually kinda ugly, but at least keeps all the problems in one place!

Happy to review whatever approach you think will work though :-)

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
@@ -129,15 +129,29 @@ def get_type_hints(thing):
 
         Tries harder: if the thing to inspect is a class but typing.get_type_hints
         raises an error or returns no hints, then this function will try calling it
-        on the __init__ method.  This second step often helps with user-defined
-        classes on older versions of Python.
+        on the __init__ method. This second step often helps with user-defined
+        classes on older versions of Python. The third step we take is trying
+        to fetch types from the __signature__ property.
+        They override any other ones we found earlier.
 
         Never errors: instead of raising TypeError for uninspectable objects, or
         NameError for unresolvable forward references, just return an empty dict.
         """
         try:
+            hints = typing.get_type_hints(thing)
+        except (AttributeError, TypeError, NameError):
             hints = {}
-            if inspect.isclass(thing) and hasattr(thing, "__signature__"):
+
+        if not inspect.isclass(thing):
+            return hints
+
+        try:
+            hints.update(typing.get_type_hints(thing.__init__))
+        except (TypeError, NameError, AttributeError):
+            pass
+
+        try:
+            if hasattr(thing, "__signature__"):
                 # It is possible for the signature and annotations attributes to
                 # differ on an object due to renamed arguments.
                 # To prevent missing arguments we use the signature to provide any type
@@ -146,20 +160,17 @@ def get_type_hints(thing):
                 # See https://github.com/HypothesisWorks/hypothesis/pull/2580
                 # for more details.
                 spec = inspect.getfullargspec(thing)
-                hints = {
-                    k: v
-                    for k, v in spec.annotations.items()
-                    if k in (spec.args + spec.kwonlyargs) and isinstance(v, type)
-                }
-            hints.update(typing.get_type_hints(thing))
+                hints.update(
+                    {
+                        k: v
+                        for k, v in spec.annotations.items()
+                        if k in (spec.args + spec.kwonlyargs) and isinstance(v, type)
+                    }
+                )
         except (AttributeError, TypeError, NameError):
-            hints = {}
-        if hints or not inspect.isclass(thing):
-            return hints
-        try:
-            return typing.get_type_hints(thing.__init__)
-        except (TypeError, NameError, AttributeError):
-            return {}
+            pass
+
+        return hints
 
 
 importlib_invalidate_caches = getattr(importlib, "invalidate_caches", lambda: ())


Test patch:
diff --git a/hypothesis-python/tests/cover/test_lookup_py36.py b/hypothesis-python/tests/cover/test_lookup_py36.py
--- a/hypothesis-python/tests/cover/test_lookup_py36.py
+++ b/hypothesis-python/tests/cover/test_lookup_py36.py
@@ -14,6 +14,7 @@
 # END HEADER
 
 import typing
+from inspect import signature
 
 import pytest
 
@@ -31,3 +32,53 @@ class TreeForwardRefs(typing.NamedTuple):
 @given(st.builds(TreeForwardRefs))
 def test_resolves_forward_references_outside_annotations(t):
     assert isinstance(t, TreeForwardRefs)
+
+
+_ValueType = typing.TypeVar("_ValueType")
+
+
+class Wrapper(typing.Generic[_ValueType]):
+    _inner_value: _ValueType
+
+    def __init__(self, inner_value: _ValueType) -> None:
+        self._inner_value = inner_value
+
+
+@given(st.builds(Wrapper))
+def test_issue_2603_regression(built):
+    """It was impossible to build annotated classes with constructors."""
+    assert isinstance(built, Wrapper)
+
+
+class AnnotatedConstructor(typing.Generic[_ValueType]):
+    value: _ValueType  # the same name we have in `__init__`
+
+    def __init__(self, value: int) -> None:
+        """By this example we show, that ``int`` is more important than ``_ValueType``."""
+        assert isinstance(value, int)
+
+
+@given(st.data())
+def test_constructor_is_more_important(data):
+    """Constructor types should take precedence over all other annotations."""
+    data.draw(st.builds(AnnotatedConstructor))
+
+
+def use_signature(self, value: str) -> None:
+    ...
+
+
+class AnnotatedConstructorWithSignature(typing.Generic[_ValueType]):
+    value: _ValueType  # the same name we have in `__init__`
+
+    __signature__ = signature(use_signature)
+
+    def __init__(self, value: int) -> None:
+        """By this example we show, that ``__signature__`` is the most important source."""
+        assert isinstance(value, str)
+
+
+@given(st.data())
+def test_signature_is_the_most_important_source(data):
+    """Signature types should take precedence over all other annotations."""
+    data.draw(st.builds(AnnotatedConstructorWithSignature))