Repository: HypothesisWorks/hypothesis

Issue:
Registering strategies for parametrised generic types behaves differently for pydantic.generics.GenericModel
The Hypothesis docs say that you may not register type strategies for parametrised generic types such as `MyCollection[int]` [1]. As expected, we see an error when trying this out:
```python
from typing import Generic, TypeVar, List
import hypothesis.strategies as st

T = TypeVar("T")
class MyCollection(Generic[T]):
    def __init__(self, items: List[T]):
        self.items = items

st.register_type_strategy(MyCollection[int], st.lists(st.integers()).map(lambda x: MyCollection(x)))
```
> InvalidArgument: Cannot register generic type MyCollection[int], because it has type arguments which would not be handled.  Instead, register a function for <class 'MyCollection'> which can inspect specific type objects and return a strategy.

---

However, the behaviour is different and surprising when using Pydantic `GenericModel`s. Specifically:
1. It seems it's _not_ possible to register a dynamic `lambda T: ...` strategy for `MyCollection`.
2. In contrast to what the docs say, it seems it _is_ possible to register a strategy for a specific `MyCollection[T]`.
```python
from typing import Generic, TypeVar, List
from pydantic.generics import GenericModel
import hypothesis.strategies as st

T = TypeVar("T")
class MyCollection(GenericModel, Generic[T]):
    items: List[T]

def select_strategy(type_):
    print(type_)
    raise ValueError("This is never reached")

# Try registering a generic strategy for all `MyCollection[T]`.
st.register_type_strategy(MyCollection, select_strategy)
st.from_type(MyCollection[int]).example()  # Surprise 1: `select_strategy` is not called!

# Try registering a strategy for a specific `MyCollection[int]`.
st.register_type_strategy(MyCollection[int], st.lists(st.booleans()).map(lambda x: MyCollection(items=x)))
st.from_type(MyCollection[int]).example()  # Surprise 2: this uses the registered strategy!
```


[1] https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.register_type_strategy


Discussion:
Thanks for reporting this - I'm sure we can fix it with some more tricky introspection 😅 

---------------------

And FYI the two strategies below are equivalent; the latter idiom is often easier to read.

```python
st.lists(st.booleans()).map(lambda x: MyCollection(items=x))
st.builds(MyCollection, items=st.lists(st.booleans()))
```

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
@@ -1810,6 +1810,19 @@ def register_type_strategy(
             f"for {origin!r} which can inspect specific type objects and return a "
             "strategy."
         )
+    if (
+        "pydantic.generics" in sys.modules
+        and issubclass(custom_type, sys.modules["pydantic.generics"].GenericModel)
+        and not re.search(r"[A-Za-z_]+\[.+\]", repr(custom_type))
+        and callable(strategy)
+    ):  # pragma: no cover
+        # See https://github.com/HypothesisWorks/hypothesis/issues/2940
+        raise InvalidArgument(
+            f"Cannot register a function for {custom_type!r}, because parametrized "
+            "`pydantic.generics.GenericModel` subclasses aren't actually generic "
+            "types at runtime.  In this case, you should register a strategy "
+            "directly for each parametrized form that you anticipate using."
+        )
 
     types._global_type_lookup[custom_type] = strategy
     from_type.__clear_cache()  # type: ignore


Test patch: