Repository: pydantic/pydantic
Pull request: Fix `mro` of generic subclass 
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->

## Change Summary

Override `ModelMetaclass.mro` such that indexed generic type can be inserted into `__mro__` of its subclasses.

For example:
```py
class A(BaseModel, Generic[T]): ...
class B(A[T]): ...
class C(B[bool]): ...

# before
assert C.__mro__ == (C, B[bool], B, A, BaseModel, Generic, object)
# after
assert C.__mro__ == (C, B[bool], B, A[bool], A, BaseModel, Generic, object)
```
This allow `C()` to be assigned to `A[bool]`.

I have checked all testcases in `test_generic.py`. This change doesn't break existing tests. Also, I collected all the tests where `__mro__` is affected by this change, and add them into new tests. 

<!-- Please give a short summary of the changes. -->

## Related issue number
fix #10039 
<!-- WARNING: please use "fix #123" style references so the issue is closed when this PR is merged. -->

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [x] Documentation reflects the changes where applicable
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Problem statement:
Nested generic inheritance
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Is there a way for `MyClassSubBool` to be recognized as an instance of `MyClass[bool]` , apart from inheriting the functionality from `MyClassSub`?

If I use multiple inheritance in `class MyClassSubBool(MyClassSub, MyClass[bool])` I do get the example to work, but it doesn't seem the most pythonic way.

Thanks for your help.

### Example Code

```Python
from typing import Generic, TypeVar

from pydantic import BaseModel

T = TypeVar('T')


class MyClass(BaseModel, Generic[T]):
    pass


class MyClassSub(MyClass[T]):
    pass


class MyClassSubBool(MyClassSub[bool]):
    pass


class Model(BaseModel):
    input_bool: MyClass[bool]


if __name__ == '__main__':
    instance_fails = Model(input_bool=MyClassSubBool())
```


### Python, Pydantic & OS Version

```Text
pydantic version: 2.7.3
        pydantic-core version: 2.18.4
          pydantic-core build: profile=release pgo=true
               python version: 3.11.4 (main, Sep 12 2023, 11:07:53) [GCC 11.4.0]
                     platform: Linux-6.5.0-44-generic-x86_64-with-glibc2.35
             related packages: typing_extensions-4.10.0
                       commit: unknown
```

Hints from issue discussion:
I simplified your example code to make it easier to understand. Overall I think this is a reasonable request, it is accepted from a type checking perspective.

Commit message: update

Patch to review:
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
index 886e6fd5f47..06689b64240 100644
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -10,7 +10,7 @@
 from abc import ABCMeta
 from functools import lru_cache, partial
 from types import FunctionType
-from typing import Any, Callable, Generic, NoReturn
+from typing import Any, Callable, Generic, NoReturn, TypeVar
 
 import typing_extensions
 from pydantic_core import PydanticUndefined, SchemaSerializer
@@ -115,6 +115,8 @@ def wrapped_model_post_init(self: BaseModel, context: Any, /) -> None:
 
             namespace['__class_vars__'] = class_vars
             namespace['__private_attributes__'] = {**base_private_attributes, **private_attributes}
+            if __pydantic_generic_metadata__:
+                namespace['__pydantic_generic_metadata__'] = __pydantic_generic_metadata__
 
             cls: type[BaseModel] = super().__new__(mcs, cls_name, bases, namespace, **kwargs)  # type: ignore
 
@@ -241,6 +243,61 @@ def __getattr__(self, item: str) -> Any:
                 return private_attributes[item]
             raise AttributeError(item)
 
+        def mro(cls):
+            original_mro = super().mro()
+
+            if cls.__bases__ == (object,):
+                return original_mro
+
+            generic_metadata = cls.__dict__.get('__pydantic_generic_metadata__', None)
+            if not generic_metadata:
+                return original_mro
+
+            origin, args = (
+                generic_metadata['origin'],
+                generic_metadata['args'],
+            )
+            if not origin:
+                return original_mro
+
+            target_params: tuple[TypeVar] = origin.__pydantic_generic_metadata__['parameters']
+            param_dict = dict(zip(target_params, args))
+
+            # This is necessary otherwise in some case TypeVar may be same:
+            #
+            # class A(BaseModel, Generic[T]): ...
+            # class B(A[int], Generic[T]): ...
+            # class C(B[str], Generic[T]): ...
+            #
+            key = '__pydantic_inserted_mro_origins__'
+            inserted_origins: set[tuple[type, tuple]] = getattr(origin, key, set()) | {(origin, ())}
+
+            new_mro = [original_mro[0]]
+            for base in original_mro[1:]:
+                base_params = getattr(base, '__pydantic_generic_metadata__', {}).get('parameters', ())
+                base_args = getattr(base, '__pydantic_generic_metadata__', {}).get('args', ())
+                base_origin = getattr(base, '__pydantic_generic_metadata__', {}).get('origin', None)
+
+                if base_origin is not None:
+                    inserted_origins.add((base_origin, ()))
+
+                if (base, base_args) not in inserted_origins and base_params:
+                    assert set(base_params) <= param_dict.keys(), 'Some bug occurs'
+                    new_base_args = tuple(param_dict[param] for param in base_params)
+                    new_base = base[new_base_args]  # type: ignore
+                    new_mro.append(new_base)
+                    inserted_origins.add((base, base_args))
+
+                # Avoid redundunt case such as
+                # class A(BaseModel, Generic[T]): ...
+                # A[T] is A  # True
+                if base is not new_mro[-1]:
+                    new_mro.append(base)
+
+            setattr(cls, key, inserted_origins)
+
+            return new_mro
+
     @classmethod
     def __prepare__(cls, *args: Any, **kwargs: Any) -> dict[str, object]:
         return _ModelNamespaceDict()
diff --git a/pydantic/main.py b/pydantic/main.py
index 7ef02b6bcbd..11f1ff2764e 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -725,13 +725,14 @@ def __class_getitem__(
             )  # use dict as ordered set
 
             with _generics.generic_recursion_self_type(origin, args) as maybe_self_type:
-                if maybe_self_type is not None:
-                    return maybe_self_type
-
+                # Check cached first or `mro` may return `PydanticRecursiveRef`
                 cached = _generics.get_cached_generic_type_late(cls, typevar_values, origin, args)
                 if cached is not None:
                     return cached
 
+                if maybe_self_type is not None:
+                    return maybe_self_type
+
                 # Attempt to rebuild the origin in case new types have been defined
                 try:
                     # depth 3 gets you above this __class_getitem__ call
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 2980af5d6c3..cece50740ca 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -475,7 +475,10 @@ class M(BaseModel):
         Working = B[m]
         generics.append(Working)
 
-    target_size = cache_size + count_create_models * 3 + 2
+    # A[int, C] -> 2 caches
+    # A[int, C][M] -> 3 caches  // this is generated by `mro`
+    # B[M] -> 3 caches
+    target_size = cache_size + count_create_models * 3 * 2 + 2
     assert len(_GENERIC_TYPES_CACHE) < target_size + _LIMITED_DICT_SIZE
     del models
     del generics
@@ -2875,3 +2878,186 @@ def test_generic_with_allow_extra():
     # This used to raise an error related to accessing the __annotations__ attribute of the Generic class
     class AllowExtraGeneric(BaseModel, Generic[T], extra='allow'):
         data: T
+
+
+def test_generic_mro_single():
+    T = TypeVar('T')
+
+    # 1.
+    class A(BaseModel, Generic[T]): ...
+
+    class B(A, Generic[T]): ...
+
+    assert B[int].__mro__ == (B[int], B, A[int], A, BaseModel, Generic, object)
+
+    # 2.
+    class A(BaseModel, Generic[T]): ...
+
+    class B(A[T]): ...
+
+    class C(B[int]): ...
+
+    assert B.__mro__ == (B, A, BaseModel, Generic, object)
+    assert C.__mro__ == (C, B[int], B, A[int], A, BaseModel, Generic, object)
+
+    # 3.
+    class A(BaseModel, Generic[T]): ...
+
+    class B(A[T], Generic[T]): ...
+
+    class C(B[T], Generic[T]): ...
+
+    assert B.__mro__ == (B, A, BaseModel, Generic, object)
+    assert B[int].__mro__ == (B[int], B, A[int], A, BaseModel, Generic, object)
+    assert C[int].__mro__ == (C[int], C, B[int], B, A[int], A, BaseModel, Generic, object)
+
+    # 4.
+    class A(BaseModel, Generic[T]): ...
+
+    class B(A[int], Generic[T]): ...
+
+    class C(B[str], Generic[T]): ...
+
+    assert B.__mro__ == (B, A[int], A, BaseModel, Generic, object)
+    assert C.__mro__ == (C, B[str], B, A[int], A, BaseModel, Generic, object)
+
+
+def test_generic_mro_multi():
+    T1 = TypeVar('T1')
+    T2 = TypeVar('T2')
+    T3 = TypeVar('T3')
+
+    # 1.
+    class A(BaseModel, Generic[T1, T2]): ...
+
+    class B(A[int, T2], BaseModel, Generic[T2]): ...
+
+    assert A[int, T2][str].__mro__ == (A[int, T2][str], A, BaseModel, Generic, object)
+    assert B[str].__mro__ == (B[str], B, A[int, T2][str], A[int, T2], A, BaseModel, Generic, object)
+
+    # 2.
+    class A(BaseModel, Generic[T1, T2]): ...
+
+    B = A[int, T2]
+    C = B[str]
+
+    assert B.__mro__ == (A[int, T2], A, BaseModel, Generic, object)
+    assert C.__mro__ == (B[str], A, BaseModel, Generic, object)
+
+    # 3.
+
+    class A(BaseModel, Generic[T1]): ...
+
+    class B1(A[T1], Generic[T1, T2]): ...
+
+    class B2(A[T2], Generic[T1, T2]): ...
+
+    assert B1[T1, str].__mro__ == (B1[T1, str], B1, A, BaseModel, Generic, object)
+    assert B2[T1, str].__mro__ == (B2[T1, str], B2, A[str], A[T2], A, BaseModel, Generic, object)
+
+    assert B1[T1, str][int].__mro__ == (B1[T1, str][int], B1, A[int], A, BaseModel, Generic, object)
+    assert B2[T1, str][int].__mro__ == (B2[T1, str][int], B2, A[str], A[T2], A, BaseModel, Generic, object)
+
+    assert B1[int, str].__mro__ == (B1[int, str], B1, A[int], A, BaseModel, Generic, object)
+    assert B2[int, str].__mro__ == (B2[int, str], B2, A[str], A[T2], A, BaseModel, Generic, object)
+
+    # 4.
+
+    class A(BaseModel, Generic[T1]): ...
+
+    class B(A[T2], Generic[T2]): ...
+
+    class C1(B[T3], Generic[T3, T1]): ...
+
+    class C2(B[T3], Generic[T3, T2]): ...
+
+    assert C1[int, str].__mro__ == (
+        C1[int, str],
+        C1,
+        B[int],
+        B[T3],
+        B,
+        A[int],
+        A[T3],
+        A[T2],
+        A,
+        BaseModel,
+        Generic,
+        object,
+    )
+
+    assert C2[int, str].__mro__ == (
+        C2[int, str],
+        C2,
+        B[int],
+        B[T3],
+        B,
+        A[int],
+        A[T3],
+        A[T2],
+        A,
+        BaseModel,
+        Generic,
+        object,
+    )
+
+
+def test_generic_mro_swap():
+    T1 = TypeVar('T1')
+    T2 = TypeVar('T2')
+
+    class A(BaseModel, Generic[T1]): ...
+
+    class B(A[T1], Generic[T1, T2]): ...
+
+    class BSwap(B[T2, T1], Generic[T1, T2]): ...
+
+    assert BSwap[T1, str].__mro__ == (
+        BSwap[T1, str],
+        BSwap,
+        B[str, T1],
+        B[T2, T1],
+        B,
+        A[str],
+        A[T2],
+        A,
+        BaseModel,
+        Generic,
+        object,
+    )
+
+    assert BSwap[T1, str][int].__mro__ == (
+        BSwap[T1, str][int],
+        BSwap,
+        B[str, int],
+        B[T2, T1],
+        B,
+        A[str],
+        A[T2],
+        A,
+        BaseModel,
+        Generic,
+        object,
+    )
+
+    assert BSwap[int, str].__mro__[1:] == BSwap[T1, str][int].__mro__[1:]
+
+
+def test_generic_field():
+    # https://github.com/pydantic/pydantic/issues/10039
+
+    T = TypeVar('T')
+
+    class A(BaseModel, Generic[T]): ...
+
+    class B(A[T]): ...
+
+    class C(B[bool]): ...
+
+    class Model(BaseModel):
+        input_bool: A[bool]
+
+    Model(input_bool=C())
+
+    assert C.__mro__ == (C, B[bool], B, A[bool], A, BaseModel, Generic, object)
+    assert issubclass(C, A[bool]) and issubclass(C, A[int]) is False


Code review comments:
diff_hunk: @@ -241,6 +243,61 @@ def __getattr__(self, item: str) -> Any:
                 return private_attributes[item]
             raise AttributeError(item)
 
+        def mro(cls):
+            original_mro = super().mro()
+
+            if cls.__bases__ == (object,):
+                return original_mro
+
+            generic_metadata = cls.__dict__.get('__pydantic_generic_metadata__', None)
+            if not generic_metadata:
+                return original_mro
+
+            origin, args = (
+                generic_metadata['origin'],
+                generic_metadata['args'],
+            )
+            if not origin:
+                return original_mro
+
+            target_params: tuple[TypeVar] = origin.__pydantic_generic_metadata__['parameters']
+            param_dict = dict(zip(target_params, args))
+
+            # This is necessary otherwise in some case TypeVar may be same:
+            #
+            # class A(BaseModel, Generic[T]): ...
+            # class B(A[int], Generic[T]): ...
+            # class C(B[str], Generic[T]): ...
+            #
+            key = '__pydantic_inserted_mro_origins__'
path: pydantic/_internal/_model_construction.py
text: @user1:
This `__pydantic_inserted_mro_origins__` should be added as an annotated attribute on the `BaseModel` class if we are going to use it like this. I think it would also be reasonable to add it as a field in the `__pydantic_generic_metadata__` if that makes sense, but if we are using a new attribute in `BaseModel` it should be added near this area: https://github.com/pydantic/pydantic/blob/ae7770375837020fca5c182acddae8ac66302452/pydantic/main.py#L141