Repository: HypothesisWorks/hypothesis

Issue:
Can't use registered strategy for typed dict
Here is a reproducible example of the problem:
```python
# test.py
from typing import TypedDict, Callable, NamedTuple

from hypothesis import given
from hypothesis.strategies import composite, register_type_strategy, builds, from_type

class SomeData(TypedDict):
    a: str
    b: int

@composite
def some_data_composite(draw: Callable):
    assert 0  # to be sure when it works

register_type_strategy(SomeData, some_data_composite())

@given(from_type(SomeData))
def test_some_data(item):
    print(item)
```

I'm expecting `pytest test.py` to fail since there is `assert 0` in `some_data_composite` strategy.

The test passes (hypothesis==6.3.0), but if I will replace `class SomeData(TypedDict)` with `class SomeData(NamedTuple)` or `class SomeData`, it will fail.

This happens because the TypedDict case is the first thing that is handled in `hypothesis.strategies._internal.core._from_type`, much earlier, than `_global_type_lookup` check.

This seems a problem for me since I'm using Hypothesis with Deal and I want to set up a default strategy for my typed dictionaries.

If this bug is confirmed, I'll try to fix it.


Discussion:
Yep, that's a bug! We'll be happy to accept a PR to make this less hard-coded 😊

- we'll also want to handle inheritance for TypedDicts - if resolving `class SomeMoreData(SomeData):`, the fields from `SomeData` should use whatever was registered for that type.  It's OK to leave this for a future PR, of course - we're usually happy to take incremental improvements!
- tip: [enabling syntax highlighting on GitHub](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks)

(I'm also happy to give hints or spoilers about how I'd fix it, if you want/get stuck. Just let me know) 

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
@@ -1503,22 +1503,6 @@ def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]:
     # 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.
-        # See https://github.com/python/cpython/pull/17214 for details.
-        optional = getattr(thing, "__optional_keys__", ())
-        anns = {k: from_type(v) for k, v in thing.__annotations__.items()}
-        return fixed_dictionaries(  # type: ignore
-            mapping={k: v for k, v in anns.items() if k not in optional},
-            optional={k: v for k, v in anns.items() if k in optional},
-        )
-
     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
@@ -1587,6 +1571,21 @@ def as_strategy(strat_or_callable, thing, final=True):
         # typing.Callable[[], foo] has __args__ = (foo,) but collections.abc.Callable
         # has __args__ = ([], foo); and as a result is non-hashable.
         pass
+    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.
+        # See https://github.com/python/cpython/pull/17214 for details.
+        optional = getattr(thing, "__optional_keys__", ())
+        anns = {k: from_type(v) for k, v in thing.__annotations__.items()}
+        return fixed_dictionaries(  # type: ignore
+            mapping={k: v for k, v in anns.items() if k not in optional},
+            optional={k: v for k, v in anns.items() if k in optional},
+        )
     # 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.


Test patch:
diff --git a/hypothesis-python/tests/cover/test_lookup_py38.py b/hypothesis-python/tests/cover/test_lookup_py38.py
--- a/hypothesis-python/tests/cover/test_lookup_py38.py
+++ b/hypothesis-python/tests/cover/test_lookup_py38.py
@@ -22,6 +22,7 @@
 from hypothesis.strategies import from_type
 
 from tests.common.debug import find_any
+from tests.common.utils import temp_registered
 
 
 @given(st.data())
@@ -113,3 +114,9 @@ class Node:
 @given(st.builds(Node))
 def test_can_resolve_recursive_dataclass(val):
     assert isinstance(val, Node)
+
+
+def test_can_register_new_type_for_typeddicts():
+    sentinel = object()
+    with temp_registered(C, st.just(sentinel)):
+        assert st.from_type(C).example() is sentinel