Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support user defined generic field types in generic models for python < 3.9. #2554

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/2465-daviskirk.md
@@ -0,0 +1 @@
Support user defined generic field types in generic models.
7 changes: 6 additions & 1 deletion pydantic/generics.py
Expand Up @@ -172,7 +172,12 @@ def replace_types(type_: Any, type_map: Mapping[Any, Any]) -> Any:
# If all arguments are the same, there is no need to modify the
# type or create a new object at all
return type_
if origin_type is not None and isinstance(type_, typing_base) and not isinstance(origin_type, typing_base):
if (
origin_type is not None
and isinstance(type_, typing_base)
and not isinstance(origin_type, typing_base)
and getattr(type_, '_name', None) is not None
):
# In python < 3.9 generic aliases don't exist so any of these like `list`,
# `type` or `collections.abc.Callable` need to be translated.
# See: https://www.python.org/dev/peps/pep-0585
Expand Down
58 changes: 57 additions & 1 deletion tests/test_generics.py
@@ -1,6 +1,20 @@
import sys
from enum import Enum
from typing import Any, Callable, ClassVar, Dict, Generic, List, Optional, Sequence, Tuple, Type, TypeVar, Union
from typing import (
Any,
Callable,
ClassVar,
Dict,
Generic,
List,
Mapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
)

import pytest
from typing_extensions import Annotated, Literal
Expand Down Expand Up @@ -808,6 +822,30 @@ class Model(GenericModel, Generic[T]):
assert replace_types(list[Union[str, list, T]], {T: int}) == list[Union[str, list, int]]


@skip_36
def test_replace_types_with_user_defined_generic_type_field():
"""Test that using user defined generic types as generic model fields are handled correctly."""

T = TypeVar('T')
KT = TypeVar('KT')
VT = TypeVar('VT')

class GenericMapping(Mapping[KT, VT]):
pass

class GenericList(List[T]):
pass

class Model(GenericModel, Generic[T, KT, VT]):

map_field: GenericMapping[KT, VT]
list_field: GenericList[T]

assert replace_types(Model, {T: bool, KT: str, VT: int}) == Model[bool, str, int]
assert replace_types(Model[T, KT, VT], {T: bool, KT: str, VT: int}) == Model[bool, str, int]
assert replace_types(Model[T, VT, KT], {T: bool, KT: str, VT: int}) == Model[T, VT, KT][bool, int, str]


@skip_36
def test_replace_types_identity_on_unchanged():
T = TypeVar('T')
Expand Down Expand Up @@ -1073,6 +1111,24 @@ class GModel(GenericModel, Generic[FieldType, ValueType]):
assert m.dict() == {'field': {'foo': 'x'}}


@skip_36
def test_generic_with_user_defined_generic_field():
T = TypeVar('T')

class GenericList(List[T]):
pass

class Model(GenericModel, Generic[T]):

field: GenericList[T]

model = Model[int](field=[5])
assert model.field[0] == 5

with pytest.raises(ValidationError):
model = Model[int](field=['a'])


@skip_36
def test_generic_annotated():
T = TypeVar('T')
Expand Down