Repository: HypothesisWorks/hypothesis

Issue:
typing.Hashable and typing.Sized error on inference
## typing.Hashable and typing.Sized error on inference

Version: 4.53.2
Python : 3.7.4


heres an minimum example to reproduce the error: 

```python
from typing import Hashable
from hypothesis import given, infer


@given(key=infer)
def test_setitem_with_hashable(key: Hashable):
    d = dict()
    d[key] = 1
    assert d[key] == 1

```

**expected behaviour**: infer infers a strategy of hashable objects from my type hint OR errors gracefully and tells me I can't use `typing.Hashable`.

**actual behaviour**:

inferring a strategy based on `typing.Hashable` type fails when we try and to index an empty `__args__` tuple in  `resolve_FrozenSet` as you can see from the tailend of this traceback:
```
env/lib/python3.7/site-packages/hypothesis/strategies/_internal/core.py:1452: in from_type
    return types.from_typing_type(thing)
env/lib/python3.7/site-packages/hypothesis/strategies/_internal/types.py:182: in from_typing_type
    for k, v in mapping.items()
env/lib/python3.7/site-packages/hypothesis/strategies/_internal/types.py:183: in <listcomp>
    if sum(try_issubclass(k, T) for T in mapping) == 1
env/lib/python3.7/site-packages/hypothesis/strategies/_internal/types.py:340: in really_inner
    return func(thing)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

thing = typing.Hashable

    @register(typing.FrozenSet, st.builds(frozenset))
    def resolve_FrozenSet(thing):
>       return st.frozensets(st.from_type(thing.__args__[0]))
E       IndexError: tuple index out of range

env/lib/python3.7/site-packages/hypothesis/strategies/_internal/types.py:377: IndexError
```


`typing.Hashable` does not accept type args via `__class_getitem__` (it would make no sense) and `typing.Hashable.__args__` exists but is an empty tuple ( which is also a bit silly but whatever). 
to be clear this works just fine:

```
from collections.abc import Hashable
from hypothesis import given, infer


@given(key=infer)
def test_setitem_with_hashable(key: Hashable):
    d = dict()
    d[key] = 1
    assert d[key] == 1
```

which is good because `typing.Hashable.__origin__` is `collections.abc.Hashable`.


I had a look for other things in the typing module with these properties and i can see that `typing.Sized` has precisely the same issue (and also works fine with `from_type(typing.Sized.__origin__)`).


---
## how can I help?
I'm happy to put up a PR to patch these in to [strategies._internal.types.py](https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/src/hypothesis/strategies/_internal/types.py#L315.)

Let me know if you're happy for me to go ahead i will get to work on a PR.








Discussion:
Go for it - I think we'll want to take the first element of `__args__` if it's non-empty, falling back to `__origin__` otherwise. A helper function might make this easier! 
> Go for it - I think we'll want to take the first element of `__args__` if it's non-empty, falling back to `__origin__` otherwise. A helper function might make this easier!

cool. thanks for tip! 

you can assign me to this issue if you like.

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
@@ -1396,12 +1396,21 @@ def everything_except(excluded_types):
     # refactoring it's hard to do without creating circular imports.
     from hypothesis.strategies._internal import types
 
-    def as_strategy(strat_or_callable, thing):
+    def as_strategy(strat_or_callable, thing, final=True):
         # User-provided strategies need some validation, and callables even more
         # of it.  We do this in three places, hence the helper function
         if not isinstance(strat_or_callable, SearchStrategy):
             assert callable(strat_or_callable)  # Validated in register_type_strategy
-            strategy = strat_or_callable(thing)
+            try:
+                # On Python 3.6, typing.Hashable is just an alias for abc.Hashable,
+                # and the resolver function for Type throws an AttributeError because
+                # Hashable has no __args__.  We discard such errors when attempting
+                # to resolve subclasses, because the function was passed a weird arg.
+                strategy = strat_or_callable(thing)
+            except Exception:  # pragma: no cover
+                if not final:
+                    return NOTHING
+                raise
         else:
             strategy = strat_or_callable
         if not isinstance(strategy, SearchStrategy):
@@ -1455,13 +1464,13 @@ def as_strategy(strat_or_callable, thing):
     # type.  For example, `Number -> integers() | floats()`, but bools() is
     # not included because bool is a subclass of int as well as Number.
     strategies = [
-        as_strategy(v, thing)
+        as_strategy(v, thing, final=False)
         for k, v in sorted(types._global_type_lookup.items(), key=repr)
         if isinstance(k, type)
         and issubclass(k, thing)
         and sum(types.try_issubclass(k, typ) for typ in types._global_type_lookup) == 1
     ]
-    if strategies:
+    if any(not s.is_empty for s in strategies):
         return one_of(strategies)
     # If we don't have a strategy registered for this type or any subtype, we
     # may be able to fall back on type annotations.
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
@@ -161,6 +161,18 @@ def from_typing_type(thing):
     # Now, confirm that we're dealing with a generic type as we expected
     if not isinstance(thing, typing_root_type):  # pragma: no cover
         raise ResolutionFailed("Cannot resolve %s to a strategy" % (thing,))
+
+    # Some "generic" classes are not generic *in* anything - for example both
+    # Hashable and Sized have `__args__ == ()` on Python 3.7 or later.
+    # (In 3.6 they're just aliases for the collections.abc classes)
+    origin = getattr(thing, "__origin__", thing)
+    if (
+        typing.Hashable is not abc.Hashable
+        and origin in vars(abc).values()
+        and len(getattr(thing, "__args__", None) or []) == 0
+    ):  # pragma: no cover  # impossible on 3.6 where we measure coverage.
+        return st.from_type(origin)
+
     # Parametrised generic types have their __origin__ attribute set to the
     # un-parametrised version, which we need to use in the subclass checks.
     # e.g.:     typing.List[int].__origin__ == typing.List
@@ -210,6 +222,7 @@ def from_typing_type(thing):
     tuple: st.builds(tuple),
     list: st.builds(list),
     set: st.builds(set),
+    abc.MutableSet: st.builds(set),
     frozenset: st.builds(frozenset),
     dict: st.builds(dict),
     type(lambda: None): st.functions(),
@@ -294,7 +307,9 @@ def from_typing_type(thing):
     _global_type_lookup.update(
         {
             typing.ByteString: st.binary(),
-            typing.Reversible: st.lists(st.integers()),
+            # Reversible is somehow a subclass of Hashable, so we tuplize it.
+            # See also the discussion at https://bugs.python.org/issue39046
+            typing.Reversible: st.lists(st.integers()).map(tuple),  # type: ignore
             typing.SupportsAbs: st.complex_numbers(),
             typing.SupportsComplex: st.complex_numbers(),
             typing.SupportsFloat: st.floats(),


Test patch:
diff --git a/hypothesis-python/tests/py3/test_lookup.py b/hypothesis-python/tests/py3/test_lookup.py
--- a/hypothesis-python/tests/py3/test_lookup.py
+++ b/hypothesis-python/tests/py3/test_lookup.py
@@ -563,3 +563,14 @@ def bar(self):
 @given(st.from_type(AbstractBar))
 def test_cannot_resolve_abstract_class_with_no_concrete_subclass(instance):
     assert False, "test body unreachable as strategy cannot resolve"
+
+
+@pytest.mark.parametrize("typ", [typing.Hashable, typing.Sized])
+@given(data=st.data())
+def test_inference_on_generic_collections_abc_aliases(typ, data):
+    # regression test for inference bug on types that are just aliases
+    # types for simple interfaces in collections abc and take no args
+    # the typing module such as Hashable and Sized
+    # see https://github.com/HypothesisWorks/hypothesis/issues/2272
+    value = data.draw(st.from_type(typ))
+    assert isinstance(value, typ)