File py314.patch of Package python-pydantic

From 9452e13c571db7d31051768c3b4d47a6e2ceea7d Mon Sep 17 00:00:00 2001
From: Victorien <65306057+Viicos@users.noreply.github.com>
Date: Thu, 10 Jul 2025 11:31:03 +0200
Subject: [PATCH] Add initial support for Python 3.14 (#11991)

Adds basic support for Python 3.14. Deferred annotations work for simple cases, but will need to be improved in the future.
---
 .github/workflows/ci.yml                  |  11 +-
 .github/workflows/integration.yml         |   4 +-
 docs/migration.md                         |   4 +-
 pydantic/_internal/_config.py             |  10 +-
 pydantic/_internal/_fields.py             |   7 +-
 pydantic/_internal/_generics.py           |   7 +-
 pydantic/_internal/_model_construction.py |  24 +-
 pydantic/_internal/_typing_extra.py       |  36 +-
 pydantic/dataclasses.py                   |  13 +-
 pyproject.toml                            |   3 +
 tests/test_dataclasses.py                 |  11 +-
 tests/test_deferred_annotations.py        |  81 ++++
 tests/test_forward_ref.py                 |  15 -
 tests/test_model_signature.py             |   2 +-
 tests/test_pickle.py                      |  12 +-
 tests/test_v1.py                          |   2 +
 uv.lock                                   | 512 ++++++++++++----------
 17 files changed, 469 insertions(+), 285 deletions(-)
 create mode 100644 tests/test_deferred_annotations.py


Index: pydantic-2.11.9/docs/migration.md
===================================================================
--- pydantic-2.11.9.orig/docs/migration.md
+++ pydantic-2.11.9/docs/migration.md
@@ -188,7 +188,7 @@ to help ease migration, but calling them
 If you'd still like to use said arguments, you can use [this workaround](https://github.com/pydantic/pydantic/issues/8825#issuecomment-1946206415).
 * JSON serialization of non-string key values is generally done with `str(key)`, leading to some changes in behavior such as the following:
 
-```python
+```python {test="skip"}
 from typing import Optional
 
 from pydantic import BaseModel as V2BaseModel
@@ -218,7 +218,7 @@ print(v2_model.model_dump_json())
 * `model_dump_json()` results are compacted in order to save space, and don't always exactly match that of `json.dumps()` output.
 That being said, you can easily modify the separators used in `json.dumps()` results in order to align the two outputs:
 
-```python
+```python {test="skip"}
 import json
 
 from pydantic import BaseModel as V2BaseModel
Index: pydantic-2.11.9/pydantic/_internal/_config.py
===================================================================
--- pydantic-2.11.9.orig/pydantic/_internal/_config.py
+++ pydantic-2.11.9/pydantic/_internal/_config.py
@@ -98,7 +98,13 @@ class ConfigWrapper:
             self.config_dict = cast(ConfigDict, config)
 
     @classmethod
-    def for_model(cls, bases: tuple[type[Any], ...], namespace: dict[str, Any], kwargs: dict[str, Any]) -> Self:
+    def for_model(
+        cls,
+        bases: tuple[type[Any], ...],
+        namespace: dict[str, Any],
+        raw_annotations: dict[str, Any],
+        kwargs: dict[str, Any],
+    ) -> Self:
         """Build a new `ConfigWrapper` instance for a `BaseModel`.
 
         The config wrapper built based on (in descending order of priority):
@@ -109,6 +115,7 @@ class ConfigWrapper:
         Args:
             bases: A tuple of base classes.
             namespace: The namespace of the class being created.
+            raw_annotations: The (non-evaluated) annotations of the model.
             kwargs: The kwargs passed to the class being created.
 
         Returns:
@@ -123,7 +130,6 @@ class ConfigWrapper:
         config_class_from_namespace = namespace.get('Config')
         config_dict_from_namespace = namespace.get('model_config')
 
-        raw_annotations = namespace.get('__annotations__', {})
         if raw_annotations.get('model_config') and config_dict_from_namespace is None:
             raise PydanticUserError(
                 '`model_config` cannot be used as a model field name. Use `model_config` for model configuration.',
Index: pydantic-2.11.9/pydantic/_internal/_fields.py
===================================================================
--- pydantic-2.11.9.orig/pydantic/_internal/_fields.py
+++ pydantic-2.11.9/pydantic/_internal/_fields.py
@@ -119,7 +119,8 @@ def collect_model_fields(  # noqa: C901
 
     # https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older
     # annotations is only used for finding fields in parent classes
-    annotations = cls.__dict__.get('__annotations__', {})
+    annotations = _typing_extra.safe_get_annotations(cls)
+
     fields: dict[str, FieldInfo] = {}
 
     class_vars: set[str] = set()
@@ -375,7 +376,9 @@ def collect_dataclass_fields(
 
         with ns_resolver.push(base):
             for ann_name, dataclass_field in dataclass_fields.items():
-                if ann_name not in base.__dict__.get('__annotations__', {}):
+                base_anns = _typing_extra.safe_get_annotations(base)
+
+                if ann_name not in base_anns:
                     # `__dataclass_fields__`contains every field, even the ones from base classes.
                     # Only collect the ones defined on `base`.
                     continue
Index: pydantic-2.11.9/pydantic/_internal/_generics.py
===================================================================
--- pydantic-2.11.9.orig/pydantic/_internal/_generics.py
+++ pydantic-2.11.9/pydantic/_internal/_generics.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import operator
 import sys
 import types
 import typing
@@ -7,6 +8,7 @@ from collections import ChainMap
 from collections.abc import Iterator, Mapping
 from contextlib import contextmanager
 from contextvars import ContextVar
+from functools import reduce
 from itertools import zip_longest
 from types import prepare_class
 from typing import TYPE_CHECKING, Annotated, Any, TypeVar
@@ -21,9 +23,6 @@ from ._core_utils import get_type_ref
 from ._forward_ref import PydanticRecursiveRef
 from ._utils import all_identical, is_model_class
 
-if sys.version_info >= (3, 10):
-    from typing import _UnionGenericAlias  # type: ignore[attr-defined]
-
 if TYPE_CHECKING:
     from ..main import BaseModel
 
@@ -311,7 +310,7 @@ def replace_types(type_: Any, type_map:
         # 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:
-            return _UnionGenericAlias(origin_type, resolved_type_args)
+            return reduce(operator.or_, resolved_type_args)
         # NotRequired[T] and Required[T] don't support tuple type resolved_type_args, hence the condition below
         return origin_type[resolved_type_args[0] if len(resolved_type_args) == 1 else resolved_type_args]
 
Index: pydantic-2.11.9/pydantic/_internal/_model_construction.py
===================================================================
--- pydantic-2.11.9.orig/pydantic/_internal/_model_construction.py
+++ pydantic-2.11.9/pydantic/_internal/_model_construction.py
@@ -105,12 +105,29 @@ class ModelMetaclass(ABCMeta):
         # that `BaseModel` itself won't have any bases, but any subclass of it will, to determine whether the `__new__`
         # call we're in the middle of is for the `BaseModel` class.
         if bases:
+            raw_annotations: dict[str, Any]
+            if sys.version_info >= (3, 14):
+                if (
+                    '__annotations__' in namespace
+                ):  # `from __future__ import annotations` was used in the model's module
+                    raw_annotations = namespace['__annotations__']
+                else:
+                    # See https://docs.python.org/3.14/library/annotationlib.html#using-annotations-in-a-metaclass:
+                    from annotationlib import Format, call_annotate_function, get_annotate_from_class_namespace
+
+                    if annotate := get_annotate_from_class_namespace(namespace):
+                        raw_annotations = call_annotate_function(annotate, format=Format.FORWARDREF)
+                    else:
+                        raw_annotations = {}
+            else:
+                raw_annotations = namespace.get('__annotations__', {})
+
             base_field_names, class_vars, base_private_attributes = mcs._collect_bases_data(bases)
 
-            config_wrapper = ConfigWrapper.for_model(bases, namespace, kwargs)
+            config_wrapper = ConfigWrapper.for_model(bases, namespace, raw_annotations, kwargs)
             namespace['model_config'] = config_wrapper.config_dict
             private_attributes = inspect_namespace(
-                namespace, config_wrapper.ignored_types, class_vars, base_field_names
+                namespace, raw_annotations, config_wrapper.ignored_types, class_vars, base_field_names
             )
             if private_attributes or base_private_attributes:
                 original_model_post_init = get_model_post_init(namespace, bases)
@@ -365,6 +382,7 @@ def get_model_post_init(namespace: dict[
 
 def inspect_namespace(  # noqa C901
     namespace: dict[str, Any],
+    raw_annotations: dict[str, Any],
     ignored_types: tuple[type[Any], ...],
     base_class_vars: set[str],
     base_class_fields: set[str],
@@ -375,6 +393,7 @@ def inspect_namespace(  # noqa C901
 
     Args:
         namespace: The attribute dictionary of the class to be created.
+        raw_annotations: The (non-evaluated) annotations of the model.
         ignored_types: A tuple of ignore types.
         base_class_vars: A set of base class class variables.
         base_class_fields: A set of base class fields.
@@ -396,7 +415,6 @@ def inspect_namespace(  # noqa C901
     all_ignored_types = ignored_types + default_ignored_types()
 
     private_attributes: dict[str, ModelPrivateAttr] = {}
-    raw_annotations = namespace.get('__annotations__', {})
 
     if '__root__' in raw_annotations or '__root__' in namespace:
         raise TypeError("To define root models, use `pydantic.RootModel` rather than a field called '__root__'")
Index: pydantic-2.11.9/pydantic/_internal/_typing_extra.py
===================================================================
--- pydantic-2.11.9.orig/pydantic/_internal/_typing_extra.py
+++ pydantic-2.11.9/pydantic/_internal/_typing_extra.py
@@ -26,6 +26,9 @@ else:
     from types import EllipsisType as EllipsisType
     from types import NoneType as NoneType
 
+if sys.version_info >= (3, 14):
+    import annotationlib
+
 if TYPE_CHECKING:
     from pydantic import BaseModel
 
@@ -289,6 +292,19 @@ def _type_convert(arg: Any) -> Any:
     return arg
 
 
+def safe_get_annotations(cls: type[Any]) -> dict[str, Any]:
+    """Get the annotations for the provided class, accounting for potential deferred forward references.
+
+    Starting with Python 3.14, accessing the `__annotations__` attribute might raise a `NameError` if
+    a referenced symbol isn't defined yet. In this case, we return the annotation in the *forward ref*
+    format.
+    """
+    if sys.version_info >= (3, 14):
+        return annotationlib.get_annotations(cls, format=annotationlib.Format.FORWARDREF)
+    else:
+        return cls.__dict__.get('__annotations__', {})
+
+
 def get_model_type_hints(
     obj: type[BaseModel],
     *,
@@ -309,9 +325,14 @@ def get_model_type_hints(
     ns_resolver = ns_resolver or NsResolver()
 
     for base in reversed(obj.__mro__):
-        ann: dict[str, Any] | None = base.__dict__.get('__annotations__')
-        if not ann or isinstance(ann, types.GetSetDescriptorType):
+        # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
+        # from the ns_resolver, but we want to be able to know which specific field failed
+        # to evaluate:
+        ann = safe_get_annotations(base)
+
+        if not ann:
             continue
+
         with ns_resolver.push(base):
             globalns, localns = ns_resolver.types_namespace
             for name, value in ann.items():
@@ -341,13 +362,18 @@ def get_cls_type_hints(
         obj: The class to inspect.
         ns_resolver: A namespace resolver instance to use. Defaults to an empty instance.
     """
-    hints: dict[str, Any] | dict[str, tuple[Any, bool]] = {}
+    hints: dict[str, Any] = {}
     ns_resolver = ns_resolver or NsResolver()
 
     for base in reversed(obj.__mro__):
-        ann: dict[str, Any] | None = base.__dict__.get('__annotations__')
-        if not ann or isinstance(ann, types.GetSetDescriptorType):
+        # For Python 3.14, we could also use `Format.VALUE` and pass the globals/locals
+        # from the ns_resolver, but we want to be able to know which specific field failed
+        # to evaluate:
+        ann = safe_get_annotations(base)
+
+        if not ann:
             continue
+
         with ns_resolver.push(base):
             globalns, localns = ns_resolver.types_namespace
             for name, value in ann.items():
Index: pydantic-2.11.9/pydantic/dataclasses.py
===================================================================
--- pydantic-2.11.9.orig/pydantic/dataclasses.py
+++ pydantic-2.11.9/pydantic/dataclasses.py
@@ -157,7 +157,12 @@ def dataclass(
           `x: int = dataclasses.field(default=pydantic.Field(..., kw_only=True), kw_only=True)`
         """
         for annotation_cls in cls.__mro__:
-            annotations: dict[str, Any] = getattr(annotation_cls, '__annotations__', {})
+            if sys.version_info >= (3, 14):
+                from annotationlib import Format, get_annotations
+
+                annotations = get_annotations(annotation_cls, format=Format.FORWARDREF)
+            else:
+                annotations: dict[str, Any] = getattr(annotation_cls, '__annotations__', {})
             for field_name in annotations:
                 field_value = getattr(cls, field_name, None)
                 # Process only if this is an instance of `FieldInfo`.
@@ -176,9 +181,9 @@ def dataclass(
                     field_args['repr'] = field_value.repr
 
                 setattr(cls, field_name, dataclasses.field(**field_args))
-                # In Python 3.9, when subclassing, information is pulled from cls.__dict__['__annotations__']
-                # for annotations, so we must make sure it's initialized before we add to it.
-                if cls.__dict__.get('__annotations__') is None:
+                if sys.version_info < (3, 10) and cls.__dict__.get('__annotations__') is None:
+                    # In Python 3.9, when a class doesn't have any annotations, accessing `__annotations__`
+                    # raises an `AttributeError`.
                     cls.__annotations__ = {}
                 cls.__annotations__[field_name] = annotations[field_name]
 
Index: pydantic-2.11.9/pyproject.toml
===================================================================
--- pydantic-2.11.9.orig/pyproject.toml
+++ pydantic-2.11.9/pyproject.toml
@@ -32,6 +32,7 @@ classifiers = [
     'Programming Language :: Python :: 3.11',
     'Programming Language :: Python :: 3.12',
     'Programming Language :: Python :: 3.13',
+    'Programming Language :: Python :: 3.14',
     'Intended Audience :: Developers',
     'Intended Audience :: Information Technology',
     'License :: OSI Approved :: MIT License',
@@ -220,6 +221,8 @@ pydocstyle = { convention = 'google' }
 'docs/*' = ['D']
 'pydantic/__init__.py' = ['F405', 'F403', 'D']
 'tests/test_forward_ref.py' = ['F821']
+# We can't configure a specific Python version per file (this one only supports 3.14+):
+'tests/test_deferred_annotations.py' = ['F821', 'F841']
 'tests/*' = ['D', 'B', 'C4']
 'pydantic/deprecated/*' = ['D', 'PYI']
 'pydantic/color.py' = ['PYI']
Index: pydantic-2.11.9/tests/test_dataclasses.py
===================================================================
--- pydantic-2.11.9.orig/tests/test_dataclasses.py
+++ pydantic-2.11.9/tests/test_dataclasses.py
@@ -30,6 +30,7 @@ from pydantic import (
     BaseModel,
     BeforeValidator,
     ConfigDict,
+    Field,
     PydanticDeprecatedSince20,
     PydanticUndefinedAnnotation,
     PydanticUserError,
@@ -45,7 +46,6 @@ from pydantic import (
 )
 from pydantic._internal._mock_val_ser import MockValSer
 from pydantic.dataclasses import is_pydantic_dataclass, rebuild_dataclass
-from pydantic.fields import Field, FieldInfo
 from pydantic.json_schema import model_json_schema
 
 
@@ -2072,15 +2072,14 @@ def test_inheritance_replace(decorator1:
 def test_dataclasses_inheritance_default_value_is_not_deleted(
     decorator1: Callable[[Any], Any], default: Literal[1]
 ) -> None:
-    if decorator1 is dataclasses.dataclass and isinstance(default, FieldInfo):
-        pytest.skip(reason="stdlib dataclasses don't support Pydantic fields")
-
     @decorator1
     class Parent:
         a: int = default
 
-    assert Parent.a == 1
-    assert Parent().a == 1
+    # stdlib dataclasses don't support Pydantic's `Field()`:
+    if decorator1 is pydantic.dataclasses.dataclass:
+        assert Parent.a == 1
+        assert Parent().a == 1
 
     @pydantic.dataclasses.dataclass
     class Child(Parent):
Index: pydantic-2.11.9/tests/test_deferred_annotations.py
===================================================================
--- /dev/null
+++ pydantic-2.11.9/tests/test_deferred_annotations.py
@@ -0,0 +1,81 @@
+"""Tests related to deferred evaluation of annotations introduced in Python 3.14 by PEP 649 and 749."""
+
+import sys
+from dataclasses import field
+from typing import Annotated
+
+import pytest
+from annotated_types import MaxLen
+
+from pydantic import BaseModel, Field, ValidationError
+from pydantic.dataclasses import dataclass
+
+pytestmark = pytest.mark.skipif(
+    sys.version_info < (3, 14), reason='Requires deferred evaluation of annotations introduced in Python 3.14'
+)
+
+
+def test_deferred_annotations_model() -> None:
+    class Model(BaseModel):
+        a: Int
+        b: Str = 'a'
+
+    Int = int
+    Str = str
+
+    inst = Model(a='1', b=b'test')
+    assert inst.a == 1
+    assert inst.b == 'test'
+
+
+@pytest.mark.xfail(
+    reason=(
+        'When rebuilding model fields, we individually re-evaluate all fields (using `_eval_type()`) '
+        "and as such we don't benefit from PEP 649's capabilities."
+    ),
+)
+def test_deferred_annotations_nested_model() -> None:
+    def outer():
+        def inner():
+            class Model(BaseModel):
+                ann: Annotated[List[Dict[str, str]], MaxLen(1)]
+
+            Dict = dict
+
+            return Model
+
+        List = list
+
+        Model = inner()
+
+        return Model
+
+    Model = outer()
+
+    with pytest.raises(ValidationError) as exc_info:
+        Model(ann=[{'a': 'b'}, {'c': 'd'}])
+
+    assert exc_info.value.errors()[0]['type'] == 'too_long'
+
+
+def test_deferred_annotations_pydantic_dataclass() -> None:
+    @dataclass
+    class A:
+        a: Int = field(default=1)
+
+    Int = int
+
+    assert A(a='1').a == 1
+
+
+@pytest.mark.xfail(
+    reason="To support Pydantic's `Field()` function in dataclasses, we directly write to `__annotations__`"
+)
+def test_deferred_annotations_pydantic_dataclass_pydantic_field() -> None:
+    @dataclass
+    class A:
+        a: Int = Field(default=1)
+
+    Int = int
+
+    assert A(a='1').a == 1
Index: pydantic-2.11.9/tests/test_forward_ref.py
===================================================================
--- pydantic-2.11.9.orig/tests/test_forward_ref.py
+++ pydantic-2.11.9/tests/test_forward_ref.py
@@ -74,21 +74,6 @@ def test_forward_ref_auto_update_no_mode
     assert f.model_dump() == {'a': {'b': {'a': {'b': {'a': None}}}}}
 
 
-def test_forward_ref_one_of_fields_not_defined(create_module):
-    @create_module
-    def module():
-        from pydantic import BaseModel
-
-        class Foo(BaseModel):
-            foo: 'Foo'
-            bar: 'Bar'
-
-    assert {k: repr(v) for k, v in module.Foo.model_fields.items()} == {
-        'foo': 'FieldInfo(annotation=Foo, required=True)',
-        'bar': "FieldInfo(annotation=ForwardRef('Bar'), required=True)",
-    }
-
-
 def test_basic_forward_ref(create_module):
     @create_module
     def module():
Index: pydantic-2.11.9/tests/test_model_signature.py
===================================================================
--- pydantic-2.11.9.orig/tests/test_model_signature.py
+++ pydantic-2.11.9/tests/test_model_signature.py
@@ -184,7 +184,7 @@ def test_annotated_field():
     assert typing_objects.is_annotated(get_origin(sig.parameters['foo'].annotation))
 
 
-@pytest.mark.skipif(sys.version_info < (3, 10), reason='repr different on older versions')
+@pytest.mark.skipif(sys.version_info < (3, 10), sys.version_info >= (3, 14), reason='repr different on older versions')
 def test_annotated_optional_field():
     from annotated_types import Gt
 
Index: pydantic-2.11.9/tests/test_pickle.py
===================================================================
--- pydantic-2.11.9.orig/tests/test_pickle.py
+++ pydantic-2.11.9/tests/test_pickle.py
@@ -1,6 +1,7 @@
 import dataclasses
 import gc
 import pickle
+import sys
 from typing import Optional
 
 import pytest
@@ -17,6 +18,11 @@ except ImportError:
 
 pytestmark = pytest.mark.skipif(cloudpickle is None, reason='cloudpickle is not installed')
 
+cloudpickle_xfail = pytest.mark.xfail(
+    condition=sys.version_info >= (3, 14),
+    reason='Cloudpickle issue: https://github.com/cloudpipe/cloudpickle/issues/572',
+)
+
 
 class IntWrapper:
     def __init__(self, v: int):
@@ -88,7 +94,7 @@ def model_factory() -> type:
         (ImportableModel, False),
         (ImportableModel, True),
         # Locally-defined model can only be pickled with cloudpickle.
-        (model_factory(), True),
+        pytest.param(model_factory(), True, marks=cloudpickle_xfail),
     ],
 )
 def test_pickle_model(model_type: type, use_cloudpickle: bool):
@@ -133,7 +139,7 @@ def nested_model_factory() -> type:
         (ImportableNestedModel, False),
         (ImportableNestedModel, True),
         # Locally-defined model can only be pickled with cloudpickle.
-        (nested_model_factory(), True),
+        pytest.param(nested_model_factory(), True, marks=cloudpickle_xfail),
     ],
 )
 def test_pickle_nested_model(model_type: type, use_cloudpickle: bool):
@@ -264,7 +270,7 @@ def nested_dataclass_model_factory() ->
         (ImportableNestedDataclassModel, False),
         (ImportableNestedDataclassModel, True),
         # Locally-defined model can only be pickled with cloudpickle.
-        (nested_dataclass_model_factory(), True),
+        pytest.param(nested_dataclass_model_factory(), True, marks=cloudpickle_xfail),
     ],
 )
 def test_pickle_dataclass_nested_in_model(model_type: type, use_cloudpickle: bool):
Index: pydantic-2.11.9/tests/test_v1.py
===================================================================
--- pydantic-2.11.9.orig/tests/test_v1.py
+++ pydantic-2.11.9/tests/test_v1.py
@@ -1,3 +1,4 @@
+import sys
 import warnings
 
 import pytest
@@ -14,6 +15,7 @@ def test_version():
     assert V1_VERSION != VERSION
 
 
+@pytest.mark.skipif(sys.version_info >= (3, 14), reason='Python 3.14+ not supported')
 @pytest.mark.thread_unsafe(reason='Mutates the value')
 def test_root_validator():
     class Model(V1BaseModel):
openSUSE Build Service is sponsored by