Repository: HypothesisWorks/hypothesis

Issue:
Test `st.from_type()` on builtin generics from Python 3.9
Python 3.9 is coming out soon!  Happily, we're already running it in our CI - so anything that worked before still works on the new Python.  There are however also *new features*, like [PEP 585](https://www.python.org/dev/peps/pep-0585/) (type annotations using builtins as generics).

As [Guido highlights](https://twitter.com/gvanrossum/status/1313216099429695489), we should also check that everything works with `from __future__ import annotations` - that's not new in 3.9 but will become the default behaviour in 3.10!

I don't think we actually need any new logic to handle this, but we should really add some py39-only tests to make sure it keeps working :grin: 


Discussion:
WIP at https://github.com/HypothesisWorks/hypothesis/compare/master...Zac-HD:pep585-annotations and it turns out that we *do* need some new logic after all.  Ah well.

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
@@ -1470,8 +1470,14 @@ def as_strategy(strat_or_callable, thing, final=True):
     # common) way to resolve a type to a strategy.  Note that the value in the
     # lookup may be a strategy or a function from type -> strategy; and we
     # convert empty results into an explicit error.
-    if thing in types._global_type_lookup:
-        return as_strategy(types._global_type_lookup[thing], thing)
+    try:
+        if thing in types._global_type_lookup:
+            return as_strategy(types._global_type_lookup[thing], thing)
+    except TypeError:  # pragma: no cover
+        # This is due to a bizarre divergence in behaviour under Python 3.9.0:
+        # typing.Callable[[], foo] has __args__ = (foo,) but collections.abc.Callable
+        # has __args__ = ([], foo); and as a result is non-hashable.
+        pass
     # We also have a special case for TypeVars.
     # They are represented as instances like `~T` when they come here.
     # We need to work with their type instead.
@@ -1482,7 +1488,11 @@ def as_strategy(strat_or_callable, thing, final=True):
     # We'll start by checking if thing is from from the typing module,
     # because there are several special cases that don't play well with
     # subclass and instance checks.
-    if isinstance(thing, typing_root_type):
+    if isinstance(thing, typing_root_type) or (
+        sys.version_info[:2] >= (3, 9)
+        and isinstance(getattr(thing, "__origin__", None), type)
+        and getattr(thing, "__args__", None)
+    ):
         return types.from_typing_type(thing)
     # If it's not from the typing module, we get all registered types that are
     # a subclass of `thing` and are not themselves a subtype of any other such
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
@@ -191,7 +191,9 @@ def from_typing_type(thing):
                 literals.append(arg)
         return st.sampled_from(literals)
     # Now, confirm that we're dealing with a generic type as we expected
-    if not isinstance(thing, typing_root_type):  # pragma: no cover
+    if sys.version_info[:2] < (3, 9) and 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
@@ -557,8 +559,12 @@ def resolve_Callable(thing):
     # use of keyword arguments and we'd rather not force positional-only.
     if not thing.__args__:  # pragma: no cover  # varies by minor version
         return st.functions()
+    # Note that a list can only appear in __args__ under Python 3.9 with the
+    # collections.abc version; see https://bugs.python.org/issue42195
     return st.functions(
-        like=(lambda: None) if len(thing.__args__) == 1 else (lambda *a, **k: None),
+        like=(lambda: None)
+        if len(thing.__args__) == 1 or thing.__args__[0] == []
+        else (lambda *a, **k: None),
         returns=st.from_type(thing.__args__[-1]),
     )
 


Test patch:
diff --git a/hypothesis-python/tests/conftest.py b/hypothesis-python/tests/conftest.py
--- a/hypothesis-python/tests/conftest.py
+++ b/hypothesis-python/tests/conftest.py
@@ -29,6 +29,8 @@
 # Skip collection of tests which require the Django test runner,
 # or that don't work on the current version of Python.
 collect_ignore_glob = ["django/*"]
+if sys.version_info < (3, 7):
+    collect_ignore_glob.append("cover/*py37*")
 if sys.version_info < (3, 8):
     collect_ignore_glob.append("cover/*py38*")
 
diff --git a/hypothesis-python/tests/cover/test_lookup_py37.py b/hypothesis-python/tests/cover/test_lookup_py37.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_lookup_py37.py
@@ -0,0 +1,200 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Most of this work is copyright (C) 2013-2020 David R. MacIver
+# (david@drmaciver.com), but it contains contributions by others. See
+# CONTRIBUTING.rst for a full list of people who may hold copyright, and
+# consult the git log if you need to determine who owns an individual
+# contribution.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+#
+# END HEADER
+
+from __future__ import annotations
+
+import collections
+import collections.abc
+import contextlib
+import sys
+
+import pytest
+
+from hypothesis import assume, given, infer
+
+# This test file is skippped on Python <= 3.6, where `list[Elem]` is always a
+# SyntaxError.  On Python 3.7 and 3.8, `from __future__ import annotations` means
+# that the syntax is supported; but the feature fails at runtime.  On Python
+# 3.9 and later, it should all work.
+#
+# For details, see https://www.python.org/dev/peps/pep-0585/
+
+if sys.version_info < (3, 9):
+    pytestmark = pytest.mark.xfail(
+        raises=Exception, strict=True, reason="Requires Python 3.9 (PEP 585) or later."
+    )
+
+
+class Elem:
+    pass
+
+
+class Value:
+    pass
+
+
+def check(t, ex):
+    assert isinstance(ex, t)
+    assert all(isinstance(e, Elem) for e in ex)
+    assume(ex)
+
+
+@given(x=infer)
+def test_resolving_standard_tuple1_as_generic(x: tuple[Elem]):
+    check(tuple, x)
+
+
+@given(x=infer)
+def test_resolving_standard_tuple2_as_generic(x: tuple[Elem, Elem]):
+    check(tuple, x)
+
+
+@given(x=infer)
+def test_resolving_standard_tuple_variadic_as_generic(x: tuple[Elem, ...]):
+    check(tuple, x)
+
+
+@given(x=infer)
+def test_resolving_standard_list_as_generic(x: list[Elem]):
+    check(list, x)
+
+
+@given(x=infer)
+def test_resolving_standard_dict_as_generic(x: dict[Elem, Value]):
+    check(dict, x)
+    assert all(isinstance(e, Value) for e in x.values())
+
+
+@given(x=infer)
+def test_resolving_standard_set_as_generic(x: set[Elem]):
+    check(set, x)
+
+
+@given(x=infer)
+def test_resolving_standard_frozenset_as_generic(x: frozenset[Elem]):
+    check(frozenset, x)
+
+
+@given(x=infer)
+def test_resolving_standard_defaultdict_as_generic(
+    x: collections.defaultdict[Elem, Value]
+):
+    check(collections.defaultdict, x)
+    assert all(isinstance(e, Value) for e in x.values())
+
+
+@given(x=infer)
+def test_resolving_standard_iterable_as_generic(x: collections.abc.Iterable[Elem]):
+    check(collections.abc.Iterable, x)
+
+
+@given(x=infer)
+def test_resolving_standard_iterator_as_generic(x: collections.abc.Iterator[Elem]):
+    check(collections.abc.Iterator, x)
+
+
+@given(x=infer)
+def test_resolving_standard_reversible_as_generic(x: collections.abc.Reversible[Elem]):
+    check(collections.abc.Reversible, x)
+
+
+@given(x=infer)
+def test_resolving_standard_container_as_generic(x: collections.abc.Container[Elem]):
+    check(collections.abc.Container, x)
+
+
+@given(x=infer)
+def test_resolving_standard_collection_as_generic(x: collections.abc.Collection[Elem]):
+    check(collections.abc.Collection, x)
+
+
+@given(x=infer)
+def test_resolving_standard_callable_ellipsis(x: collections.abc.Callable[..., Elem]):
+    assert isinstance(x, collections.abc.Callable)
+    assert callable(x)
+    # ... implies *args, **kwargs; as would any argument types
+    assert isinstance(x(), Elem)
+    assert isinstance(x(1, 2, 3, a=4, b=5, c=6), Elem)
+
+
+@given(x=infer)
+def test_resolving_standard_callable_no_args(x: collections.abc.Callable[[], Elem]):
+    assert isinstance(x, collections.abc.Callable)
+    assert callable(x)
+    # [] implies that no arguments are accepted
+    assert isinstance(x(), Elem)
+    with pytest.raises(TypeError):
+        x(1)
+    with pytest.raises(TypeError):
+        x(a=1)
+
+
+@given(x=infer)
+def test_resolving_standard_collections_mutableset_as_generic(
+    x: collections.abc.MutableSet[Elem],
+):
+    check(collections.abc.MutableSet, x)
+
+
+@given(x=infer)
+def test_resolving_standard_mapping_as_generic(x: collections.abc.Mapping[Elem, Value]):
+    check(collections.abc.Mapping, x)
+    assert all(isinstance(e, Value) for e in x.values())
+
+
+@given(x=infer)
+def test_resolving_standard_mutable_mapping_as_generic(
+    x: collections.abc.MutableMapping[Elem, Value],
+):
+    check(collections.abc.MutableMapping, x)
+    assert all(isinstance(e, Value) for e in x.values())
+
+
+@given(x=infer)
+def test_resolving_standard_sequence_as_generic(x: collections.abc.Sequence[Elem]):
+    check(collections.abc.Sequence, x)
+
+
+@given(x=infer)
+def test_resolving_standard_mutable_sequence_as_generic(
+    x: collections.abc.MutableSequence[Elem],
+):
+    check(collections.abc.MutableSequence, x)
+
+
+@given(x=infer)
+def test_resolving_standard_keysview_as_generic(x: collections.abc.KeysView[Elem]):
+    check(collections.abc.KeysView, x)
+
+
+@given(x=infer)
+def test_resolving_standard_itemsview_as_generic(
+    x: collections.abc.ItemsView[Elem, Value]
+):
+    assert isinstance(x, collections.abc.ItemsView)
+    assert all(isinstance(e, Elem) and isinstance(v, Value) for e, v in x)
+    assume(x)
+
+
+@given(x=infer)
+def test_resolving_standard_valuesview_as_generic(x: collections.abc.ValuesView[Elem]):
+    check(collections.abc.ValuesView, x)
+
+
+@given(x=infer)
+def test_resolving_standard_contextmanager_as_generic(
+    x: contextlib.AbstractContextManager[Elem],
+):
+    assert isinstance(x, contextlib.AbstractContextManager)