Repository: pydantic/pydantic

Issue:
X | Y union syntax breaks GenericModel
# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.9.1
            pydantic compiled: True
                 install path: /home/paystone/.local/lib/python3.10/site-packages/pydantic
               python version: 3.10.2 (main, Jan 29 2022, 02:55:36) [GCC 10.2.1 20210110]
                     platform: Linux-5.18.2-arch1-1-x86_64-with-glibc2.31
     optional deps. installed: ['typing-extensions']
```

Python 3.10 incorporated [PEP 604](https://peps.python.org/pep-0604/), which added syntax for writing Unions as `X | Y`, instead of `typing.Union[X, Y]`. It seems that when this syntax is used in combination with TypeVars as part of a `GenericModel`, the binding of TypeVars to their types fails.

Below is first a working example using `typing.Union`, followed by a non-working example using `|` syntax, with the stack trace. The example seeks to construct a `Relationship` GenericModel, and a `BidirectionalMultiRelationship` model which contains a list of either `Relationship[A, B]` or `Relationship[B, A]` models:

```py
from typing import Union, Generic, TypeVar
from pydantic.generics import GenericModel

SourceT = TypeVar("SourceT")
TargetT = TypeVar("TargetT")

class Relationship(GenericModel, Generic[SourceT, TargetT]):
    source: SourceT
    target: TargetT

class BidirectionalMultiRelationship(GenericModel, Generic[SourceT, TargetT]):
    relationships: list[Union[Relationship[SourceT, TargetT], Relationship[TargetT, SourceT]]]
    
BidirectionalMultiRelationship[str, int]
```

```py
from typing import Generic, TypeVar
from pydantic.generics import GenericModel

SourceT = TypeVar("SourceT")
TargetT = TypeVar("TargetT")

class Relationship(GenericModel, Generic[SourceT, TargetT]):
    source: SourceT
    target: TargetT

class BidirectionalMultiRelationship(GenericModel, Generic[SourceT, TargetT]):
    relationships: list[Relationship[SourceT, TargetT] | Relationship[TargetT, SourceT]]
    
BidirectionalMultiRelationship[str, int]
```

```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [2], in <cell line: 14>()
     11 class BidirectionalMultiRelationship(GenericModel, Generic[SourceT, TargetT]):
     12     relationships: Relationship[SourceT, TargetT] | Relationship[TargetT, SourceT]
---> 14 BidirectionalMultiRelationship[str, int]

File ~/.local/lib/python3.10/site-packages/pydantic/generics.py:137, in GenericModel.__class_getitem__(cls, params)
    133     _generic_types_cache[(cls, params[0])] = created_model
    135 # Recursively walk class type hints and replace generic typevars
    136 # with concrete types that were passed.
--> 137 _prepare_model_fields(created_model, fields, instance_type_hints, typevars_map)
    139 return created_model

File ~/.local/lib/python3.10/site-packages/pydantic/generics.py:356, in _prepare_model_fields(created_model, fields, instance_type_hints, typevars_map)
    353 assert field.type_.__class__ is DeferredType, field.type_.__class__
    355 field_type_hint = instance_type_hints[key]
--> 356 concrete_type = replace_types(field_type_hint, typevars_map)
    357 field.type_ = concrete_type
    358 field.outer_type_ = concrete_type

File ~/.local/lib/python3.10/site-packages/pydantic/generics.py:263, in replace_types(type_, type_map)
    261         origin_type = getattr(typing, type_._name)
    262     assert origin_type is not None
--> 263     return origin_type[resolved_type_args]
    265 # We handle pydantic generic models separately as they don't have the same
    266 # semantics as "typing" classes or generic aliases
    267 if not origin_type and lenient_issubclass(type_, GenericModel) and not type_.__concrete__:

TypeError: 'type' object is not subscriptable
```

On inspection I found that at the point that the exception is thrown, `origin_type` is `types.UnionType`, where it should be `Relationship`.

Apologies if I missed an existing issue for this. I couldn't find anything in my search.


Discussion:
Same thing with subscriptable types, with somehow simpler example:

```python
# 1.py
from __future__ import annotations

from pydantic import BaseModel

class Model(BaseModel):
    foo: list[str]
```

```
% python3.8 1.py
Traceback (most recent call last):
  File "1.py", line 5, in <module>
    class Model(BaseModel):
  File "pydantic/main.py", line 188, in pydantic.main.ModelMetaclass.__new__
  File "pydantic/typing.py", line 419, in pydantic.typing.resolve_annotations
    For example:
  File "/usr/local/lib/python3.8/typing.py", line 270, in _eval_type
    return t._evaluate(globalns, localns)
  File "/usr/local/lib/python3.8/typing.py", line 518, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
TypeError: 'type' object is not subscriptable
```

Obviously, python3.8 cannot evaluate `list[str]` here: while `from __future__ import annotations` (which is _postponed evaluation of annotations_) allows to use it in _annotations_, it's not a valid construct in python < 3.9 and cannot be evaluated directly. Thus I'm not sure if it's even fixable. The solution is to either use newer python, or use `typing.List` instead of `list` in annotations.
While i believe https://github.com/pydantic/pydantic/issues/4146#issuecomment-1156371670 is answered in https://github.com/pydantic/pydantic/issues/2314#issuecomment-772285980, I'd love to bump the original issue with a slightly reduced example.

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.10.4
            pydantic compiled: True
                 install path: {project_path}/.venv/lib/python3.10/site-packages/pydantic
               python version: 3.10.8 (main, Nov 18 2022, 12:43:38) [Clang 14.0.0 (clang-1400.0.29.102)]
                     platform: macOS-13.1-arm64-arm-64bit
     optional deps. installed: ['dotenv', 'email-validator', 'typing-extensions']
```

```py
from typing import Generic, TypeVar

import pydantic
from pydantic import generics

DataT = TypeVar("DataT")

class MyGenericModel(generics.GenericModel, Generic[DataT]):
    generic: list[DataT] | None


class A(pydantic.BaseModel):
    wrapper: MyGenericModel[int]
```
results in
```
Traceback (most recent call last):
  File "/<>/.pyenv/versions/3.10.8/lib/python3.10/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 12, in <module>
  File "<input>", line 13, in A
  File "<>/.venv/lib/python3.10/site-packages/pydantic/generics.py", line 145, in __class_getitem__
    _prepare_model_fields(created_model, fields, instance_type_hints, typevars_map)
  File "<>/.venv/lib/python3.10/site-packages/pydantic/generics.py", line 364, in _prepare_model_fields
    concrete_type = replace_types(field_type_hint, typevars_map)
  File "<>/.venv/lib/python3.10/site-packages/pydantic/generics.py", line 271, in replace_types
    return origin_type[resolved_type_args]
TypeError: 'type' object is not subscriptable
```

The same with Union
```py
from typing import Generic, TypeVar, Union

import pydantic
from pydantic import generics

DataT = TypeVar("DataT")

class MyGenericModel(generics.GenericModel, Generic[DataT]):
    generic: Union[list[DataT], None]


class A(pydantic.BaseModel):
    wrapper: MyGenericModel[int]
```
seems to be working without the issue.
Confirmed, should be fixed in V2, @dmontagu might be worth checking this is fixed in #4970.

Happy to review a PR to fix this in V1.10, but I suspect it's not trivial.
A slightly more reduced example would be:
```py
from pydantic import generics

generics.replace_types(list[str] | None, {str: int})
```

I guess the problem is that `type(list | int)` is `types.UnionType` and a dum-dum approach could be to translate `types.UnionType` back to `typing.Union` or just to do smth like `functools.reduce(operator.or_, type_args)` if `origin_type` is `types.UnionType`?


For what it's worth, on the #4970 PR, I checked that the following code runs without issue (on Python 3.11 anyway):
```python
from typing import Generic, TypeVar

import pydantic

DataT = TypeVar("DataT")

class MyGenericModel(pydantic.BaseModel, Generic[DataT]):
    generic: list[DataT] | None

class A(pydantic.BaseModel):
    wrapper: MyGenericModel[int]

m = MyGenericModel[int](generic=None)
print(A(wrapper=m))
# wrapper=MyGenericModel[int](generic=None)
```

Resolving patch:
diff --git a/pydantic/generics.py b/pydantic/generics.py
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -1,4 +1,5 @@
 import sys
+import types
 import typing
 from typing import (
     TYPE_CHECKING,
@@ -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
 
@@ -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


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -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
@@ -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."""
 
@@ -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')