Repository: HypothesisWorks/hypothesis

Issue:
Teach `st.from_type()` about remaining generic types
While working through the list of types in [PEP 585](https://www.python.org/dev/peps/pep-0585/) for #2653, I discovered some generic types that we don't currently have registered strategies for.  While they're rarely used and nobody has every complained, it would be nice to add them!  Note that this applies to every version of Python we support via the `typing` module equivalents; they're *not* new in 3.9.

https://github.com/HypothesisWorks/hypothesis/compare/master...Zac-HD:register-more-types has my current progress.  Still to do:

- [x] work out how `dict_items` is added to the strategy for `collections.abc.Set[T]` (which breaks because the elements are tuples, not the requested type), and stop that.
- [x] write tests against the `typing` module versions which work on all supported Pythons.
- [x] get `collections.abc.Generator` working.

`Coroutine`, `AsyncGenerator`, and `contextlib.AbstractAsyncContextManager` are hereby ruled out of scope though, since (a) nobody has asked for them and (b) testing them would be a complete pain because we don't currently have any async tests ([see here for how that would work](https://hypothesis.readthedocs.io/en/latest/details.html?highlight=async#custom-function-execution)).


Discussion:

Resolving patch:
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
@@ -23,6 +23,7 @@
 import ipaddress
 import numbers
 import os
+import re
 import sys
 import typing
 import uuid
@@ -32,6 +33,7 @@
 from hypothesis import strategies as st
 from hypothesis.errors import InvalidArgument, ResolutionFailed
 from hypothesis.internal.compat import ForwardRef, typing_root_type
+from hypothesis.internal.conjecture.utils import many as conjecture_utils_many
 from hypothesis.strategies._internal.ipaddress import (
     SPECIAL_IPv4_RANGES,
     SPECIAL_IPv6_RANGES,
@@ -215,13 +217,17 @@ def from_typing_type(thing):
         for k, v in _global_type_lookup.items()
         if is_generic_type(k) and try_issubclass(k, thing)
     }
-    if typing.Dict in mapping:
+    if typing.Dict in mapping or typing.Set in mapping:
         # ItemsView can cause test_lookup.py::test_specialised_collection_types
         # to fail, due to weird isinstance behaviour around the elements.
         mapping.pop(typing.ItemsView, None)
         if sys.version_info[:2] == (3, 6):  # pragma: no cover
             # `isinstance(dict().values(), Container) is False` on py36 only -_-
             mapping.pop(typing.ValuesView, None)
+    if typing.Deque in mapping and len(mapping) > 1:
+        # Resolving generic sequences to include a deque is more trouble for e.g.
+        # the ghostwriter than it's worth, via undefined names in the repr.
+        mapping.pop(typing.Deque)
     if len(mapping) > 1:
         # issubclass treats bytestring as a kind of sequence, which it is,
         # but treating it as such breaks everything else when it is presumed
@@ -339,6 +345,7 @@ def _networks(bits):
         _networks(128).map(lambda x: ipaddress.IPv6Network(x, strict=False)),
         st.sampled_from(SPECIAL_IPv6_RANGES).map(ipaddress.IPv6Network),
     ),
+    os.PathLike: st.builds(PurePath, st.text()),
     # Pull requests with more types welcome!
 }
 
@@ -346,8 +353,11 @@ def _networks(bits):
     [type(None)] + sorted(_global_type_lookup, key=str)
 )
 
-if sys.version_info[:2] >= (3, 6):  # pragma: no branch
-    _global_type_lookup[os.PathLike] = st.builds(PurePath, st.text())
+if sys.version_info[:2] >= (3, 7):  # pragma: no branch
+    _global_type_lookup[re.Match] = (
+        st.text().map(lambda c: re.match(".", c, flags=re.DOTALL)).filter(bool)
+    )
+    _global_type_lookup[re.Pattern] = st.builds(re.compile, st.sampled_from(["", b""]))
 if sys.version_info[:2] >= (3, 9):  # pragma: no cover
     # subclass of MutableMapping, and in Python 3.9 we resolve to a union
     # which includes this... but we don't actually ever want to build one.
@@ -371,6 +381,7 @@ def _networks(bits):
         # memoryview types, it is a subclass of Hashable and those types are not.
         # We therefore only generate the bytes type.
         typing.ByteString: st.binary(),
+        collections.abc.ByteString: st.binary(),
         # TODO: SupportsAbs and SupportsRound should be covariant, ie have functions.
         typing.SupportsAbs: st.one_of(
             st.booleans(),
@@ -552,6 +563,74 @@ def resolve_Iterator(thing):
     return st.iterables(st.from_type(thing.__args__[0]))
 
 
+@register(typing.Counter, st.builds(collections.Counter))
+def resolve_Counter(thing):
+    return st.dictionaries(
+        keys=st.from_type(thing.__args__[0]),
+        values=st.integers(),
+    ).map(collections.Counter)
+
+
+@register(typing.Deque, st.builds(collections.deque))
+def resolve_deque(thing):
+    return st.lists(st.from_type(thing.__args__[0])).map(collections.deque)
+
+
+@register(typing.ChainMap, st.builds(dict).map(collections.ChainMap))
+def resolve_ChainMap(thing):
+    return resolve_Dict(thing).map(collections.ChainMap)
+
+
+@register("OrderedDict", st.builds(dict).map(collections.OrderedDict))
+def resolve_OrderedDict(thing):
+    # typing.OrderedDict is new in Python 3.7.2
+    return resolve_Dict(thing).map(collections.OrderedDict)
+
+
+@register(typing.Pattern, st.builds(re.compile, st.sampled_from(["", b""])))
+def resolve_Pattern(thing):
+    if isinstance(thing.__args__[0], typing.TypeVar):
+        return st.builds(re.compile, st.sampled_from(["", b""]))
+    return st.just(re.compile(thing.__args__[0]()))
+
+
+@register(  # pragma: no branch  # coverage does not see lambda->exit branch
+    typing.Match,
+    st.text().map(lambda c: re.match(".", c, flags=re.DOTALL)).filter(bool),
+)
+def resolve_Match(thing):
+    if thing.__args__[0] == bytes:
+        return (
+            st.binary(min_size=1)
+            .map(lambda c: re.match(b".", c, flags=re.DOTALL))
+            .filter(bool)
+        )
+    return st.text().map(lambda c: re.match(".", c, flags=re.DOTALL)).filter(bool)
+
+
+class GeneratorStrategy(st.SearchStrategy):
+    def __init__(self, yields, returns):
+        assert isinstance(yields, st.SearchStrategy)
+        assert isinstance(returns, st.SearchStrategy)
+        self.yields = yields
+        self.returns = returns
+
+    def __repr__(self):
+        return f"<generators yields={self.yields!r} returns={self.returns!r}>"
+
+    def do_draw(self, data):
+        elements = conjecture_utils_many(data, min_size=0, max_size=100, average_size=5)
+        while elements.more():
+            yield data.draw(self.yields)
+        return data.draw(self.returns)
+
+
+@register(typing.Generator, GeneratorStrategy(st.none(), st.none()))
+def resolve_Generator(thing):
+    yields, _, returns = thing.__args__
+    return GeneratorStrategy(st.from_type(yields), st.from_type(returns))
+
+
 @register(typing.Callable, st.functions())
 def resolve_Callable(thing):
     # Generated functions either accept no arguments, or arbitrary arguments.


Test patch:
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
@@ -19,9 +19,11 @@
 import enum
 import inspect
 import io
+import re
 import string
 import sys
 import typing
+from inspect import signature
 from numbers import Real
 
 import pytest
@@ -47,7 +49,7 @@
 xfail_on_39 = () if sys.version_info[:2] < (3, 9) else pytest.mark.xfail
 
 
-@pytest.mark.parametrize("typ", generics)
+@pytest.mark.parametrize("typ", generics, ids=repr)
 @settings(
     suppress_health_check=[HealthCheck.too_slow, HealthCheck.filter_too_much],
     database=None,
@@ -91,6 +93,24 @@ def test_typing_Type_Union(ex):
     assert ex in (str, list)
 
 
+@pytest.mark.parametrize(
+    "typ",
+    [
+        collections.abc.ByteString,
+        # These are nonexistent or exist-but-are-not-types on Python 3.6
+        typing.Match if sys.version_info[:2] >= (3, 7) else int,
+        typing.Pattern if sys.version_info[:2] >= (3, 7) else int,
+        getattr(re, "Match", int),
+        getattr(re, "Pattern", int),
+    ],
+    ids=repr,
+)
+@given(data=st.data())
+def test_rare_types(data, typ):
+    ex = data.draw(from_type(typ))
+    assert isinstance(ex, typ)
+
+
 class Elem:
     pass
 
@@ -113,6 +133,8 @@ class Elem:
         (typing.Mapping[Elem, None], typing.Mapping),
         (typing.Container[Elem], typing.Container),
         (typing.NamedTuple("A_NamedTuple", (("elem", Elem),)), tuple),
+        (typing.Counter[Elem], typing.Counter),
+        (typing.Deque[Elem], typing.Deque),
     ],
     ids=repr,
 )
@@ -125,9 +147,30 @@ def test_specialised_collection_types(data, typ, coll_type):
     assume(instances)  # non-empty collections without calling len(iterator)
 
 
-@given(from_type(typing.DefaultDict[int, Elem]).filter(len))
-def test_defaultdict_values_type(ex):
-    assert all(isinstance(elem, Elem) for elem in ex.values())
+class ElemValue:
+    pass
+
+
+@pytest.mark.parametrize(
+    "typ,coll_type",
+    [
+        (typing.ChainMap[Elem, ElemValue], typing.ChainMap),
+        (typing.DefaultDict[Elem, ElemValue], typing.DefaultDict),
+    ]
+    + (
+        [(typing.OrderedDict[Elem, ElemValue], typing.OrderedDict)]
+        if hasattr(typing, "OrderedDict")  # Python 3.7.2 and later
+        else []
+    ),
+    ids=repr,
+)
+@given(data=st.data())
+def test_specialised_mapping_types(data, typ, coll_type):
+    ex = data.draw(from_type(typ).filter(len))
+    assert isinstance(ex, coll_type)
+    instances = [isinstance(elem, Elem) for elem in ex]
+    assert all(instances)
+    assert all(isinstance(elem, ElemValue) for elem in ex.values())
 
 
 @given(from_type(typing.ItemsView[Elem, Elem]).filter(len))
@@ -138,6 +181,27 @@ def test_ItemsView(ex):
     assert all(all(isinstance(e, Elem) for e in elem) for elem in ex)
 
 
+@pytest.mark.skipif(sys.version_info[:2] == (3, 6), reason="not a type on py36")
+@pytest.mark.parametrize("generic", [typing.Match, typing.Pattern])
+@pytest.mark.parametrize("typ", [bytes, str])
+@given(data=st.data())
+def test_regex_types(data, generic, typ):
+    x = data.draw(from_type(generic[typ]))
+    assert isinstance(x[0] if generic is typing.Match else x.pattern, typ)
+
+
+@given(x=infer)
+def test_Generator(x: typing.Generator[Elem, None, ElemValue]):
+    assert isinstance(x, typing.Generator)
+    try:
+        while True:
+            e = next(x)
+            assert isinstance(e, Elem)
+            x.send(None)  # The generators we create don't check the send type
+    except StopIteration as stop:
+        assert isinstance(stop.value, ElemValue)
+
+
 def test_Optional_minimises_to_None():
     assert minimal(from_type(typing.Optional[int]), lambda ex: True) is None
 
@@ -679,3 +743,53 @@ def test_compat_get_type_hints_aware_of_None_default():
 
     assert typing.get_type_hints(constructor)["a"] == typing.Optional[str]
     assert inspect.signature(constructor).parameters["a"].annotation == str
+
+
+_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))
diff --git a/hypothesis-python/tests/cover/test_lookup_py36.py b/hypothesis-python/tests/cover/test_lookup_py36.py
deleted file mode 100644
--- a/hypothesis-python/tests/cover/test_lookup_py36.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# 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
-
-import typing
-from inspect import signature
-
-import pytest
-
-from hypothesis import given, strategies as st
-from hypothesis.internal.compat import PYPY
-
-
-class TreeForwardRefs(typing.NamedTuple):
-    val: int
-    l: typing.Optional["TreeForwardRefs"]
-    r: typing.Optional["TreeForwardRefs"]
-
-
-@pytest.mark.skipif(PYPY, reason="pypy36 does not resolve the forward refs")
-@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))
diff --git a/hypothesis-python/tests/cover/test_lookup_py37.py b/hypothesis-python/tests/cover/test_lookup_py37.py
--- a/hypothesis-python/tests/cover/test_lookup_py37.py
+++ b/hypothesis-python/tests/cover/test_lookup_py37.py
@@ -18,6 +18,7 @@
 import collections
 import collections.abc
 import contextlib
+import re
 import sys
 
 import pytest
@@ -87,6 +88,11 @@ def test_resolving_standard_frozenset_as_generic(x: frozenset[Elem]):
     check(frozenset, x)
 
 
+@given(x=infer)
+def test_resolving_standard_deque_as_generic(x: collections.deque[Elem]):
+    check(collections.deque, x)
+
+
 @given(x=infer)
 def test_resolving_standard_defaultdict_as_generic(
     x: collections.defaultdict[Elem, Value]
@@ -95,6 +101,26 @@ def test_resolving_standard_defaultdict_as_generic(
     assert all(isinstance(e, Value) for e in x.values())
 
 
+@given(x=infer)
+def test_resolving_standard_ordered_dict_as_generic(
+    x: collections.OrderedDict[Elem, Value]
+):
+    check(collections.OrderedDict, x)
+    assert all(isinstance(e, Value) for e in x.values())
+
+
+@given(x=infer)
+def test_resolving_standard_counter_as_generic(x: collections.Counter[Elem]):
+    check(collections.Counter, x)
+    assume(any(x.values()))  # Check that we generated at least one nonzero count
+
+
+@given(x=infer)
+def test_resolving_standard_chainmap_as_generic(x: collections.ChainMap[Elem, Value]):
+    check(collections.ChainMap, 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)
@@ -105,6 +131,20 @@ def test_resolving_standard_iterator_as_generic(x: collections.abc.Iterator[Elem
     check(collections.abc.Iterator, x)
 
 
+@given(x=infer)
+def test_resolving_standard_generator_as_generic(
+    x: collections.abc.Generator[Elem, None, Value]
+):
+    assert isinstance(x, collections.abc.Generator)
+    try:
+        while True:
+            e = next(x)
+            assert isinstance(e, Elem)
+            x.send(None)  # The generators we create don't check the send type
+    except StopIteration as stop:
+        assert isinstance(stop.value, Value)
+
+
 @given(x=infer)
 def test_resolving_standard_reversible_as_generic(x: collections.abc.Reversible[Elem]):
     check(collections.abc.Reversible, x)
@@ -141,6 +181,11 @@ def test_resolving_standard_callable_no_args(x: collections.abc.Callable[[], Ele
         x(a=1)
 
 
+@given(x=infer)
+def test_resolving_standard_collections_set_as_generic(x: collections.abc.Set[Elem]):
+    check(collections.abc.Set, x)
+
+
 @given(x=infer)
 def test_resolving_standard_collections_mutableset_as_generic(
     x: collections.abc.MutableSet[Elem],
@@ -198,3 +243,27 @@ def test_resolving_standard_contextmanager_as_generic(
     x: contextlib.AbstractContextManager[Elem],
 ):
     assert isinstance(x, contextlib.AbstractContextManager)
+
+
+@given(x=infer)
+def test_resolving_standard_re_match_bytes_as_generic(x: re.Match[bytes]):
+    assert isinstance(x, re.Match)
+    assert isinstance(x[0], bytes)
+
+
+@given(x=infer)
+def test_resolving_standard_re_match_str_as_generic(x: re.Match[str]):
+    assert isinstance(x, re.Match)
+    assert isinstance(x[0], str)
+
+
+@given(x=infer)
+def test_resolving_standard_re_pattern_bytes_as_generic(x: re.Pattern[bytes]):
+    assert isinstance(x, re.Pattern)
+    assert isinstance(x.pattern, bytes)
+
+
+@given(x=infer)
+def test_resolving_standard_re_pattern_str_as_generic(x: re.Pattern[str]):
+    assert isinstance(x, re.Pattern)
+    assert isinstance(x.pattern, str)