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

Handle X | Y union in GenericModel #4977

Merged
merged 5 commits into from Feb 6, 2023
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/4146-thenx.md
@@ -0,0 +1 @@
Fix `X | Y` union syntax breaking `GenericModel`
8 changes: 8 additions & 0 deletions pydantic/generics.py
@@ -1,4 +1,5 @@
import sys
import types
import typing
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -26,6 +27,9 @@
from .typing import display_as_type, get_all_type_hints, get_args, get_origin, typing_base
from .utils import LimitedDict, all_identical, lenient_issubclass

if sys.version_info >= (3, 10):
from typing import _UnionGenericAlias

GenericModelT = TypeVar('GenericModelT', bound='GenericModel')
TypeVarType = Any # since mypy doesn't allow the use of TypeVar as a type

Expand Down Expand Up @@ -268,6 +272,10 @@ def replace_types(type_: Any, type_map: Mapping[Any, Any]) -> Any:
# See: https://www.python.org/dev/peps/pep-0585
origin_type = getattr(typing, type_._name)
assert origin_type is not None
# PEP-604 syntax (Ex.: list | str) is represented with a types.UnionType object that does not have __getitem__.
# We also cannot use isinstance() since we have to compare types.
if sys.version_info >= (3, 10) and origin_type is types.UnionType: # noqa: E721
return _UnionGenericAlias(origin_type, resolved_type_args)
return origin_type[resolved_type_args]

# We handle pydantic generic models separately as they don't have the same
Expand Down
104 changes: 104 additions & 0 deletions tests/test_generics.py
Expand Up @@ -735,6 +735,27 @@ class Model(GenericModel, Generic[t]):
assert type(float_or_int_model(data='1').data) is float


@pytest.mark.skipif(sys.version_info < (3, 10), reason='pep-604 syntax (Ex.: list | int) was added in python3.10')
def test_generic_model_caching_detect_order_of_union_args_basic_with_pep_604_syntax(create_module):
# Basic variant of https://github.com/pydantic/pydantic/issues/4474 with pep-604 syntax.
@create_module
def module():
from typing import Generic, TypeVar

from pydantic.generics import GenericModel

t = TypeVar('t')

class Model(GenericModel, Generic[t]):
data: t

int_or_float_model = Model[int | float]
float_or_int_model = Model[float | int]

assert type(int_or_float_model(data='1').data) is int
assert type(float_or_int_model(data='1').data) is float


@pytest.mark.skip(
reason="""
Depends on similar issue in CPython itself: https://github.com/python/cpython/issues/86483
Expand Down Expand Up @@ -854,6 +875,27 @@ class Model(GenericModel, Generic[T]):
assert replace_types(list[Union[str, list, T]], {T: int}) == list[Union[str, list, int]]


@pytest.mark.skipif(sys.version_info < (3, 10), reason='pep-604 syntax (Ex.: list | int) was added in python3.10')
def test_replace_types_with_pep_604_syntax() -> None:
T = TypeVar('T')

class Model(GenericModel, Generic[T]):
a: T

assert replace_types(T | None, {T: int}) == int | None
assert replace_types(T | int | str, {T: float}) == float | int | str
assert replace_types(list[T] | None, {T: int}) == list[int] | None
assert replace_types(List[str | list | T], {T: int}) == List[str | list | int]
assert replace_types(list[str | list | T], {T: int}) == list[str | list | int]
assert replace_types(list[str | list | list[T]], {T: int}) == list[str | list | list[int]]
assert replace_types(list[Model[T] | None] | None, {T: T}) == list[Model[T] | None] | None
assert (
replace_types(T | list[T | list[T | list[T | None] | None] | None] | None, {T: int})
== int | list[int | list[int | list[int | None] | None] | None] | None
)
assert replace_types(list[list[list[T | None]]], {T: int}) == list[list[list[int | None]]]


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

Expand Down Expand Up @@ -916,6 +958,68 @@ class NormalModel(BaseModel):
assert inner_model.__concrete__ is True


@pytest.mark.skipif(sys.version_info < (3, 10), reason='pep-604 syntax (Ex.: list | int) was added in python3.10')
def test_wrapping_resolved_generic_with_pep_604_syntax() -> None:
T = TypeVar('T')

class InnerModel(GenericModel, Generic[T]):
generic: list[T] | None

class OuterModel(BaseModel):
wrapper: InnerModel[int]

with pytest.raises(ValidationError):
OuterModel(wrapper={'generic': ['string_instead_of_int']})
assert OuterModel(wrapper={'generic': [1]}).dict() == {'wrapper': {'generic': [1]}}


@pytest.mark.skipif(sys.version_info < (3, 10), reason='pep-604 syntax (Ex.: list | int) was added in python3.10')
def test_type_propagation_in_deep_generic_with_pep_604_syntax() -> None:
T = TypeVar('T')

class InnerModel(GenericModel, Generic[T]):
generic: list[T] | None

class OuterModel(GenericModel, Generic[T]):
wrapper: InnerModel[T] | None

with pytest.raises(ValidationError):
OuterModel[int](wrapper={'generic': ['string_instead_of_int']})
assert OuterModel[int](wrapper={'generic': [1]}) == {'wrapper': {'generic': [1]}}


@pytest.mark.skipif(sys.version_info < (3, 10), reason='pep-604 syntax (Ex.: list | int) was added in python3.10')
def test_deep_generic_with_pep_604_syntax() -> None:
T = TypeVar('T')
S = TypeVar('S')
R = TypeVar('R')

class OuterModel(GenericModel, Generic[T, S, R]):
a: Dict[R, list[T] | None]
b: S | R | None
c: R
d: float

class InnerModel(GenericModel, Generic[T, R]):
c: list[T] | None
d: list[R] | None

class NormalModel(BaseModel):
e: int
f: str

inner_model = InnerModel[int, str]
generic_model = OuterModel[inner_model, NormalModel, int]

inner_models = [inner_model(c=[1], d=['a'])]
generic_model(a={1: inner_models, 2: None}, b=None, c=1, d=1.5)
generic_model(a={}, b=NormalModel(e=1, f='a'), c=1, d=1.5)
generic_model(a={}, b=1, c=1, d=1.5)

assert InnerModel.__concrete__ is False
assert inner_model.__concrete__ is True


def test_deep_generic_with_inner_typevar():
T = TypeVar('T')

Expand Down