From 92b5d0c6d47f3d9a0cc97fb1397d14fb628ffa3f Mon Sep 17 00:00:00 2001 From: PrettyWood Date: Mon, 1 Mar 2021 12:55:10 +0100 Subject: [PATCH 1/4] fix: support properly `Enum` when combined with generic models --- changes/2436-PrettyWood.md | 1 + pydantic/generics.py | 3 ++- tests/test_generics.py | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 changes/2436-PrettyWood.md diff --git a/changes/2436-PrettyWood.md b/changes/2436-PrettyWood.md new file mode 100644 index 0000000000..4a528a2445 --- /dev/null +++ b/changes/2436-PrettyWood.md @@ -0,0 +1 @@ +Support properly `Enum` when combined with generic models diff --git a/pydantic/generics.py b/pydantic/generics.py index eeebeb0278..19d082f8dc 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -1,5 +1,6 @@ import sys import typing +from enum import Enum from typing import ( TYPE_CHECKING, Any, @@ -211,7 +212,7 @@ def iter_contained_typevars(v: Any) -> Iterator[TypeVarType]: 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, Iterable) and not lenient_issubclass(v, Enum): 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..0f46abd4a6 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -1039,3 +1039,21 @@ 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 From 5c5c106f7cdfd7f1b8f6417fa30e82fa6418d9ed Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 2 Mar 2021 18:50:58 +0000 Subject: [PATCH 2/4] whitelist iterables --- pydantic/generics.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pydantic/generics.py b/pydantic/generics.py index 19d082f8dc..ad224a477a 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -1,13 +1,11 @@ import sys import typing -from enum import Enum from typing import ( TYPE_CHECKING, Any, ClassVar, Dict, Generic, - Iterable, Iterator, List, Mapping, @@ -206,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) and not lenient_issubclass(v, Enum): + elif isinstance(v, (DictValues, list)): for var in v: yield from iter_contained_typevars(var) else: From 58d19115c2130f98664ffaee1487aaf8606f0f19 Mon Sep 17 00:00:00 2001 From: PrettyWood Date: Tue, 2 Mar 2021 21:46:56 +0100 Subject: [PATCH 3/4] update change description --- changes/2436-PrettyWood.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/2436-PrettyWood.md b/changes/2436-PrettyWood.md index 4a528a2445..44d63fb304 100644 --- a/changes/2436-PrettyWood.md +++ b/changes/2436-PrettyWood.md @@ -1 +1 @@ -Support properly `Enum` when combined with generic models +Avoid `RecursionError` when using some types like `Enum` or `Literal` with generic models \ No newline at end of file From f5c24cbca4a6d56b1f96b318b9ef293a894607be Mon Sep 17 00:00:00 2001 From: PrettyWood Date: Tue, 2 Mar 2021 22:06:41 +0100 Subject: [PATCH 4/4] add test for Literal --- tests/test_generics.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_generics.py b/tests/test_generics.py index 0f46abd4a6..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 @@ -1057,3 +1058,16 @@ class MyModel(BaseModel): 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'}}