Output of python -c "import pydantic.utils; print(pydantic.utils.version_info())"::
pydantic version: 1.7.1
pydantic compiled: False
install path: /home/al459/envs/pydantic/pydantic
python version: 3.9.0 (default, Oct 27 2020, 13:45:54) [GCC 9.3.0]
platform: Linux-5.4.0-52-generic-x86_64-with-glibc2.31
optional deps. installed: []
I would like to create a generic model and subclass it with a constrained string type but I get a TypeError: 'GenericBaseModel[ConstrainedStrValue]' already defined above, please consider reusing if I test the script below:
from typing import Generic, TypeVar
from pydantic import constr
from pydantic.generics import GenericModel
T = TypeVar("T")
class GenericBaseModel(GenericModel, Generic[T]):
data: T
str20 = GenericBaseModel[constr(max_length=20)]
str10 = GenericBaseModel[constr(max_length=10)]
Error:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ERROR collecting tests/test_custom.py โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
NameError: Name conflict: 'GenericBaseModel[ConstrainedStrValue]' in 'tests.test_custom' is already used by <class 'te
sts.test_custom.GenericBaseModel[ConstrainedStrValue]'>
The above exception was the direct cause of the following exception:
tests/test_custom.py:14: in <module>
ConstrB = GenericBaseModel[constr(max_length=10)]
pydantic/generics.py:82: in __class_getitem__
raise TypeError(f'{model_name!r} already defined above, please consider reusing it') from NameError(
E TypeError: 'GenericBaseModel[ConstrainedStrValue]' already defined above, please consider reusing it
The strange thing is if I wrap it in a function and test it the test passes:
def test_generic_subclass_with_constrained_field():
T = TypeVar("T")
class GenericBaseModel(GenericModel, Generic[T]):
data: T
str20 = GenericBaseModel[constr(max_length=20)]
str10 = GenericBaseModel[constr(max_length=10)]
Just tried to reproduce your problem and I can. It's also not fixed by https://github.com/samuelcolvin/pydantic/pull/1989 (checked it there as well).
The problem with the "test" actually makes sense. There's special code in the generic module for handling calls "from modules" compared to within functions. This was intended to help with pickling as far as I know but there might be a problem with it.
This will affect more than just the constrained strings. I will try an outline the Problem:
In order to pickle models, any model that is created on a module level is saved to the module by name. This means that if two models have the same name but are different objects they will have this problem.
So the bug to fix is how to consistently name generic types so that models that do different things (like the two constrained string models with different arguments) do not get the same name.
For a really quick hacky workaround:
An easy workaround for you is to just make sure all the subclasses are named differently (assuming you don't want to pickle stuff):
from pydantic import constr
from pydantic.generics import GenericModel
from uuid import uuid4
T = TypeVar("T")
class GenericBaseModel(GenericModel, Generic[T]):
data: T
@classmethod
def __concrete_name__(cls, params): # this names
s = super().__concrete_name__(params)
return s + str(uuid4()) # obviously if there's a smarter way of naming your classes then that's better, this is just an example of a random string
str20 = GenericBaseModel[constr(max_length=20)]
str10 = GenericBaseModel[constr(max_length=10)]
A better solution would be this:
class Str20(ConstrainedStr):
max_length = 20
class Str10(ConstrainedStr):
max_length = 10
class GenericBaseModel(GenericModel, Generic[T]):
data: T
str20 = GenericBaseModel[Str20]
str10 = GenericBaseModel[Str10]
Declaring the types that way names them differently, which should give you pickle support and make mypy happier (assuming the max lengths aren't completely dynamic).
Hi @lerooze
Are you happy with @daviskirk's answer? (thanks btw!)
I guess we could generate a class name based on parameters in constr function of even a unique one but it feels like a pointless overhead. Using ConstrainedStr directly seems like the way to go
Hi @PrettyWood ,
Yes I'm very happy with @daviskirk 's answer.
Thanks to @samuelcolvin and to everyone for the great package!
Most helpful comment
A better solution would be this:
Declaring the types that way names them differently, which should give you pickle support and make mypy happier (assuming the max lengths aren't completely dynamic).