Repository: HypothesisWorks/hypothesis

Issue:
Difference between types with type arguments and without them
I have this code:

```python
def associativity_law(
    mappable: MappableN[_FirstType, _SecondType, _ThirdType],
    first: Callable[[_FirstType], _NewType1],
    second: Callable[[_NewType1], _NewType2],
) -> None:
    assert mappable.map(first).map(second) == mappable.map(
        compose(first, second),
    )
```

Then, I use this code to generate values based on this annotation:

```python
@given(st.data())
def test_assoc(data: st.DataObject):
    st.register_type_strategy(
        MappableN[_FirstType, _SecondType, _ThirdType],  # just `MappableN` does not work
        st.one_of(
            st.builds(Result.from_value),
            st.builds(Result.from_failure),
        ),
    )
    x = data.draw(st.builds(associativity_law))
    assert x is None
```

So, the thing is that just `MappableN` does not work when trying to use it as a type in `st.register_type_strategy`. Output for the broken case:

```
E           hypothesis.errors.ResolutionFailed: Could not resolve returns.interfaces.mappable.MappableN[~_FirstType, ~_SecondType, ~_ThirdType] to a strategy; consider using register_type_strategy

.venv/lib/python3.8/site-packages/hypothesis/strategies/_internal/types.py:229: ResolutionFailed
```

I guess, that using `MappableN[_FirstType, _SecondType, _ThirdType]` to register a type is incorrect, because types should be used like this.
Refactor `registered_type` to be a common test utility
This pattern is used quite a lot:

```python
try:
    st.register_type_strategy(...)
finally:
    types._global_type_lookup.pop(...)
    st.from_type.__clear_cache()
```

Instead, something like `with registered_type(type_, resolve_to):` can be used.

So, it is a good idea to create a new helper for it in `tests/common/utils.py`
And also refactor the existing code that uses the old version.

More context: https://github.com/HypothesisWorks/hypothesis/pull/2543#discussion_r468529457


Discussion:
So, my fix for this problem (very straight-forward):

```python
    origin_handler = _global_type_lookup.get(origin)
    if origin_handler:
        return origin_handler if isinstance(origin_handler, st.SearchStrategy) else origin_handler(thing)
```

I have patched the code here: https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/src/hypothesis/strategies/_internal/types.py#L183

Does this look like something in the right direction? 
Yeah, I think that would work - we'll just need some user-defined generic types to test with.

It's still possible that I'll look at the results and decide that this is the wrong approach, but I think it's unlikely if this does solve your problem without breaking anything else. 
I will send a PR shorly. 
This is only the case for `python3.7+`. `python3.6` handles this correctly. 


Resolving patch:
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
@@ -73,6 +73,7 @@
 from hypothesis.internal.reflection import (
     define_function_signature,
     deprecated_posargs,
+    get_pretty_function_description,
     is_typed_named_tuple,
     nicerepr,
     proxies,
@@ -2087,6 +2088,12 @@ def register_type_strategy(
 
     ``strategy`` may be a search strategy, or a function that takes a type and
     returns a strategy (useful for generic types).
+
+    Note that you may not register a parametrised generic type (such as
+    ``MyCollection[int]``) directly, because the resolution logic does not
+    handle this case correctly.  Instead, you may register a *function* for
+    ``MyCollection`` and `inspect the type parameters within that function
+    <https://stackoverflow.com/q/48572831>`__.
     """
     # TODO: We would like to move this to the top level, but pending some major
     # refactoring it's hard to do without creating circular imports.
@@ -2101,6 +2108,28 @@ def register_type_strategy(
         )
     elif isinstance(strategy, SearchStrategy) and strategy.is_empty:
         raise InvalidArgument("strategy=%r must not be empty")
+    elif types.has_type_arguments(custom_type):
+        origin = getattr(custom_type, "__origin__", None)
+        if callable(strategy):
+            strategy_repr = get_pretty_function_description(strategy)
+        else:
+            strategy_repr = repr(strategy)
+        note_deprecation(
+            "Registering a generic type with arguments (%r) is deprecated, and "
+            "will be an error in future, because the resolution logic is badly "
+            "broken.  Instead, register a function for the origin type (%r) "
+            "which can inspect specific type objects and return a strategy.  "
+            "%s will be registered for any type arguments."
+            % (custom_type, origin, strategy_repr),
+            since="RELEASEDAY",
+        )
+        if origin in types._global_type_lookup:
+            raise InvalidArgument(
+                "Cannot register %r, because the without-arguments form %r already "
+                "has a registered strategy %s" % (custom_type, origin, strategy_repr)
+            )
+        custom_type = origin
+
     types._global_type_lookup[custom_type] = strategy
     from_type.__clear_cache()  # type: ignore
 
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
@@ -44,6 +44,16 @@
 except ImportError:
     typing_extensions = None  # type: ignore
 
+try:
+    from typing import GenericMeta as _GenericMeta  # python < 3.7
+except ImportError:
+    _GenericMeta = ()  # type: ignore
+
+try:
+    from typing import _GenericAlias  # type: ignore  # python >= 3.7
+except ImportError:
+    _GenericAlias = ()
+
 
 def type_sorting_key(t):
     """Minimise to None, then non-container types, then container types."""
@@ -85,11 +95,7 @@ def is_a_new_type(thing):
 
 def is_a_type(thing):
     """Return True if thing is a type or a generic type like thing."""
-    return (
-        isinstance(thing, type)
-        or isinstance(thing, typing_root_type)
-        or is_a_new_type(thing)
-    )
+    return isinstance(thing, type) or is_generic_type(thing) or is_a_new_type(thing)
 
 
 def is_typing_literal(thing):
@@ -101,6 +107,30 @@ def is_typing_literal(thing):
     )
 
 
+def has_type_arguments(type_):
+    """Decides whethere or not this type has applied type arguments."""
+    args = getattr(type_, "__args__", None)
+    if args and isinstance(type_, (_GenericAlias, _GenericMeta)):
+        # There are some cases when declared types do already have type arguments
+        # Like `Sequence`, that is `_GenericAlias(abc.Sequence[T])[T]`
+        parameters = getattr(type_, "__parameters__", None)
+        if parameters:  # So, we need to know if type args are just "aliases"
+            return args != parameters
+    return bool(args)
+
+
+def is_generic_type(type_):
+    """Decides whethere a given type is generic or not."""
+    # The ugly truth is that `MyClass`, `MyClass[T]`, and `MyClass[int]` are very different.
+    # In different python versions the might have the same type (3.6)
+    # or it can be regular type vs `_GenericAlias` (3.7+)
+    # We check for `MyClass[T]` and `MyClass[int]` with the first condition,
+    # while the second condition is for `MyClass` in `python3.7+`.
+    return isinstance(type_, typing_root_type) or (
+        isinstance(type_, type) and typing.Generic in type_.__mro__
+    )
+
+
 def from_typing_type(thing):
     # We start with special-case support for Union and Tuple - the latter
     # isn't actually a generic type. Then we handle Literal since it doesn't
@@ -187,7 +217,7 @@ def from_typing_type(thing):
     mapping = {
         k: v
         for k, v in _global_type_lookup.items()
-        if isinstance(k, typing_root_type) and try_issubclass(k, thing)
+        if is_generic_type(k) and try_issubclass(k, thing)
     }
     if typing.Dict in mapping:
         # The subtype relationships between generic and concrete View types
@@ -225,7 +255,7 @@ def from_typing_type(thing):
         if sum(try_issubclass(k, T) for T in mapping) == 1
     ]
     empty = ", ".join(repr(s) for s in strategies if s.is_empty)
-    if empty or not strategies:  # pragma: no cover
+    if empty or not strategies:
         raise ResolutionFailed(
             "Could not resolve %s to a strategy; consider using "
             "register_type_strategy" % (empty or thing,)


Test patch:
diff --git a/hypothesis-python/tests/common/utils.py b/hypothesis-python/tests/common/utils.py
--- a/hypothesis-python/tests/common/utils.py
+++ b/hypothesis-python/tests/common/utils.py
@@ -22,6 +22,8 @@
 from hypothesis.errors import HypothesisDeprecationWarning
 from hypothesis.internal.reflection import proxies
 from hypothesis.reporting import default, with_reporter
+from hypothesis.strategies._internal.core import from_type, register_type_strategy
+from hypothesis.strategies._internal.types import _global_type_lookup
 
 no_shrink = tuple(set(Phase) - {Phase.shrink})
 
@@ -164,3 +166,21 @@ def assert_falsifying_output(
 
     assert "%s example:" % (example_type,)
     assert_output_contains_failure(out.getvalue(), test, **kwargs)
+
+
+@contextlib.contextmanager
+def temp_registered(type_, strat_or_factory):
+    """Register and un-register a type for st.from_type().
+
+    This not too hard, but there's a subtlety in restoring the
+    previously-registered strategy which we got wrong in a few places.
+    """
+    prev = _global_type_lookup.get(type_)
+    try:
+        register_type_strategy(type_, strat_or_factory)
+        yield
+    finally:
+        del _global_type_lookup[type_]
+        from_type.__clear_cache()
+        if prev is not None:
+            register_type_strategy(type_, prev)
diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py
--- a/hypothesis-python/tests/cover/test_lookup.py
+++ b/hypothesis-python/tests/cover/test_lookup.py
@@ -31,7 +31,7 @@
 from hypothesis.strategies import from_type
 from hypothesis.strategies._internal import types
 from tests.common.debug import assert_all_examples, find_any, minimal
-from tests.common.utils import fails_with
+from tests.common.utils import fails_with, temp_registered
 
 sentinel = object()
 generics = sorted(
@@ -195,31 +195,19 @@ def inner_2(ex):
 
 def test_register_generic_typing_strats():
     # I don't expect anyone to do this, but good to check it works as expected
-    try:
+    with temp_registered(typing.Sequence, types._global_type_lookup[typing.Set]):
         # We register sets for the abstract sequence type, which masks subtypes
         # from supertype resolution but not direct resolution
-        st.register_type_strategy(
-            typing.Sequence, types._global_type_lookup[typing.Set]
+        assert_all_examples(
+            from_type(typing.Sequence[int]), lambda ex: isinstance(ex, set)
+        )
+        assert_all_examples(
+            from_type(typing.Container[int]),
+            lambda ex: not isinstance(ex, typing.Sequence),
+        )
+        assert_all_examples(
+            from_type(typing.List[int]), lambda ex: isinstance(ex, list)
         )
-
-        @given(from_type(typing.Sequence[int]))
-        def inner_1(ex):
-            assert isinstance(ex, set)
-
-        @given(from_type(typing.Container[int]))
-        def inner_2(ex):
-            assert not isinstance(ex, typing.Sequence)
-
-        @given(from_type(typing.List[int]))
-        def inner_3(ex):
-            assert isinstance(ex, list)
-
-        inner_1()
-        inner_2()
-        inner_3()
-    finally:
-        types._global_type_lookup.pop(typing.Sequence)
-        st.from_type.__clear_cache()
 
 
 def if_available(name):
diff --git a/hypothesis-python/tests/cover/test_type_lookup.py b/hypothesis-python/tests/cover/test_type_lookup.py
--- a/hypothesis-python/tests/cover/test_type_lookup.py
+++ b/hypothesis-python/tests/cover/test_type_lookup.py
@@ -14,6 +14,8 @@
 # END HEADER
 
 import enum
+import sys
+from typing import Callable, Generic, List, Sequence, TypeVar, Union
 
 import pytest
 
@@ -26,7 +28,8 @@
 from hypothesis.strategies._internal import types
 from hypothesis.strategies._internal.core import _strategies
 from hypothesis.strategies._internal.types import _global_type_lookup
-from tests.common.utils import fails_with
+from tests.common.debug import assert_all_examples, find_any
+from tests.common.utils import checks_deprecated_behaviour, fails_with, temp_registered
 
 # Build a set of all types output by core strategies
 blacklist = [
@@ -56,6 +59,12 @@
             continue
 
 
+def test_generic_sequence_of_integers_may_be_lists_or_bytes():
+    strat = st.from_type(Sequence[int])
+    find_any(strat, lambda x: isinstance(x, bytes))
+    find_any(strat, lambda x: isinstance(x, list))
+
+
 @pytest.mark.parametrize("typ", sorted(types_with_core_strat, key=str))
 def test_resolve_core_strategies(typ):
     @given(st.from_type(typ))
@@ -85,13 +94,8 @@ def test_lookup_values_are_strategies():
 @pytest.mark.parametrize("typ", sorted(types_with_core_strat, key=str))
 def test_lookup_overrides_defaults(typ):
     sentinel = object()
-    try:
-        strat = types._global_type_lookup[typ]
-        st.register_type_strategy(typ, st.just(sentinel))
+    with temp_registered(typ, st.just(sentinel)):
         assert st.from_type(typ).example() is sentinel
-    finally:
-        st.register_type_strategy(typ, strat)
-        st.from_type.__clear_cache()
     assert st.from_type(typ).example() is not sentinel
 
 
@@ -112,51 +116,35 @@ def test_custom_type_resolution_fails_without_registering():
 
 def test_custom_type_resolution():
     sentinel = object()
-    try:
-        st.register_type_strategy(UnknownType, st.just(sentinel))
+    with temp_registered(UnknownType, st.just(sentinel)):
         assert st.from_type(UnknownType).example() is sentinel
         # Also covered by registration of child class
         assert st.from_type(ParentUnknownType).example() is sentinel
-    finally:
-        types._global_type_lookup.pop(UnknownType)
-        st.from_type.__clear_cache()
-        assert UnknownType not in types._global_type_lookup
 
 
 def test_custom_type_resolution_with_function():
     sentinel = object()
-    try:
-        st.register_type_strategy(UnknownType, lambda _: st.just(sentinel))
+    with temp_registered(UnknownType, lambda _: st.just(sentinel)):
         assert st.from_type(UnknownType).example() is sentinel
         assert st.from_type(ParentUnknownType).example() is sentinel
-    finally:
-        types._global_type_lookup.pop(UnknownType)
-        st.from_type.__clear_cache()
 
 
 def test_custom_type_resolution_with_function_non_strategy():
-    try:
-        st.register_type_strategy(UnknownType, lambda _: None)
+    with temp_registered(UnknownType, lambda _: None):
         with pytest.raises(ResolutionFailed):
             st.from_type(UnknownType).example()
         with pytest.raises(ResolutionFailed):
             st.from_type(ParentUnknownType).example()
-    finally:
-        types._global_type_lookup.pop(UnknownType)
 
 
 def test_errors_if_generic_resolves_empty():
-    try:
-        st.register_type_strategy(UnknownType, lambda _: st.nothing())
+    with temp_registered(UnknownType, lambda _: st.nothing()):
         fails_1 = st.from_type(UnknownType)
         with pytest.raises(ResolutionFailed):
             fails_1.example()
         fails_2 = st.from_type(ParentUnknownType)
         with pytest.raises(ResolutionFailed):
             fails_2.example()
-    finally:
-        types._global_type_lookup.pop(UnknownType)
-        st.from_type.__clear_cache()
 
 
 def test_cannot_register_empty():
@@ -216,3 +204,94 @@ def test_uninspectable_from_type():
 def test_can_generate_from_all_registered_types(data, typ):
     value = data.draw(st.from_type(typ), label="value")
     assert isinstance(value, typ)
+
+
+T = TypeVar("T")
+
+
+class MyGeneric(Generic[T]):
+    def __init__(self, arg: T) -> None:
+        self.arg = arg
+
+
+def using_generic(instance: MyGeneric[T]) -> T:
+    return instance.arg
+
+
+def using_concrete_generic(instance: MyGeneric[int]) -> int:
+    return instance.arg
+
+
+def test_generic_origin_empty():
+    with pytest.raises(ResolutionFailed):
+        find_any(st.builds(using_generic))
+
+
+_skip_callables_mark = pytest.mark.skipif(sys.version_info[:2] < (3, 7), reason="old")
+
+
+@pytest.mark.parametrize(
+    "generic",
+    (
+        Union[str, int],
+        Sequence[Sequence[int]],
+        MyGeneric[str],
+        # On Python <= 3.6, we always trigger the multi-registration guard clause
+        # and raise InvalidArgument on the first attempted registration.
+        pytest.param(Callable[..., str], marks=_skip_callables_mark),
+        pytest.param(Callable[[int], str], marks=_skip_callables_mark),
+    ),
+    ids=repr,
+)
+@pytest.mark.parametrize("strategy", [st.none(), lambda _: st.none()])
+@checks_deprecated_behaviour
+def test_generic_origin_with_type_args(generic, strategy):
+    try:
+        # Registering a generic type with args is deprecated
+        st.register_type_strategy(generic, strategy)
+        assert generic not in types._global_type_lookup
+        assert generic.__origin__ in types._global_type_lookup
+        # But trying to register another strategy does, since that could be
+        # a symptom of trying multiple registrations for different args
+        with pytest.raises(InvalidArgument):
+            st.register_type_strategy(generic, strategy)
+    finally:
+        st.from_type.__clear_cache()
+        for x in (generic, generic.__origin__):
+            types._global_type_lookup.pop(x, None)
+
+
+@pytest.mark.parametrize(
+    "generic",
+    (
+        Callable,
+        List,
+        Sequence,
+        # you can register types with all generic parameters
+        List[T],
+        Sequence[T],
+        # User-defined generics should also work
+        MyGeneric,
+        MyGeneric[T],
+    ),
+)
+def test_generic_origin_without_type_args(generic):
+    with temp_registered(generic, st.just("example")):
+        pass
+
+
+@pytest.mark.skipif(sys.version_info[:2] <= (3, 5), reason="typing on 3.5 is wild")
+def test_generic_origin_from_type():
+    with temp_registered(MyGeneric, st.builds(MyGeneric)):
+        find_any(st.from_type(MyGeneric[T]))
+        find_any(st.from_type(MyGeneric[int]))
+        find_any(st.from_type(MyGeneric))
+        find_any(st.builds(using_generic))
+        find_any(st.builds(using_concrete_generic))
+
+
+def test_generic_origin_concrete_builds():
+    with temp_registered(MyGeneric, st.builds(MyGeneric, st.integers())):
+        assert_all_examples(
+            st.builds(using_generic), lambda example: isinstance(example, int)
+        )