14
0

Accepting request 1322568 from devel:languages:python

- Update to 2.12.5
  * Fix pickle error when using model_construct() on a model with MISSING as a default value in #12522.
  * Several updates to the documentation
- Remove patches bump-pydantic-core-2.35.1.patch, field-name-validator-core-schemas.patch,
  py314.patch, and support-pydantic-core-2.39.0.patch as they've merged upstream.

OBS-URL: https://build.opensuse.org/request/show/1322568
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python-pydantic?expand=0&rev=37
This commit is contained in:
2025-12-17 16:29:49 +00:00
committed by Git OBS Bridge
8 changed files with 21 additions and 1532 deletions

View File

@@ -1,424 +0,0 @@
From 4494c31a4834bdc2301cfa3d94f4bbc62c2774dc Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Wed, 11 Jun 2025 14:52:26 +0200
Subject: [PATCH] Bump `pydantic-core` to v2.35.1
Make use of `ensure_ascii` option
Update typechecking tests
Remove core schema validation hook
---
docs/api/standard_library_types.md | 2 +-
docs/why.md | 2 +-
pydantic/_internal/_core_utils.py | 8 -
pydantic/_internal/_generate_schema.py | 5 +-
pydantic/functional_serializers.py | 4 +-
pydantic/functional_validators.py | 10 +-
pydantic/main.py | 4 +
pydantic/type_adapter.py | 3 +
pydantic/version.py | 2 +-
pyproject.toml | 4 +-
tests/typechecking/decorators.py | 79 +++++++--
12 files changed, 203 insertions(+), 140 deletions(-)
Index: pydantic-2.11.7/docs/api/standard_library_types.md
===================================================================
--- pydantic-2.11.7.orig/docs/api/standard_library_types.md
+++ pydantic-2.11.7/docs/api/standard_library_types.md
@@ -81,7 +81,7 @@ event = Event(dt='2032-04-23T10:20:30.40
print(event.model_dump())
"""
-{'dt': datetime.datetime(2032, 4, 23, 10, 20, 30, 400000, tzinfo=TzInfo(+02:30))}
+{'dt': datetime.datetime(2032, 4, 23, 10, 20, 30, 400000, tzinfo=TzInfo(9000))}
"""
```
Index: pydantic-2.11.7/docs/why.md
===================================================================
--- pydantic-2.11.7.orig/docs/why.md
+++ pydantic-2.11.7/docs/why.md
@@ -363,7 +363,7 @@ Functional validators and serializers, a
print(Meeting(when='2020-01-01T12:00+01:00'))
- #> when=datetime.datetime(2020, 1, 1, 12, 0, tzinfo=TzInfo(+01:00))
+ #> when=datetime.datetime(2020, 1, 1, 12, 0, tzinfo=TzInfo(3600))
print(Meeting(when='now'))
#> when=datetime.datetime(2032, 1, 2, 3, 4, 5, 6)
print(Meeting(when='2020-01-01T12:00'))
Index: pydantic-2.11.7/pydantic/_internal/_core_utils.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/_internal/_core_utils.py
+++ pydantic-2.11.7/pydantic/_internal/_core_utils.py
@@ -1,12 +1,10 @@
from __future__ import annotations
import inspect
-import os
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Any, Union
from pydantic_core import CoreSchema, core_schema
-from pydantic_core import validate_core_schema as _validate_core_schema
from typing_extensions import TypeGuard, get_args, get_origin
from typing_inspection import typing_objects
@@ -109,12 +107,6 @@ def get_ref(s: core_schema.CoreSchema) -
return s.get('ref', None)
-def validate_core_schema(schema: CoreSchema) -> CoreSchema:
- if os.getenv('PYDANTIC_VALIDATE_CORE_SCHEMAS'):
- return _validate_core_schema(schema)
- return schema
-
-
def _clean_schema_for_pretty_print(obj: Any, strip_metadata: bool = True) -> Any: # pragma: no cover
"""A utility function to remove irrelevant information from a core schema."""
if isinstance(obj, Mapping):
Index: pydantic-2.11.7/pydantic/_internal/_generate_schema.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/_internal/_generate_schema.py
+++ pydantic-2.11.7/pydantic/_internal/_generate_schema.py
@@ -70,7 +70,6 @@ from ._core_utils import (
get_ref,
get_type_ref,
is_list_like_schema_with_items_schema,
- validate_core_schema,
)
from ._decorators import (
Decorator,
@@ -666,9 +665,7 @@ class GenerateSchema:
return schema
def clean_schema(self, schema: CoreSchema) -> CoreSchema:
- schema = self.defs.finalize_schema(schema)
- schema = validate_core_schema(schema)
- return schema
+ return self.defs.finalize_schema(schema)
def _add_js_function(self, metadata_schema: CoreSchema, js_function: Callable[..., Any]) -> None:
metadata = metadata_schema.get('metadata', {})
Index: pydantic-2.11.7/pydantic/functional_serializers.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/functional_serializers.py
+++ pydantic-2.11.7/pydantic/functional_serializers.py
@@ -300,7 +300,7 @@ def field_serializer(
if TYPE_CHECKING:
# The first argument in the following callables represent the `self` type:
- ModelPlainSerializerWithInfo: TypeAlias = Callable[[Any, SerializationInfo], Any]
+ ModelPlainSerializerWithInfo: TypeAlias = Callable[[Any, SerializationInfo[Any]], Any]
"""A model serializer method with the `info` argument, in `plain` mode."""
ModelPlainSerializerWithoutInfo: TypeAlias = Callable[[Any], Any]
@@ -309,7 +309,7 @@ if TYPE_CHECKING:
ModelPlainSerializer: TypeAlias = 'ModelPlainSerializerWithInfo | ModelPlainSerializerWithoutInfo'
"""A model serializer method in `plain` mode."""
- ModelWrapSerializerWithInfo: TypeAlias = Callable[[Any, SerializerFunctionWrapHandler, SerializationInfo], Any]
+ ModelWrapSerializerWithInfo: TypeAlias = Callable[[Any, SerializerFunctionWrapHandler, SerializationInfo[Any]], Any]
"""A model serializer method with the `info` argument, in `wrap` mode."""
ModelWrapSerializerWithoutInfo: TypeAlias = Callable[[Any, SerializerFunctionWrapHandler], Any]
Index: pydantic-2.11.7/pydantic/functional_validators.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/functional_validators.py
+++ pydantic-2.11.7/pydantic/functional_validators.py
@@ -332,7 +332,7 @@ if TYPE_CHECKING:
def __call__(self, cls: Any, value: Any, /) -> Any: ...
class _V2ValidatorClsMethod(Protocol):
- def __call__(self, cls: Any, value: Any, info: _core_schema.ValidationInfo, /) -> Any: ...
+ def __call__(self, cls: Any, value: Any, info: core_schema.ValidationInfo[Any], /) -> Any: ...
class _OnlyValueWrapValidatorClsMethod(Protocol):
def __call__(self, cls: Any, value: Any, handler: _core_schema.ValidatorFunctionWrapHandler, /) -> Any: ...
@@ -343,7 +343,7 @@ if TYPE_CHECKING:
cls: Any,
value: Any,
handler: _core_schema.ValidatorFunctionWrapHandler,
- info: _core_schema.ValidationInfo,
+ info: core_schema.ValidationInfo[Any],
/,
) -> Any: ...
@@ -559,7 +559,7 @@ class ModelWrapValidator(Protocol[_Model
# thus validators _must_ handle all cases
value: Any,
handler: ModelWrapValidatorHandler[_ModelType],
- info: _core_schema.ValidationInfo,
+ info: core_schema.ValidationInfo[Any],
/,
) -> _ModelType: ...
@@ -604,7 +604,7 @@ class FreeModelBeforeValidator(Protocol)
# or anything else that gets passed to validate_python
# thus validators _must_ handle all cases
value: Any,
- info: _core_schema.ValidationInfo,
+ info: core_schema.ValidationInfo[Any],
/,
) -> Any: ...
@@ -619,7 +619,7 @@ class ModelBeforeValidator(Protocol):
# or anything else that gets passed to validate_python
# thus validators _must_ handle all cases
value: Any,
- info: _core_schema.ValidationInfo,
+ info: core_schema.ValidationInfo[Any],
/,
) -> Any: ...
@@ -629,7 +629,7 @@ ModelAfterValidatorWithoutInfo = Callabl
have info argument.
"""
-ModelAfterValidator = Callable[[_ModelType, _core_schema.ValidationInfo], _ModelType]
+ModelAfterValidator = Callable[[_ModelType, core_schema.ValidationInfo[Any]], _ModelType]
"""A `@model_validator` decorated function signature. This is used when `mode='after'`."""
_AnyModelWrapValidator = Union[ModelWrapValidator[_ModelType], ModelWrapValidatorWithoutInfo[_ModelType]]
Index: pydantic-2.11.7/pydantic/main.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/main.py
+++ pydantic-2.11.7/pydantic/main.py
@@ -480,6 +480,7 @@ class BaseModel(metaclass=_model_constru
self,
*,
indent: int | None = None,
+ ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
context: Any | None = None,
@@ -499,6 +500,8 @@ class BaseModel(metaclass=_model_constru
Args:
indent: Indentation to use in the JSON output. If None is passed, the output will be compact.
+ ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped.
+ If `False` (the default), these characters will be output as-is.
include: Field(s) to include in the JSON output.
exclude: Field(s) to exclude from the JSON output.
context: Additional context to pass to the serializer.
@@ -519,6 +522,7 @@ class BaseModel(metaclass=_model_constru
return self.__pydantic_serializer__.to_json(
self,
indent=indent,
+ ensure_ascii=ensure_ascii,
include=include,
exclude=exclude,
context=context,
Index: pydantic-2.11.7/pydantic/type_adapter.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/type_adapter.py
+++ pydantic-2.11.7/pydantic/type_adapter.py
@@ -591,6 +591,7 @@ class TypeAdapter(Generic[T]):
/,
*,
indent: int | None = None,
+ ensure_ascii: bool = False,
include: IncEx | None = None,
exclude: IncEx | None = None,
by_alias: bool | None = None,
@@ -611,6 +612,8 @@ class TypeAdapter(Generic[T]):
Args:
instance: The instance to be serialized.
indent: Number of spaces for JSON indentation.
+ ensure_ascii: If `True`, the output is guaranteed to have all incoming non-ASCII characters escaped.
+ If `False` (the default), these characters will be output as-is.
include: Fields to include.
exclude: Fields to exclude.
by_alias: Whether to use alias names for field names.
Index: pydantic-2.11.7/pydantic/version.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/version.py
+++ pydantic-2.11.7/pydantic/version.py
@@ -66,7 +66,7 @@ def version_info() -> str:
def check_pydantic_core_version() -> bool:
"""Check that the installed `pydantic-core` dependency is compatible."""
# Keep this in sync with the version constraint in the `pyproject.toml` dependencies:
- return __pydantic_core_version__ == '2.33.2'
+ return __pydantic_core_version__ == '2.35.1'
def parse_mypy_version(version: str) -> tuple[int, int, int]:
Index: pydantic-2.11.7/pyproject.toml
===================================================================
--- pydantic-2.11.7.orig/pyproject.toml
+++ pydantic-2.11.7/pyproject.toml
@@ -43,10 +43,10 @@ classifiers = [
]
requires-python = '>=3.9'
dependencies = [
- 'typing-extensions>=4.12.2',
+ 'typing-extensions>=4.13.0',
'annotated-types>=0.6.0',
# Keep this in sync with the version in the `check_pydantic_core_version()` function:
- 'pydantic-core==2.33.2',
+ 'pydantic-core==2.35.1',
'typing-inspection>=0.4.0',
]
dynamic = ['version', 'readme']
Index: pydantic-2.11.7/tests/typechecking/decorators.py
===================================================================
--- pydantic-2.11.7.orig/tests/typechecking/decorators.py
+++ pydantic-2.11.7/tests/typechecking/decorators.py
@@ -31,13 +31,25 @@ class BeforeModelValidator(BaseModel):
"""TODO This shouldn't be valid. At runtime, `self` is the actual value and `value` is the `ValidationInfo` instance."""
@model_validator(mode='before')
- def valid_method_info(self, value: Any, info: ValidationInfo) -> Any: ...
+ def valid_method_info_default(self, value: Any, info: ValidationInfo) -> Any: ...
+
+ @model_validator(mode='before')
+ def valid_method_info(self, value: Any, info: ValidationInfo[int]) -> Any:
+ assert_type(info.context, int)
@model_validator(mode='before')
@classmethod
def valid_classmethod(cls, value: Any) -> Any: ...
@model_validator(mode='before')
+ @classmethod
+ def valid_classmethod_info_default(cls, value: Any, info: ValidationInfo) -> Any: ...
+
+ @model_validator(mode='before')
+ @classmethod
+ def valid_classmethod_info(cls, value: Any, info: ValidationInfo[int]) -> Any: ...
+
+ @model_validator(mode='before')
@staticmethod
def valid_staticmethod(value: Any) -> Any: ...
@@ -91,7 +103,10 @@ class AfterModelValidator(BaseModel):
def valid_method_no_info(self) -> Self: ...
@model_validator(mode='after')
- def valid_method_info(self, info: ValidationInfo) -> Self: ...
+ def valid_method_info_default(self, info: ValidationInfo) -> Self: ...
+
+ @model_validator(mode='after')
+ def valid_method_info(self, info: ValidationInfo[int]) -> Self: ...
class BeforeFieldValidator(BaseModel):
@@ -114,7 +129,11 @@ class BeforeFieldValidator(BaseModel):
@field_validator('foo', mode='before', json_schema_input_type=int) # `json_schema_input_type` allowed here.
@classmethod
- def valid_with_info(cls, value: Any, info: ValidationInfo) -> Any: ...
+ def valid_with_info_default(cls, value: Any, info: ValidationInfo) -> Any: ...
+
+ @field_validator('foo', mode='before', json_schema_input_type=int) # `json_schema_input_type` allowed here.
+ @classmethod
+ def valid_with_info(cls, value: Any, info: ValidationInfo[int]) -> Any: ...
class AfterFieldValidator(BaseModel):
@@ -122,6 +141,14 @@ class AfterFieldValidator(BaseModel):
@classmethod
def valid_classmethod(cls, value: Any) -> Any: ...
+ @field_validator('foo', mode='after')
+ @classmethod
+ def valid_classmethod_info_default(cls, value: Any, info: ValidationInfo) -> Any: ...
+
+ @field_validator('foo', mode='after')
+ @classmethod
+ def valid_classmethod_info(cls, value: Any, info: ValidationInfo[int]) -> Any: ...
+
@field_validator('foo', mode='after', json_schema_input_type=int) # type: ignore[call-overload] # pyright: ignore[reportCallIssue, reportArgumentType]
@classmethod
def invalid_input_type_not_allowed(cls, value: Any) -> Any: ...
@@ -148,7 +175,13 @@ class WrapFieldValidator(BaseModel):
@field_validator('foo', mode='wrap', json_schema_input_type=int) # `json_schema_input_type` allowed here.
@classmethod
- def valid_with_info(cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) -> Any: ...
+ def valid_with_info_default(
+ cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
+ ) -> Any: ...
+
+ @field_validator('foo', mode='wrap', json_schema_input_type=int) # `json_schema_input_type` allowed here.
+ @classmethod
+ def valid_with_info(cls, value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo[int]) -> Any: ...
class PlainModelSerializer(BaseModel):
@@ -162,7 +195,10 @@ class PlainModelSerializer(BaseModel):
def valid_plain_serializer_2(self) -> Any: ...
@model_serializer(mode='plain')
- def valid_plain_serializer_info(self, info: SerializationInfo) -> Any: ...
+ def valid_plain_serializer_info_default(self, info: SerializationInfo) -> Any: ...
+
+ @model_serializer(mode='plain')
+ def valid_plain_serializer_info(self, info: SerializationInfo[int]) -> Any: ...
class WrapModelSerializer(BaseModel):
@@ -175,7 +211,12 @@ class WrapModelSerializer(BaseModel):
return value
@model_serializer(mode='wrap')
- def valid_info(self, handler: SerializerFunctionWrapHandler, info: SerializationInfo) -> Any:
+ def valid_info_default(self, handler: SerializerFunctionWrapHandler, info: SerializationInfo) -> Any:
+ value = handler(self)
+ return value
+
+ @model_serializer(mode='wrap')
+ def valid_info(self, handler: SerializerFunctionWrapHandler, info: SerializationInfo[int]) -> Any:
value = handler(self)
return value
@@ -205,7 +246,10 @@ class PlainFieldSerializer(BaseModel):
"""
@field_serializer('a', mode='plain')
- def valid_method_info(self, value: Any, info: FieldSerializationInfo) -> Any: ...
+ def valid_method_info_default(self, value: Any, info: FieldSerializationInfo) -> Any: ...
+
+ @field_serializer('a', mode='plain')
+ def valid_method_info(self, value: Any, info: FieldSerializationInfo[int]) -> Any: ...
@field_serializer('a', mode='plain')
@staticmethod
@@ -213,7 +257,11 @@ class PlainFieldSerializer(BaseModel):
@field_serializer('a', mode='plain')
@staticmethod
- def valid_staticmethod_info(value: Any, info: FieldSerializationInfo) -> Any: ...
+ def valid_staticmethod_info_default(value: Any, info: FieldSerializationInfo) -> Any: ...
+
+ @field_serializer('a', mode='plain')
+ @staticmethod
+ def valid_staticmethod_info(value: Any, info: FieldSerializationInfo[int]) -> Any: ...
@field_serializer('a', mode='plain')
@classmethod
@@ -221,7 +269,11 @@ class PlainFieldSerializer(BaseModel):
@field_serializer('a', mode='plain')
@classmethod
- def valid_classmethod_info(cls, value: Any, info: FieldSerializationInfo) -> Any: ...
+ def valid_classmethod_info_default(cls, value: Any, info: FieldSerializationInfo) -> Any: ...
+
+ @field_serializer('a', mode='plain')
+ @classmethod
+ def valid_classmethod_info(cls, value: Any, info: FieldSerializationInfo[int]) -> Any: ...
partial_ = field_serializer('a', mode='plain')(partial(lambda v, x: v, x=1))
@@ -250,4 +302,11 @@ class WrapFieldSerializer(BaseModel):
def valid_no_info(self, value: Any, handler: SerializerFunctionWrapHandler) -> Any: ...
@field_serializer('a', mode='wrap')
- def valid_info(self, value: Any, handler: SerializerFunctionWrapHandler, info: FieldSerializationInfo) -> Any: ...
+ def valid_info_default(
+ self, value: Any, handler: SerializerFunctionWrapHandler, info: FieldSerializationInfo
+ ) -> Any: ...
+
+ @field_serializer('a', mode='wrap')
+ def valid_info(
+ self, value: Any, handler: SerializerFunctionWrapHandler, info: FieldSerializationInfo[int]
+ ) -> Any: ...

View File

@@ -1,233 +0,0 @@
From cd0d37c4c18f24b5624ae86cfe5288cd82edf2c1 Mon Sep 17 00:00:00 2001
From: Douwe Maan <hi@douwe.me>
Date: Wed, 16 Apr 2025 18:01:58 +0000
Subject: [PATCH 1/4] Stop using deprecated field_name argument on validation
function schemas
---
docs/concepts/types.md | 2 +-
pydantic/_internal/_generate_schema.py | 45 ++++++++++----------------
pydantic/functional_validators.py | 5 +--
tests/test_validators.py | 2 +-
4 files changed, 20 insertions(+), 34 deletions(-)
Index: pydantic-2.11.7/docs/concepts/types.md
===================================================================
--- pydantic-2.11.7.orig/docs/concepts/types.md
+++ pydantic-2.11.7/docs/concepts/types.md
@@ -979,7 +979,7 @@ class CustomType:
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> core_schema.CoreSchema:
return core_schema.with_info_after_validator_function(
- cls.validate, handler(int), field_name=handler.field_name
+ cls.validate, handler(int)
)
Index: pydantic-2.11.7/pydantic/_internal/_generate_schema.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/_internal/_generate_schema.py
+++ pydantic-2.11.7/pydantic/_internal/_generate_schema.py
@@ -222,7 +222,6 @@ def filter_field_decorator_info_by_field
def apply_each_item_validators(
schema: core_schema.CoreSchema,
each_item_validators: list[Decorator[ValidatorDecoratorInfo]],
- field_name: str | None,
) -> core_schema.CoreSchema:
# This V1 compatibility shim should eventually be removed
@@ -234,21 +233,20 @@ def apply_each_item_validators(
# note that this won't work for any Annotated types that get wrapped by a function validator
# but that's okay because that didn't exist in V1
if schema['type'] == 'nullable':
- schema['schema'] = apply_each_item_validators(schema['schema'], each_item_validators, field_name)
+ schema['schema'] = apply_each_item_validators(schema['schema'], each_item_validators)
return schema
elif schema['type'] == 'tuple':
if (variadic_item_index := schema.get('variadic_item_index')) is not None:
schema['items_schema'][variadic_item_index] = apply_validators(
schema['items_schema'][variadic_item_index],
each_item_validators,
- field_name,
)
elif is_list_like_schema_with_items_schema(schema):
inner_schema = schema.get('items_schema', core_schema.any_schema())
- schema['items_schema'] = apply_validators(inner_schema, each_item_validators, field_name)
+ schema['items_schema'] = apply_validators(inner_schema, each_item_validators)
elif schema['type'] == 'dict':
inner_schema = schema.get('values_schema', core_schema.any_schema())
- schema['values_schema'] = apply_validators(inner_schema, each_item_validators, field_name)
+ schema['values_schema'] = apply_validators(inner_schema, each_item_validators)
else:
raise TypeError(
f'`@validator(..., each_item=True)` cannot be applied to fields with a schema of {schema["type"]}'
@@ -840,7 +838,7 @@ class GenerateSchema:
extras_keys_schema=extras_keys_schema,
model_name=cls.__name__,
)
- inner_schema = apply_validators(fields_schema, decorators.root_validators.values(), None)
+ inner_schema = apply_validators(fields_schema, decorators.root_validators.values())
inner_schema = apply_model_validators(inner_schema, model_validators, 'inner')
model_schema = core_schema.model_schema(
@@ -1380,9 +1378,9 @@ class GenerateSchema:
field_info.validate_default = True
each_item_validators = [v for v in this_field_validators if v.info.each_item is True]
this_field_validators = [v for v in this_field_validators if v not in each_item_validators]
- schema = apply_each_item_validators(schema, each_item_validators, name)
+ schema = apply_each_item_validators(schema, each_item_validators)
- schema = apply_validators(schema, this_field_validators, name)
+ schema = apply_validators(schema, this_field_validators)
# the default validator needs to go outside of any other validators
# so that it is the topmost validator for the field validator
@@ -1972,7 +1970,7 @@ class GenerateSchema:
collect_init_only=has_post_init,
)
- inner_schema = apply_validators(args_schema, decorators.root_validators.values(), None)
+ inner_schema = apply_validators(args_schema, decorators.root_validators.values())
model_validators = decorators.model_validators.values()
inner_schema = apply_model_validators(inner_schema, model_validators, 'inner')
@@ -2484,24 +2482,16 @@ class GenerateSchema:
_VALIDATOR_F_MATCH: Mapping[
tuple[FieldValidatorModes, Literal['no-info', 'with-info']],
- Callable[[Callable[..., Any], core_schema.CoreSchema, str | None], core_schema.CoreSchema],
+ Callable[[Callable[..., Any], core_schema.CoreSchema], core_schema.CoreSchema],
] = {
- ('before', 'no-info'): lambda f, schema, _: core_schema.no_info_before_validator_function(f, schema),
- ('after', 'no-info'): lambda f, schema, _: core_schema.no_info_after_validator_function(f, schema),
- ('plain', 'no-info'): lambda f, _1, _2: core_schema.no_info_plain_validator_function(f),
- ('wrap', 'no-info'): lambda f, schema, _: core_schema.no_info_wrap_validator_function(f, schema),
- ('before', 'with-info'): lambda f, schema, field_name: core_schema.with_info_before_validator_function(
- f, schema, field_name=field_name
- ),
- ('after', 'with-info'): lambda f, schema, field_name: core_schema.with_info_after_validator_function(
- f, schema, field_name=field_name
- ),
- ('plain', 'with-info'): lambda f, _, field_name: core_schema.with_info_plain_validator_function(
- f, field_name=field_name
- ),
- ('wrap', 'with-info'): lambda f, schema, field_name: core_schema.with_info_wrap_validator_function(
- f, schema, field_name=field_name
- ),
+ ('before', 'no-info'): lambda f, schema: core_schema.no_info_before_validator_function(f, schema),
+ ('after', 'no-info'): lambda f, schema: core_schema.no_info_after_validator_function(f, schema),
+ ('plain', 'no-info'): lambda f, _: core_schema.no_info_plain_validator_function(f),
+ ('wrap', 'no-info'): lambda f, schema: core_schema.no_info_wrap_validator_function(f, schema),
+ ('before', 'with-info'): lambda f, schema: core_schema.with_info_before_validator_function(f, schema),
+ ('after', 'with-info'): lambda f, schema: core_schema.with_info_after_validator_function(f, schema),
+ ('plain', 'with-info'): lambda f, _: core_schema.with_info_plain_validator_function(f),
+ ('wrap', 'with-info'): lambda f, schema: core_schema.with_info_wrap_validator_function(f, schema),
}
@@ -2512,7 +2502,6 @@ def apply_validators(
validators: Iterable[Decorator[RootValidatorDecoratorInfo]]
| Iterable[Decorator[ValidatorDecoratorInfo]]
| Iterable[Decorator[FieldValidatorDecoratorInfo]],
- field_name: str | None,
) -> core_schema.CoreSchema:
"""Apply validators to a schema.
@@ -2528,7 +2517,7 @@ def apply_validators(
info_arg = inspect_validator(validator.func, validator.info.mode)
val_type = 'with-info' if info_arg else 'no-info'
- schema = _VALIDATOR_F_MATCH[(validator.info.mode, val_type)](validator.func, schema, field_name)
+ schema = _VALIDATOR_F_MATCH[(validator.info.mode, val_type)](validator.func, schema)
return schema
Index: pydantic-2.11.7/pydantic/functional_validators.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/functional_validators.py
+++ pydantic-2.11.7/pydantic/functional_validators.py
@@ -75,7 +75,7 @@ class AfterValidator:
info_arg = _inspect_validator(self.func, 'after')
if info_arg:
func = cast(core_schema.WithInfoValidatorFunction, self.func)
- return core_schema.with_info_after_validator_function(func, schema=schema, field_name=handler.field_name)
+ return core_schema.with_info_after_validator_function(func, schema=schema)
else:
func = cast(core_schema.NoInfoValidatorFunction, self.func)
return core_schema.no_info_after_validator_function(func, schema=schema)
@@ -136,7 +136,6 @@ class BeforeValidator:
return core_schema.with_info_before_validator_function(
func,
schema=schema,
- field_name=handler.field_name,
json_schema_input_schema=input_schema,
)
else:
@@ -230,7 +229,6 @@ class PlainValidator:
func = cast(core_schema.WithInfoValidatorFunction, self.func)
return core_schema.with_info_plain_validator_function(
func,
- field_name=handler.field_name,
serialization=serialization, # pyright: ignore[reportArgumentType]
json_schema_input_schema=input_schema,
)
@@ -307,7 +305,6 @@ class WrapValidator:
return core_schema.with_info_wrap_validator_function(
func,
schema=schema,
- field_name=handler.field_name,
json_schema_input_schema=input_schema,
)
else:
Index: pydantic-2.11.7/tests/test_validators.py
===================================================================
--- pydantic-2.11.7.orig/tests/test_validators.py
+++ pydantic-2.11.7/tests/test_validators.py
@@ -21,7 +21,7 @@ from unittest.mock import MagicMock
import pytest
from dirty_equals import HasRepr, IsInstance
from pydantic_core import core_schema
-from typing_extensions import TypedDict
+from typing_extensions import TypeAliasType, TypedDict
from pydantic import (
BaseModel,
@@ -2684,7 +2684,7 @@ def foobar_validate(value: Any, info: co
class Foobar:
@classmethod
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
- return core_schema.with_info_plain_validator_function(foobar_validate, field_name=handler.field_name)
+ return core_schema.with_info_plain_validator_function(foobar_validate)
def test_custom_type_field_name_model():
@@ -2779,6 +2779,29 @@ def test_plain_validator_field_name():
assert m.foobar == {'value': '1', 'field_name': 'foobar', 'data': {'x': 123}}
+def test_validator_field_name_with_reused_type_alias():
+ calls = []
+
+ def validate_my_field(value: str, info: ValidationInfo):
+ calls.append((info.field_name, value))
+ return value
+
+ MyField = TypeAliasType('MyField', Annotated[str, AfterValidator(validate_my_field)])
+
+ class MyModel(BaseModel):
+ field1: MyField
+ field2: MyField
+
+ MyModel.model_validate(
+ {
+ 'field1': 'value1',
+ 'field2': 'value2',
+ }
+ )
+
+ assert calls == [('field1', 'value1'), ('field2', 'value2')]
+
+
def validate_wrap(value: Any, handler: core_schema.ValidatorFunctionWrapHandler, info: core_schema.ValidationInfo):
data = info.data
if isinstance(data, dict):

View File

@@ -1,566 +0,0 @@
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):

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e14caadd65cf15f778c90cd9f4878942b44aeaeaf3a6b595fbc88b0555df0dc1
size 3090332

3
pydantic-2.12.5.tar.gz Normal file
View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c79f06e47b8a49593a02ad0b3a0102089c4d22c55666183614dbbad33c12ae73
size 1976436

View File

@@ -1,3 +1,12 @@
-------------------------------------------------------------------
Wed Dec 3 20:31:21 UTC 2025 - Guang Yee <gyee@suse.com>
- Update to 2.12.5
* Fix pickle error when using model_construct() on a model with MISSING as a default value in #12522.
* Several updates to the documentation
- Remove patches bump-pydantic-core-2.35.1.patch, field-name-validator-core-schemas.patch,
py314.patch, and support-pydantic-core-2.39.0.patch as they've merged upstream.
-------------------------------------------------------------------
Mon Sep 29 12:28:15 UTC 2025 - Markéta Machová <mmachova@suse.com>

View File

@@ -27,25 +27,17 @@
%endif
%{?sle15_python_module_pythons}
Name: python-pydantic%{psuffix}
Version: 2.11.9
Version: 2.12.5
Release: 0
Summary: Data validation and settings management using python type hinting
License: MIT
URL: https://github.com/pydantic/pydantic
Source: https://github.com/pydantic/pydantic/archive/v%{version}.tar.gz#/pydantic-%{version}.tar.gz
# PATCH-FIX-UPSTREAM bump-pydantic-core-2.35.1.patch gh#pydantic/pydantic#11963
Patch0: bump-pydantic-core-2.35.1.patch
# PATCH-FIX-UPSTREAM field-name-validator-core-schemas.patch gh#pydantic/pydantic#11761
Patch1: field-name-validator-core-schemas.patch
# PATCH-FIX-UPSTREAM Based on gh#pydantic/pydantic#11883
Patch2: support-pydantic-core-2.39.0.patch
# PATCH-FIX-UPSTREAM https://github.com/pydantic/pydantic/pull/11991 Add initial support for Python 3.14
Patch3: py314.patch
BuildRequires: %{python_module hatch-fancy-pypi-readme}
BuildRequires: %{python_module hatchling}
BuildRequires: %{python_module packaging}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pydantic-core = 2.39.0}
BuildRequires: %{python_module pydantic-core = 2.41.5}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
@@ -65,9 +57,9 @@ BuildRequires: %{python_module python-dotenv >= 0.10.4}
BuildRequires: %{python_module rich}
BuildRequires: %{python_module typing-inspection}
%endif
Requires: python-annotated-types >= 0.4.0
Requires: python-pydantic-core = 2.39.0
Requires: python-typing-extensions >= 4.12.2
Requires: python-annotated-types >= 0.6.0
Requires: python-pydantic-core = 2.41.5
Requires: python-typing-extensions >= 4.14.1
Requires: python-typing-inspection
BuildArch: noarch
%python_subpackages
@@ -78,6 +70,10 @@ Data validation and settings management using Python type hinting.
%prep
%autosetup -p1 -n pydantic-%{version}
# FIXME: make it compatible with the older version of setuptools.
# make sure to remove this hack once we have a newer version of setuptools.
sed -i '/.*Programming Language :: Python :: 3\.14.*/d' pyproject.toml
%build
%pyproject_wheel

View File

@@ -1,293 +0,0 @@
From d6c65493a8436b22733d0f04d0bb3df1bc952ac9 Mon Sep 17 00:00:00 2001
From: Viicos <65306057+Viicos@users.noreply.github.com>
Date: Fri, 16 May 2025 15:46:24 +0200
Subject: [PATCH 1/8] Add `UNSET` sentinel
---
pydantic/_internal/_generate_schema.py | 3 +
pydantic/fields.py | 4 +-
pydantic/json_schema.py | 7 +-
pyproject.toml | 2 +-
5 files changed, 15 insertions(+), 122 deletions(-)
Index: pydantic-2.11.7/pydantic/_internal/_generate_schema.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/_internal/_generate_schema.py
+++ pydantic-2.11.7/pydantic/_internal/_generate_schema.py
@@ -42,6 +42,7 @@ from zoneinfo import ZoneInfo
import typing_extensions
from pydantic_core import (
+ MISSING,
CoreSchema,
MultiHostUrl,
PydanticCustomError,
@@ -1050,6 +1051,8 @@ class GenerateSchema:
return core_schema.multi_host_url_schema()
elif obj is None or obj is _typing_extra.NoneType:
return core_schema.none_schema()
+ if obj is MISSING:
+ return core_schema.missing_sentinel_schema()
elif obj in IP_TYPES:
return self._ip_schema(obj)
elif obj in TUPLE_TYPES:
Index: pydantic-2.11.7/pydantic/fields.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/fields.py
+++ pydantic-2.11.7/pydantic/fields.py
@@ -15,7 +15,7 @@ from warnings import warn
import annotated_types
import typing_extensions
-from pydantic_core import PydanticUndefined
+from pydantic_core import MISSING, PydanticUndefined
from typing_extensions import Self, TypeAlias, Unpack, deprecated
from typing_inspection import typing_objects
from typing_inspection.introspection import UNKNOWN, AnnotationSource, ForbiddenQualifier, Qualifier, inspect_annotation
@@ -392,7 +392,7 @@ class FieldInfo(_repr.Representation):
Returns:
A field object with the passed values.
"""
- if annotation is default:
+ if annotation is not MISSING and annotation is default:
raise PydanticUserError(
'Error when building FieldInfo from annotated attribute. '
"Make sure you don't have any field name clashing with a type annotation.",
Index: pydantic-2.11.7/pydantic/json_schema.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/json_schema.py
+++ pydantic-2.11.7/pydantic/json_schema.py
@@ -36,7 +36,7 @@ from typing import (
)
import pydantic_core
-from pydantic_core import CoreSchema, PydanticOmit, core_schema, to_jsonable_python
+from pydantic_core import MISSING, CoreSchema, PydanticOmit, core_schema, to_jsonable_python
from pydantic_core.core_schema import ComputedField
from typing_extensions import TypeAlias, assert_never, deprecated, final
from typing_inspection.introspection import get_literal_values
@@ -805,6 +805,17 @@ class GenerateJsonSchema:
result['type'] = 'null'
return result
+ def missing_sentinel_schema(self, schema: core_schema.MissingSentinelSchema) -> JsonSchemaValue:
+ """Generates a JSON schema that matches the `MISSING` sentinel value.
+
+ Args:
+ schema: The core schema.
+
+ Returns:
+ The generated JSON schema.
+ """
+ raise PydanticOmit
+
def enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue:
"""Generates a JSON schema that matches an Enum value.
@@ -1109,7 +1120,7 @@ class GenerateJsonSchema:
json_schema = self.generate_inner(schema['schema'])
default = self.get_default_value(schema)
- if default is NoDefault:
+ if default is NoDefault or default is MISSING:
return json_schema
# we reflect the application of custom plain, no-info serializers to defaults for
Index: pydantic-2.11.7/pydantic/version.py
===================================================================
--- pydantic-2.11.7.orig/pydantic/version.py
+++ pydantic-2.11.7/pydantic/version.py
@@ -66,7 +66,7 @@ def version_info() -> str:
def check_pydantic_core_version() -> bool:
"""Check that the installed `pydantic-core` dependency is compatible."""
# Keep this in sync with the version constraint in the `pyproject.toml` dependencies:
- return __pydantic_core_version__ == '2.35.1'
+ return __pydantic_core_version__ == '2.39.0'
def parse_mypy_version(version: str) -> tuple[int, int, int]:
Index: pydantic-2.11.7/docs/concepts/experimental.md
===================================================================
--- pydantic-2.11.7.orig/docs/concepts/experimental.md
+++ pydantic-2.11.7/docs/concepts/experimental.md
@@ -502,3 +502,49 @@ args, kwargs = val.validate_json('{"args
print(args, kwargs)
#> ('arg1',) {'extra': 1}
```
+
+## `MISSING` sentinel
+
+The `MISSING` sentinel is a singleton indicating a field value was not provided during validation.
+
+This singleton can be used as a default value, as an alternative to `None` when it has an explicit
+meaning. During serialization, any field with `MISSING` as a value is excluded from the output.
+
+```python
+from typing import Union
+
+from pydantic import BaseModel
+from pydantic.experimental.missing_sentinel import MISSING
+
+
+class Configuration(BaseModel):
+ timeout: Union[int, None, MISSING] = MISSING
+
+
+# configuration defaults, stored somewhere else:
+defaults = {'timeout': 200}
+
+conf = Configuration()
+
+# `timeout` is excluded from the serialization output:
+conf.model_dump()
+# {}
+
+# The `MISSING` value doesn't appear in the JSON Schema:
+Configuration.model_json_schema()['properties']['timeout']
+#> {'anyOf': [{'type': 'integer'}, {'type': 'null'}], 'title': 'Timeout'}}
+
+
+# `is` can be used to discrimate between the sentinel and other values:
+timeout = conf.timeout if conf.timeout is not MISSING else defaults['timeout']
+```
+
+This feature is marked as experimental because it relies on the draft [PEP 661](https://peps.python.org/pep-0661/), introducing sentinels in the standard library.
+
+As such, the following limitations currently apply:
+
+* Static type checking of sentinels is only supported with Pyright
+ [1.1.402](https://github.com/microsoft/pyright/releases/tag/1.1.402)
+ or greater, and the `enableExperimentalFeatures` type evaluation setting
+ should be enabled.
+* Pickling of models containing `MISSING` as a value is not supported.
Index: pydantic-2.11.7/docs/errors/validation_errors.md
===================================================================
--- pydantic-2.11.7.orig/docs/errors/validation_errors.md
+++ pydantic-2.11.7/docs/errors/validation_errors.md
@@ -1384,6 +1384,27 @@ except ValidationError as exc:
#> 'missing_positional_only_argument'
```
+## `missing_sentinel_error`
+
+This error is raised when the experimental `MISSING` sentinel is the only value allowed, and wasn't
+provided during validation:
+
+```python
+from pydantic import BaseModel, ValidationError
+from pydantic.experimental.missing_sentinel import MISSING
+
+
+class Model(BaseModel):
+ f: MISSING
+
+
+try:
+ Model(f=1)
+except ValidationError as exc:
+ print(repr(exc.errors()[0]['type']))
+ #> 'missing_sentinel_error'
+```
+
## `model_attributes_type`
This error is raised when the input value is not a valid dictionary, model instance, or instance that fields can be extracted from:
Index: pydantic-2.11.7/pydantic/experimental/missing_sentinel.py
===================================================================
--- /dev/null
+++ pydantic-2.11.7/pydantic/experimental/missing_sentinel.py
@@ -0,0 +1,5 @@
+"""Experimental module exposing a function a `MISSING` sentinel."""
+
+from pydantic_core import MISSING
+
+__all__ = ('MISSING',)
Index: pydantic-2.11.7/pyproject.toml
===================================================================
--- pydantic-2.11.7.orig/pyproject.toml
+++ pydantic-2.11.7/pyproject.toml
@@ -46,7 +46,7 @@ dependencies = [
'typing-extensions>=4.13.0',
'annotated-types>=0.6.0',
# Keep this in sync with the version in the `check_pydantic_core_version()` function:
- 'pydantic-core==2.35.1',
+ 'pydantic-core==2.39.0',
'typing-inspection>=0.4.0',
]
dynamic = ['version', 'readme']
Index: pydantic-2.11.7/tests/test_missing_sentinel.py
===================================================================
--- /dev/null
+++ pydantic-2.11.7/tests/test_missing_sentinel.py
@@ -0,0 +1,71 @@
+import pickle
+from typing import Union
+
+import pytest
+from pydantic_core import MISSING, PydanticSerializationUnexpectedValue
+
+from pydantic import BaseModel, TypeAdapter, ValidationError
+
+
+def test_missing_sentinel_model() -> None:
+ class Model(BaseModel):
+ f: Union[int, MISSING] = MISSING
+ g: MISSING = MISSING
+
+ m1 = Model()
+
+ assert m1.model_dump() == {}
+ assert m1.model_dump_json() == '{}'
+
+ m2 = Model.model_validate({'f': MISSING, 'g': MISSING})
+
+ assert m2.f is MISSING
+ assert m2.g is MISSING
+
+ m3 = Model(f=1)
+
+ assert m3.model_dump() == {'f': 1}
+ assert m3.model_dump_json() == '{"f":1}'
+
+
+def test_missing_sentinel_type_adapter() -> None:
+ """Note that this usage isn't explicitly supported (and useless in practice)."""
+
+ # TODO Remove annotation with PEP 747:
+ ta: TypeAdapter[object] = TypeAdapter(MISSING)
+
+ assert ta.validate_python(MISSING) is MISSING
+
+ with pytest.raises(ValidationError) as exc_info:
+ ta.validate_python(1)
+
+ assert exc_info.value.errors()[0]['type'] == 'missing_sentinel_error'
+
+ assert ta.dump_python(MISSING) is MISSING
+
+ with pytest.raises(PydanticSerializationUnexpectedValue):
+ ta.dump_python(1)
+
+
+# Defined in module to be picklable:
+class ModelPickle(BaseModel):
+ f: Union[int, MISSING] = MISSING
+
+
+@pytest.mark.xfail(reason="PEP 661 sentinels aren't picklable yet in the experimental typing-extensions implementation")
+def test_missing_sentinel_pickle() -> None:
+ m = ModelPickle()
+ m_reconstructed = pickle.loads(pickle.dumps(m))
+
+ assert m_reconstructed.f is MISSING
+
+
+def test_missing_sentinel_json_schema() -> None:
+ class Model(BaseModel):
+ f: Union[int, MISSING] = MISSING
+ g: MISSING = MISSING
+ h: MISSING
+
+ assert Model.model_json_schema()['properties'] == {
+ 'f': {'title': 'F', 'type': 'integer'},
+ }