Pydantic: Repeatedly subclassing a GenericModel with a Constrained Type fails

Created on 29 Oct 2020  ยท  5Comments  ยท  Source: samuelcolvin/pydantic

Checks

  • [x] I added a descriptive title to this issue
  • [x] I have searched (google, github) for similar issues and couldn't find anything
  • [x] I have read and followed the docs and still think this is a bug

Bug

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)]
bug

Most helpful comment

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).

All 5 comments

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!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Yolley picture Yolley  ยท  18Comments

DrPyser picture DrPyser  ยท  19Comments

sm-Fifteen picture sm-Fifteen  ยท  45Comments

rrbarbosa picture rrbarbosa  ยท  35Comments

demospace picture demospace  ยท  26Comments