Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Registering strategies for parametrised generic types behaves differently for pydantic.generics.GenericModel #2940

Closed
lsorber opened this issue Apr 16, 2021 · 4 comments · Fixed by #3291
Labels
interop how to play nicely with other packages legibility make errors helpful and Hypothesis grokable

Comments

@lsorber
Copy link

lsorber commented Apr 16, 2021

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:

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 GenericModels. 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].
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

@Zac-HD Zac-HD added the interop how to play nicely with other packages label Apr 17, 2021
@Zac-HD
Copy link
Member

Zac-HD commented Apr 17, 2021

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.

st.lists(st.booleans()).map(lambda x: MyCollection(items=x))
st.builds(MyCollection, items=st.lists(st.booleans()))

@Zac-HD
Copy link
Member

Zac-HD commented Apr 13, 2022

I've looked into this at some length, and I don't think we actually can support it 😕

  • Hypothesis can only do the invoke-a-function resolution when we can access the un-parametrized generic type (plus the type params).
  • but, Pydantic doesn't keep that around - instead, pydantic.generics.GenericModel.__class_getitem__ creates a new child class which doesn't seem to store it's type arguments anywhere.
  • st.from_type() can't feasibly resolve child classes using things registered for a parent class; that way lies madness. I'm also unwilling to build so many special cases just for pydantic into the core of Hypothesis.

Possible solutions, from best to worst:

  1. If GenericModel also inherited from types.GenericAlias, I think everything would probably Just Work(tm), at least on Python 3.9.2 and later - Hypothesis would see that we didn't know the class, but that it was a parametrized generic, grab the origin and args types, and there we go.
  2. ✅ Add an explicit warning (or error) if you register a function for a GenericModel, since we know this isn't going to work out later.
    (better errors are the special case I'm most willing to grant, since there's no API compatibility committment.)

@lsorber
Copy link
Author

lsorber commented Apr 21, 2022

Thanks for looking into this @Zac-HD. Would it be reasonable to ask Pydantic to inherit from types.GenericAlias? If so I can open an issue there.

@Zac-HD
Copy link
Member

Zac-HD commented Apr 21, 2022

Yeah, I think that would make sense for generic types - and there's a decent chance that it would be pretty easy to implement too 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interop how to play nicely with other packages legibility make errors helpful and Hypothesis grokable
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants