From: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= <tiangolo@gmail.com>
Date: Sun, 25 Jan 2026 16:46:21 +0100
Subject: =?utf-8?q?=E2=9C=A8_Add_support_for_Python_3=2E13_and_Python_3=2E1?=
 =?utf-8?q?4_with_new_logic_from_Pydantic?=

Origin: https://github.com/art049/odmantic/pull/546/commits/15029da18e4482bb1068ed53d14ca4357e0aa023
Forwarded: not-needed
---
 odmantic/model.py  |   3 +-
 odmantic/typing.py | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 120 insertions(+), 5 deletions(-)

diff --git a/odmantic/model.py b/odmantic/model.py
index c0cfcab..125f2f2 100644
--- a/odmantic/model.py
+++ b/odmantic/model.py
@@ -69,6 +69,7 @@ from odmantic.typing import (
     Annotated,
     GenericAlias,
     dataclass_transform,
+    get_annotations_from_namespace,
     get_args,
     get_first_type_argument_subclassing,
     get_origin,
@@ -216,7 +217,7 @@ class BaseModelMetaclass(pydantic._internal._model_construction.ModelMetaclass):
     ) -> None:
         """Validate the class name space in place"""
         annotations = resolve_annotations(
-            namespace.get("__annotations__", {}), namespace.get("__module__")
+            get_annotations_from_namespace(namespace), namespace.get("__module__")
         )
         config = validate_config(namespace.get("model_config", ODMConfigDict()), name)
         odm_fields: Dict[str, ODMBaseField] = {}
diff --git a/odmantic/typing.py b/odmantic/typing.py
index 4beb82c..61a715d 100644
--- a/odmantic/typing.py
+++ b/odmantic/typing.py
@@ -1,10 +1,33 @@
 import sys
-from typing import TYPE_CHECKING, AbstractSet, Any  # noqa: F401
+from typing import (  # noqa: F401
+    TYPE_CHECKING,
+    AbstractSet,
+    Any,
+    ClassVar,
+    Dict,
+    ForwardRef,
+    Iterable,
+    Mapping,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+    _eval_type,  # type: ignore
+)
+import typing
+import types
 from typing import Callable as TypingCallable
-from typing import Dict, Iterable, Mapping, Tuple, Type, TypeVar, Union  # noqa: F401
 
-from pydantic.v1.typing import is_classvar, resolve_annotations  # noqa: F401
-from pydantic.v1.utils import lenient_issubclass
+# Copy from Pydantic: pydantic/_internal/_typing_extra.py
+if sys.version_info < (3, 10):
+    WithArgsTypes: tuple[Any, ...] = (typing._GenericAlias, types.GenericAlias)  # type: ignore[attr-defined]
+else:
+    WithArgsTypes: tuple[Any, ...] = (
+        typing._GenericAlias,
+        types.GenericAlias,
+        types.UnionType,
+    )
+
 
 if sys.version_info < (3, 11):
     from typing_extensions import dataclass_transform
@@ -36,6 +59,97 @@ if TYPE_CHECKING:
     IncEx: TypeAlias = "set[int] | set[str] | dict[int, Any] | dict[str, Any] | None"
 
 
+def lenient_issubclass(
+    cls: Any, class_or_tuple: Union[type[Any], tuple[type[Any], ...], None]
+) -> bool:
+    # Copy of Pydantic: pydantic/_internal/_utils.py
+    try:
+        return isinstance(cls, type) and issubclass(cls, class_or_tuple)  # type: ignore[arg-type]
+    except TypeError:  # pragma: no cover
+        if isinstance(cls, WithArgsTypes):
+            return False
+        raise  # pragma: no cover
+
+def _check_classvar(v: Union[Type[Any], None]) -> bool:
+    # Copy of Pydantic: pydantic/v1/typing.py
+    # This can probably be replaced by the logic in: pydantic/_internal/_typing_extra.py
+    # after dropping support for Python 3.8 and 3.9
+    if v is None:
+        return False
+
+    return v.__class__ == ClassVar.__class__ and getattr(v, '_name', None) == 'ClassVar'
+
+def is_classvar(ann_type: Type[Any]) -> bool:
+    # Copy of Pydantic: pydantic/v1/typing.py
+    # This can probably be replaced by the logic in: pydantic/_internal/_typing_extra.py
+    # after dropping support for Python 3.8 and 3.9
+    if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)):
+        return True
+
+    # this is an ugly workaround for class vars that contain forward references and are therefore themselves
+    # forward references, see #3679
+    if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith('ClassVar['):
+        return True
+
+    return False
+
+
+def resolve_annotations(
+    raw_annotations: Dict[str, Type[Any]], module_name: Union[str, None]
+) -> Dict[str, Type[Any]]:
+    # Ref: pydantic/v1/typing.py
+    # Copy from Pydantic v2's pydantic.v1 for compatibility with versions of Python not
+    # supported by Pydantic v2, after dropping support for Python 3.8 and 3.9 this can
+    # be replaced by the logic used in Pydantic v2's ModelMetaclass.__new__
+    # in: pydantic/_internal/_model_construction.py
+    base_globals: Union[Dict[str, Any], None] = None
+    if module_name:
+        try:
+            module = sys.modules[module_name]
+        except KeyError:
+            # happens occasionally, see https://github.com/pydantic/pydantic/issues/2363
+            pass
+        else:
+            base_globals = module.__dict__
+
+    annotations = {}
+    for name, value in raw_annotations.items():
+        if isinstance(value, str):
+            if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= (
+                3,
+                10,
+                1,
+            ):
+                value = ForwardRef(value, is_argument=False, is_class=True)
+            else:
+                value = ForwardRef(value, is_argument=False)
+        try:
+            if sys.version_info >= (3, 13):
+                value = _eval_type(value, base_globals, None, type_params=())
+            else:
+                value = _eval_type(value, base_globals, None)
+        except NameError:
+            # this is ok, it can be fixed with update_forward_refs
+            pass
+        annotations[name] = value
+    return annotations
+
+
+def get_annotations_from_namespace(class_dict: Dict[str, Any]) -> Dict[str, Any]:
+    # Ref: https://github.com/pydantic/pydantic/pull/11991
+    raw_annotations: Dict[str, Any] = class_dict.get("__annotations__", {})
+    if sys.version_info >= (3, 14) and "__annotations__" not in class_dict:
+        from annotationlib import (
+            Format,
+            call_annotate_function,
+            get_annotate_from_class_namespace,
+        )
+
+        if annotate := get_annotate_from_class_namespace(class_dict):
+            raw_annotations = call_annotate_function(annotate, format=Format.FORWARDREF)
+    return raw_annotations
+
+
 def is_type_argument_subclass(
     type_: Type, class_or_tuple: Union[Type[Any], Tuple[Type[Any], ...]]
 ) -> bool:
