diff --git a/changes/2436-PrettyWood.md b/changes/2436-PrettyWood.md new file mode 100644 index 0000000000..44d63fb304 --- /dev/null +++ b/changes/2436-PrettyWood.md @@ -0,0 +1 @@ +Avoid `RecursionError` when using some types like `Enum` or `Literal` with generic models \ No newline at end of file diff --git a/pydantic/generics.py b/pydantic/generics.py index eeebeb0278..ad224a477a 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -6,7 +6,6 @@ ClassVar, Dict, Generic, - Iterable, Iterator, List, Mapping, @@ -205,13 +204,16 @@ def check_parameters_count(cls: Type[GenericModel], parameters: Tuple[Any, ...]) raise TypeError(f'Too {description} parameters for {cls.__name__}; actual {actual}, expected {expected}') +DictValues: Type[Any] = {}.values().__class__ + + def iter_contained_typevars(v: Any) -> Iterator[TypeVarType]: """Recursively iterate through all subtypes and type args of `v` and yield any typevars that are found.""" if isinstance(v, TypeVar): yield v elif hasattr(v, '__parameters__') and not get_origin(v) and lenient_issubclass(v, GenericModel): yield from v.__parameters__ - elif isinstance(v, Iterable): + elif isinstance(v, (DictValues, list)): for var in v: yield from iter_contained_typevars(var) else: diff --git a/tests/test_generics.py b/tests/test_generics.py index 4728188a03..d1e42d8666 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -3,6 +3,7 @@ from typing import Any, Callable, ClassVar, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, Union import pytest +from typing_extensions import Literal from pydantic import BaseModel, Field, ValidationError, root_validator, validator from pydantic.generics import GenericModel, _generic_types_cache, iter_contained_typevars, replace_types @@ -1039,3 +1040,34 @@ class Model2(GenericModel, Generic[T]): Model2 = module.Model2 result = Model1[str].parse_obj(dict(ref=dict(ref=dict(ref=dict(ref=123))))) assert result == Model1(ref=Model2(ref=Model1(ref=Model2(ref='123')))) + + +@skip_36 +def test_generic_enum(): + T = TypeVar('T') + + class SomeGenericModel(GenericModel, Generic[T]): + some_field: T + + class SomeStringEnum(str, Enum): + A = 'A' + B = 'B' + + class MyModel(BaseModel): + my_gen: SomeGenericModel[SomeStringEnum] + + m = MyModel.parse_obj({'my_gen': {'some_field': 'A'}}) + assert m.my_gen.some_field is SomeStringEnum.A + + +@skip_36 +def test_generic_literal(): + FieldType = TypeVar('FieldType') + ValueType = TypeVar('ValueType') + + class GModel(GenericModel, Generic[FieldType, ValueType]): + field: Dict[FieldType, ValueType] + + Fields = Literal['foo', 'bar'] + m = GModel[Fields, str](field={'foo': 'x'}) + assert m.dict() == {'field': {'foo': 'x'}}