Repository: HypothesisWorks/hypothesis

Issue:
Better support for `typing_extensions` module
[The `typing_extensions` module](https://pypi.org/project/typing-extensions/) "contains both backports of new types added to the typing module, as well as experimental types that will eventually be added to the typing module".

What makes this relevant to us is that we have a bunch of internal code that looks for these types *specifically in the `typing` module*, e.g.:

https://github.com/HypothesisWorks/hypothesis/blob/cd9d7b3a54d89fb3dc8617f225fd976e6ea14d8e/hypothesis-python/src/hypothesis/strategies/_internal/types.py#L84-L88

So if you're using `typing.Literal` in Python 3.8, that works.  But if you're using `typing_extensions.Literal` in Python 3.7 - or 3.8 for compatibility - you're out of luck.

Going through and inserting checks to handle `typing_extensions` would improve our inference support for projects like [`LibCST`](https://pypi.org/project/libcst/) - I found this while working on an extension to Zac-HD/hypothesmith#2, but obviously it's a generally useful feature. 

The main cost will be setting up tests for another optional integration to check that our non-typing_extensions-specific features work whether it's installed or not, that they work when installed, and ideally that we don't import it if it's not already in use.


Discussion:

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
@@ -1343,9 +1343,15 @@ def everything_except(excluded_types):
 
 
 def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]:
+    # 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.
+    from hypothesis.strategies._internal import types
+
     if (
         hasattr(typing, "_TypedDictMeta")
         and type(thing) is typing._TypedDictMeta  # type: ignore
+        or hasattr(types.typing_extensions, "_TypedDictMeta")
+        and type(thing) is types.typing_extensions._TypedDictMeta  # type: ignore
     ):  # pragma: no cover
         # The __optional_keys__ attribute may or may not be present, but if there's no
         # way to tell and we just have to assume that everything is required.
@@ -1357,10 +1363,6 @@ def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]:
             optional={k: v for k, v in anns.items() if k in optional},
         )
 
-    # 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.
-    from hypothesis.strategies._internal import types
-
     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
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
@@ -39,6 +39,11 @@
 from hypothesis.strategies._internal.lazy import unwrap_strategies
 from hypothesis.strategies._internal.strategies import OneOfStrategy
 
+try:
+    import typing_extensions
+except ImportError:
+    typing_extensions = None  # type: ignore
+
 
 def type_sorting_key(t):
     """Minimise to None, then non-container types, then container types."""
@@ -68,9 +73,13 @@ def is_a_new_type(thing):
     # than an actual type, but we can check whether that thing matches.
     return (
         hasattr(thing, "__supertype__")
-        and hasattr(typing, "NewType")
+        and (
+            hasattr(typing, "NewType")
+            and getattr(thing, "__module__", None) == "typing"
+            or hasattr(typing_extensions, "NewType")
+            and getattr(thing, "__module__", None) == "typing_extensions"
+        )
         and inspect.isfunction(thing)
-        and getattr(thing, "__module__", None) == "typing"
     )
 
 
@@ -87,6 +96,8 @@ def is_typing_literal(thing):
     return (
         hasattr(typing, "Literal")
         and getattr(thing, "__origin__", None) == typing.Literal
+        or hasattr(typing_extensions, "Literal")
+        and getattr(thing, "__origin__", None) == typing_extensions.Literal
     )
 
 
@@ -393,11 +404,11 @@ def _networks(bits):
     _global_type_lookup[typing.SupportsIndex] = st.integers() | st.booleans()  # type: ignore
 
 
-def register(type_, fallback=None):
+def register(type_, fallback=None, *, module=typing):
     if isinstance(type_, str):
         # Use the name of generic types which are not available on all
         # versions, and the function just won't be added to the registry
-        type_ = getattr(typing, type_, None)
+        type_ = getattr(module, type_, None)
         if type_ is None:  # pragma: no cover
             return lambda f: f
 
@@ -419,6 +430,7 @@ def really_inner(thing):
 
 
 @register("Type")
+@register("Type", module=typing_extensions)
 def resolve_Type(thing):
     if getattr(thing, "__args__", None) is None:
         return st.just(type)
@@ -488,6 +500,7 @@ def resolve_Dict(thing):
 
 
 @register("DefaultDict", st.builds(collections.defaultdict))
+@register("DefaultDict", st.builds(collections.defaultdict), module=typing_extensions)
 def resolve_DefaultDict(thing):
     return resolve_Dict(thing).map(lambda d: collections.defaultdict(None, d))
 


Test patch:
diff --git a/hypothesis-python/tests/typing_extensions/__init__.py b/hypothesis-python/tests/typing_extensions/__init__.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/typing_extensions/__init__.py
@@ -0,0 +1,14 @@
+# 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
diff --git a/hypothesis-python/tests/typing_extensions/test_backported_types.py b/hypothesis-python/tests/typing_extensions/test_backported_types.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/typing_extensions/test_backported_types.py
@@ -0,0 +1,96 @@
+# 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 collections
+import sys
+from typing import Union
+
+import pytest
+from typing_extensions import DefaultDict, Literal, NewType, Type
+
+import hypothesis.strategies as st
+from hypothesis import assume, given
+from hypothesis.strategies import from_type
+
+
+@pytest.mark.skipif(sys.version_info[:2] < (3, 7), reason="lovecraftian implemenation")
+@pytest.mark.parametrize("value", ["dog", b"goldfish", 42, 63.4, -80.5, False])
+def test_typing_extensions_Literal(value):
+    assert from_type(Literal[value]).example() == value
+
+
+@pytest.mark.skipif(sys.version_info[:2] < (3, 7), reason="lovecraftian implemenation")
+@given(st.data())
+def test_typing_extensions_Literal_nested(data):
+    lit = Literal
+    values = [
+        (lit["hamster", 0], ("hamster", 0)),
+        (lit[26, False, "bunny", 130], (26, False, "bunny", 130)),
+        (lit[lit[1]], {1}),
+        (lit[lit[1], 2], {1, 2}),
+        (lit[1, lit[2], 3], {1, 2, 3}),
+        (lit[lit[lit[1], lit[2]], lit[lit[3], lit[4]]], {1, 2, 3, 4}),
+    ]
+    literal_type, flattened_literals = data.draw(st.sampled_from(values))
+    assert data.draw(st.from_type(literal_type)) in flattened_literals
+
+
+@pytest.mark.skipif(sys.version_info[:2] == (3, 5), reason="no attribute annotations")
+def test_simple_typeddict():
+    exec(
+        """
+from hypothesis import given
+from hypothesis.strategies import from_type
+from typing_extensions import TypedDict
+
+class A(TypedDict):
+    a: int
+
+@given(from_type(A))
+def test_simple_typeddict(value):
+    assert type(value) == dict
+    assert set(value) == {"a"}
+    assert isinstance(value["a"], int)
+
+test_simple_typeddict()
+"""
+    )
+
+
+def test_typing_extensions_Type_int():
+    assert from_type(Type[int]).example() is int
+
+
+@given(from_type(Type[Union[str, list]]))
+def test_typing_extensions_Type_Union(ex):
+    assert ex in (str, list)
+
+
+def test_resolves_NewType():
+    typ = NewType("T", int)
+    nested = NewType("NestedT", typ)
+    uni = NewType("UnionT", Union[int, None])
+    assert isinstance(from_type(typ).example(), int)
+    assert isinstance(from_type(nested).example(), int)
+    assert isinstance(from_type(uni).example(), (int, type(None)))
+
+
+@pytest.mark.skipif(sys.version_info[:2] < (3, 6), reason="new addition")
+@given(from_type(DefaultDict[int, int]))
+def test_defaultdict(ex):
+    assert isinstance(ex, collections.defaultdict)
+    assume(ex)
+    assert all(isinstance(elem, int) for elem in ex)
+    assert all(isinstance(elem, int) for elem in ex.values())