Skip to content

Commit

Permalink
fix: avoid RecursionError when using some types like Enum or `Lit…
Browse files Browse the repository at this point in the history
…eral` with generic models (#2438)

* fix: support properly `Enum` when combined with generic models

* whitelist iterables

* update change description

* add test for Literal

Co-authored-by: Samuel Colvin <s@muelcolvin.com>
  • Loading branch information
PrettyWood and samuelcolvin committed Mar 3, 2021
1 parent 429b439 commit ab69114
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 2 deletions.
1 change: 1 addition & 0 deletions changes/2436-PrettyWood.md
@@ -0,0 +1 @@
Avoid `RecursionError` when using some types like `Enum` or `Literal` with generic models
6 changes: 4 additions & 2 deletions pydantic/generics.py
Expand Up @@ -6,7 +6,6 @@
ClassVar,
Dict,
Generic,
Iterable,
Iterator,
List,
Mapping,
Expand Down Expand Up @@ -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:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_generics.py
Expand Up @@ -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
Expand Down Expand Up @@ -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'}}

0 comments on commit ab69114

Please sign in to comment.