Загрузить файлы в «venv/Lib/site-packages/pydantic/_internal»

This commit is contained in:
2026-07-02 20:01:00 +00:00
parent 6ef733a1c2
commit 211715785a
5 changed files with 1799 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import sys
# `slots` is available on Python >= 3.10
if sys.version_info >= (3, 10):
slots_true = {'slots': True}
else:
slots_true = {}

View File

@@ -0,0 +1,403 @@
from __future__ import annotations
from collections import defaultdict
from collections.abc import Iterable
from copy import copy
from functools import lru_cache, partial
from typing import TYPE_CHECKING, Any
from pydantic_core import CoreSchema, PydanticCustomError, ValidationError, to_jsonable_python
from pydantic_core import core_schema as cs
from ._fields import PydanticMetadata
from ._import_utils import import_cached_field_info
if TYPE_CHECKING:
pass
STRICT = {'strict'}
FAIL_FAST = {'fail_fast'}
LENGTH_CONSTRAINTS = {'min_length', 'max_length'}
INEQUALITY = {'le', 'ge', 'lt', 'gt'}
NUMERIC_CONSTRAINTS = {'multiple_of', *INEQUALITY}
ALLOW_INF_NAN = {'allow_inf_nan'}
STR_CONSTRAINTS = {
*LENGTH_CONSTRAINTS,
*STRICT,
'strip_whitespace',
'to_lower',
'to_upper',
'pattern',
'coerce_numbers_to_str',
'ascii_only',
}
BYTES_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT}
LIST_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST}
TUPLE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST}
SET_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT, *FAIL_FAST}
DICT_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT}
GENERATOR_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *STRICT}
SEQUENCE_CONSTRAINTS = {*LENGTH_CONSTRAINTS, *FAIL_FAST}
FLOAT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT}
DECIMAL_CONSTRAINTS = {'max_digits', 'decimal_places', *FLOAT_CONSTRAINTS}
INT_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *ALLOW_INF_NAN, *STRICT}
BOOL_CONSTRAINTS = STRICT
UUID_CONSTRAINTS = STRICT
DATE_TIME_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT}
TIMEDELTA_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT}
TIME_CONSTRAINTS = {*NUMERIC_CONSTRAINTS, *STRICT}
LAX_OR_STRICT_CONSTRAINTS = STRICT
ENUM_CONSTRAINTS = STRICT
COMPLEX_CONSTRAINTS = STRICT
UNION_CONSTRAINTS = {'union_mode'}
URL_CONSTRAINTS = {
'max_length',
'allowed_schemes',
'host_required',
'default_host',
'default_port',
'default_path',
}
TEXT_SCHEMA_TYPES = ('str', 'bytes', 'url', 'multi-host-url')
SEQUENCE_SCHEMA_TYPES = ('list', 'tuple', 'set', 'frozenset', 'generator', *TEXT_SCHEMA_TYPES)
NUMERIC_SCHEMA_TYPES = ('float', 'int', 'date', 'time', 'timedelta', 'datetime')
CONSTRAINTS_TO_ALLOWED_SCHEMAS: dict[str, set[str]] = defaultdict(set)
constraint_schema_pairings: list[tuple[set[str], tuple[str, ...]]] = [
(STR_CONSTRAINTS, TEXT_SCHEMA_TYPES),
(BYTES_CONSTRAINTS, ('bytes',)),
(LIST_CONSTRAINTS, ('list',)),
(TUPLE_CONSTRAINTS, ('tuple',)),
(SET_CONSTRAINTS, ('set', 'frozenset')),
(DICT_CONSTRAINTS, ('dict',)),
(GENERATOR_CONSTRAINTS, ('generator',)),
(FLOAT_CONSTRAINTS, ('float',)),
(INT_CONSTRAINTS, ('int',)),
(DATE_TIME_CONSTRAINTS, ('date', 'time', 'datetime', 'timedelta')),
# TODO: this is a bit redundant, we could probably avoid some of these
(STRICT, (*TEXT_SCHEMA_TYPES, *SEQUENCE_SCHEMA_TYPES, *NUMERIC_SCHEMA_TYPES, 'typed-dict', 'model')),
(UNION_CONSTRAINTS, ('union',)),
(URL_CONSTRAINTS, ('url', 'multi-host-url')),
(BOOL_CONSTRAINTS, ('bool',)),
(UUID_CONSTRAINTS, ('uuid',)),
(LAX_OR_STRICT_CONSTRAINTS, ('lax-or-strict',)),
(ENUM_CONSTRAINTS, ('enum',)),
(DECIMAL_CONSTRAINTS, ('decimal',)),
(COMPLEX_CONSTRAINTS, ('complex',)),
]
for constraints, schemas in constraint_schema_pairings:
for c in constraints:
CONSTRAINTS_TO_ALLOWED_SCHEMAS[c].update(schemas)
def as_jsonable_value(v: Any) -> Any:
if type(v) not in (int, str, float, bytes, bool, type(None)):
return to_jsonable_python(v)
return v
def expand_grouped_metadata(annotations: Iterable[Any]) -> Iterable[Any]:
"""Expand the annotations.
Args:
annotations: An iterable of annotations.
Returns:
An iterable of expanded annotations.
Example:
```python
from annotated_types import Ge, Len
from pydantic._internal._known_annotated_metadata import expand_grouped_metadata
print(list(expand_grouped_metadata([Ge(4), Len(5)])))
#> [Ge(ge=4), MinLen(min_length=5)]
```
"""
import annotated_types as at
FieldInfo = import_cached_field_info()
for annotation in annotations:
if isinstance(annotation, at.GroupedMetadata):
yield from annotation
elif isinstance(annotation, FieldInfo):
yield from annotation.metadata
# this is a bit problematic in that it results in duplicate metadata
# all of our "consumers" can handle it, but it is not ideal
# we probably should split up FieldInfo into:
# - annotated types metadata
# - individual metadata known only to Pydantic
annotation = copy(annotation)
annotation.metadata = []
yield annotation
else:
yield annotation
@lru_cache
def _get_at_to_constraint_map() -> dict[type, str]:
"""Return a mapping of annotated types to constraints.
Normally, we would define a mapping like this in the module scope, but we can't do that
because we don't permit module level imports of `annotated_types`, in an attempt to speed up
the import time of `pydantic`. We still only want to have this dictionary defined in one place,
so we use this function to cache the result.
"""
import annotated_types as at
return {
at.Gt: 'gt',
at.Ge: 'ge',
at.Lt: 'lt',
at.Le: 'le',
at.MultipleOf: 'multiple_of',
at.MinLen: 'min_length',
at.MaxLen: 'max_length',
}
def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | None: # noqa: C901
"""Apply `annotation` to `schema` if it is an annotation we know about (Gt, Le, etc.).
Otherwise return `None`.
This does not handle all known annotations. If / when it does, it can always
return a CoreSchema and return the unmodified schema if the annotation should be ignored.
Assumes that GroupedMetadata has already been expanded via `expand_grouped_metadata`.
Args:
annotation: The annotation.
schema: The schema.
Returns:
An updated schema with annotation if it is an annotation we know about, `None` otherwise.
Raises:
RuntimeError: If a constraint can't be applied to a specific schema type.
ValueError: If an unknown constraint is encountered.
"""
import annotated_types as at
from ._validators import NUMERIC_VALIDATOR_LOOKUP, forbid_inf_nan_check
schema = schema.copy()
schema_update, other_metadata = collect_known_metadata([annotation])
schema_type = schema['type']
chain_schema_constraints: set[str] = {
'pattern',
'strip_whitespace',
'to_lower',
'to_upper',
'coerce_numbers_to_str',
'ascii_only',
}
chain_schema_steps: list[CoreSchema] = []
for constraint, value in schema_update.items():
if constraint not in CONSTRAINTS_TO_ALLOWED_SCHEMAS:
raise ValueError(f'Unknown constraint {constraint}')
allowed_schemas = CONSTRAINTS_TO_ALLOWED_SCHEMAS[constraint]
# if it becomes necessary to handle more than one constraint
# in this recursive case with function-after or function-wrap, we should refactor
# this is a bit challenging because we sometimes want to apply constraints to the inner schema,
# whereas other times we want to wrap the existing schema with a new one that enforces a new constraint.
if schema_type in {'function-before', 'function-wrap', 'function-after'} and constraint == 'strict':
schema['schema'] = apply_known_metadata(annotation, schema['schema']) # type: ignore # schema is function schema
return schema
# if we're allowed to apply constraint directly to the schema, like le to int, do that
if schema_type in allowed_schemas:
if constraint == 'union_mode' and schema_type == 'union':
schema['mode'] = value # type: ignore # schema is UnionSchema
else:
schema[constraint] = value
continue
# else, apply a function after validator to the schema to enforce the corresponding constraint
if constraint in chain_schema_constraints:
def _apply_constraint_with_incompatibility_info(
value: Any, handler: cs.ValidatorFunctionWrapHandler
) -> Any:
try:
x = handler(value)
except ValidationError as ve:
# if the error is about the type, it's likely that the constraint is incompatible the type of the field
# for example, the following invalid schema wouldn't be caught during schema build, but rather at this point
# with a cryptic 'string_type' error coming from the string validator,
# that we'd rather express as a constraint incompatibility error (TypeError)
# Annotated[list[int], Field(pattern='abc')]
if 'type' in ve.errors()[0]['type']:
raise TypeError(
f"Unable to apply constraint '{constraint}' to supplied value {value} for schema of type '{schema_type}'" # noqa: B023
)
raise ve
return x
chain_schema_steps.append(
cs.no_info_wrap_validator_function(
_apply_constraint_with_incompatibility_info, cs.str_schema(**{constraint: value})
)
)
elif constraint in NUMERIC_VALIDATOR_LOOKUP:
if constraint in LENGTH_CONSTRAINTS:
inner_schema = schema
while inner_schema['type'] in {'function-before', 'function-wrap', 'function-after'}:
inner_schema = inner_schema['schema'] # type: ignore
inner_schema_type = inner_schema['type']
if inner_schema_type == 'list' or (
inner_schema_type == 'json-or-python' and inner_schema['json_schema']['type'] == 'list' # type: ignore
):
js_constraint_key = 'minItems' if constraint == 'min_length' else 'maxItems'
else:
js_constraint_key = 'minLength' if constraint == 'min_length' else 'maxLength'
else:
js_constraint_key = constraint
schema = cs.no_info_after_validator_function(
partial(NUMERIC_VALIDATOR_LOOKUP[constraint], **{constraint: value}), schema
)
metadata = schema.get('metadata', {})
if (existing_json_schema_updates := metadata.get('pydantic_js_updates')) is not None:
metadata['pydantic_js_updates'] = {
**existing_json_schema_updates,
**{js_constraint_key: as_jsonable_value(value)},
}
else:
metadata['pydantic_js_updates'] = {js_constraint_key: as_jsonable_value(value)}
schema['metadata'] = metadata
elif constraint == 'allow_inf_nan' and value is False:
schema = cs.no_info_after_validator_function(
forbid_inf_nan_check,
schema,
)
else:
# It's rare that we'd get here, but it's possible if we add a new constraint and forget to handle it
# Most constraint errors are caught at runtime during attempted application
raise RuntimeError(f"Unable to apply constraint '{constraint}' to schema of type '{schema_type}'")
for annotation in other_metadata:
if (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()):
constraint = at_to_constraint_map[annotation_type]
validator = NUMERIC_VALIDATOR_LOOKUP.get(constraint)
if validator is None:
raise ValueError(f'Unknown constraint {constraint}')
schema = cs.no_info_after_validator_function(
partial(validator, {constraint: getattr(annotation, constraint)}), schema
)
continue
elif isinstance(annotation, (at.Predicate, at.Not)):
predicate_name = f'{annotation.func.__qualname__!r} ' if hasattr(annotation.func, '__qualname__') else ''
# Note: B023 is ignored because even though we iterate over `other_metadata`, it is guaranteed
# to be of length 1. `apply_known_metadata()` is called from `GenerateSchema`, where annotations
# were already expanded via `expand_grouped_metadata()`. Confusing, but this falls into the annotations
# refactor.
if isinstance(annotation, at.Predicate):
def val_func(v: Any) -> Any:
predicate_satisfied = annotation.func(v) # noqa: B023
if not predicate_satisfied:
raise PydanticCustomError(
'predicate_failed',
f'Predicate {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023
)
return v
else:
def val_func(v: Any) -> Any:
predicate_satisfied = annotation.func(v) # noqa: B023
if predicate_satisfied:
raise PydanticCustomError(
'not_operation_failed',
f'Not of {predicate_name}failed', # pyright: ignore[reportArgumentType] # noqa: B023
)
return v
schema = cs.no_info_after_validator_function(val_func, schema)
else:
# ignore any other unknown metadata
return None
if chain_schema_steps:
chain_schema_steps = [schema] + chain_schema_steps
return cs.chain_schema(chain_schema_steps)
return schema
def collect_known_metadata(annotations: Iterable[Any]) -> tuple[dict[str, Any], list[Any]]:
"""Split `annotations` into known metadata and unknown annotations.
Args:
annotations: An iterable of annotations.
Returns:
A tuple contains a dict of known metadata and a list of unknown annotations.
Example:
```python
from annotated_types import Gt, Len
from pydantic._internal._known_annotated_metadata import collect_known_metadata
print(collect_known_metadata([Gt(1), Len(42), ...]))
#> ({'gt': 1, 'min_length': 42}, [Ellipsis])
```
"""
annotations = expand_grouped_metadata(annotations)
res: dict[str, Any] = {}
remaining: list[Any] = []
for annotation in annotations:
# isinstance(annotation, PydanticMetadata) also covers ._fields:_PydanticGeneralMetadata
if isinstance(annotation, PydanticMetadata):
res.update(annotation.__dict__)
# we don't use dataclasses.asdict because that recursively calls asdict on the field values
elif (annotation_type := type(annotation)) in (at_to_constraint_map := _get_at_to_constraint_map()):
constraint = at_to_constraint_map[annotation_type]
res[constraint] = getattr(annotation, constraint)
elif isinstance(annotation, type) and issubclass(annotation, PydanticMetadata):
# also support PydanticMetadata classes being used without initialisation,
# e.g. `Annotated[int, Strict]` as well as `Annotated[int, Strict()]`
res.update({k: v for k, v in vars(annotation).items() if not k.startswith('_')})
else:
remaining.append(annotation)
# Nones can sneak in but pydantic-core will reject them
# it'd be nice to clean things up so we don't put in None (we probably don't _need_ to, it was just easier)
# but this is simple enough to kick that can down the road
res = {k: v for k, v in res.items() if v is not None}
return res, remaining
def check_metadata(metadata: dict[str, Any], allowed: Iterable[str], source_type: Any) -> None:
"""A small utility function to validate that the given metadata can be applied to the target.
More than saving lines of code, this gives us a consistent error message for all of our internal implementations.
Args:
metadata: A dict of metadata.
allowed: An iterable of allowed metadata.
source_type: The source type.
Raises:
TypeError: If there is metadatas that can't be applied on source type.
"""
unknown = metadata.keys() - set(allowed)
if unknown:
raise TypeError(
f'The following constraints cannot be applied to {source_type!r}: {", ".join([f"{k!r}" for k in unknown])}'
)

View File

@@ -0,0 +1,228 @@
from __future__ import annotations
from collections.abc import Iterator, Mapping
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar, Union
from pydantic_core import CoreSchema, SchemaSerializer, SchemaValidator
from ..errors import PydanticErrorCodes, PydanticUserError
from ..plugin._schema_validator import PluggableSchemaValidator
if TYPE_CHECKING:
from ..dataclasses import PydanticDataclass
from ..main import BaseModel
from ..type_adapter import TypeAdapter
ValSer = TypeVar('ValSer', bound=Union[SchemaValidator, PluggableSchemaValidator, SchemaSerializer])
T = TypeVar('T')
class MockCoreSchema(Mapping[str, Any]):
"""Mocker for `pydantic_core.CoreSchema` which optionally attempts to
rebuild the thing it's mocking when one of its methods is accessed and raises an error if that fails.
"""
__slots__ = '_error_message', '_code', '_attempt_rebuild', '_built_memo'
def __init__(
self,
error_message: str,
*,
code: PydanticErrorCodes,
attempt_rebuild: Callable[[], CoreSchema | None] | None = None,
) -> None:
self._error_message = error_message
self._code: PydanticErrorCodes = code
self._attempt_rebuild = attempt_rebuild
self._built_memo: CoreSchema | None = None
def __getitem__(self, key: str) -> Any:
return self._get_built().__getitem__(key)
def __len__(self) -> int:
return self._get_built().__len__()
def __iter__(self) -> Iterator[str]:
return self._get_built().__iter__()
def _get_built(self) -> CoreSchema:
if self._built_memo is not None:
return self._built_memo
if self._attempt_rebuild:
schema = self._attempt_rebuild()
if schema is not None:
self._built_memo = schema
return schema
raise PydanticUserError(self._error_message, code=self._code)
def rebuild(self) -> CoreSchema | None:
self._built_memo = None
if self._attempt_rebuild:
schema = self._attempt_rebuild()
if schema is not None:
return schema
else:
raise PydanticUserError(self._error_message, code=self._code)
return None
class MockValSer(Generic[ValSer]):
"""Mocker for `pydantic_core.SchemaValidator` or `pydantic_core.SchemaSerializer` which optionally attempts to
rebuild the thing it's mocking when one of its methods is accessed and raises an error if that fails.
"""
__slots__ = '_error_message', '_code', '_val_or_ser', '_attempt_rebuild'
def __init__(
self,
error_message: str,
*,
code: PydanticErrorCodes,
val_or_ser: Literal['validator', 'serializer'],
attempt_rebuild: Callable[[], ValSer | None] | None = None,
) -> None:
self._error_message = error_message
self._val_or_ser = SchemaValidator if val_or_ser == 'validator' else SchemaSerializer
self._code: PydanticErrorCodes = code
self._attempt_rebuild = attempt_rebuild
def __getattr__(self, item: str) -> None:
__tracebackhide__ = True
if self._attempt_rebuild:
val_ser = self._attempt_rebuild()
if val_ser is not None:
return getattr(val_ser, item)
# raise an AttributeError if `item` doesn't exist
getattr(self._val_or_ser, item)
raise PydanticUserError(self._error_message, code=self._code)
def rebuild(self) -> ValSer | None:
if self._attempt_rebuild:
val_ser = self._attempt_rebuild()
if val_ser is not None:
return val_ser
else:
raise PydanticUserError(self._error_message, code=self._code)
return None
def set_type_adapter_mocks(adapter: TypeAdapter) -> None:
"""Set `core_schema`, `validator` and `serializer` to mock core types on a type adapter instance.
Args:
adapter: The type adapter instance to set the mocks on
"""
type_repr = str(adapter._type)
undefined_type_error_message = (
f'`TypeAdapter[{type_repr}]` is not fully defined; you should define `{type_repr}` and all referenced types,'
f' then call `.rebuild()` on the instance.'
)
def attempt_rebuild_fn(attr_fn: Callable[[TypeAdapter], T]) -> Callable[[], T | None]:
def handler() -> T | None:
if adapter.rebuild(raise_errors=False, _parent_namespace_depth=5) is not False:
return attr_fn(adapter)
return None
return handler
adapter.core_schema = MockCoreSchema( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
attempt_rebuild=attempt_rebuild_fn(lambda ta: ta.core_schema),
)
adapter.validator = MockValSer( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='validator',
attempt_rebuild=attempt_rebuild_fn(lambda ta: ta.validator),
)
adapter.serializer = MockValSer( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='serializer',
attempt_rebuild=attempt_rebuild_fn(lambda ta: ta.serializer),
)
def set_model_mocks(cls: type[BaseModel], undefined_name: str = 'all referenced types') -> None:
"""Set `__pydantic_core_schema__`, `__pydantic_validator__` and `__pydantic_serializer__` to mock core types on a model.
Args:
cls: The model class to set the mocks on
undefined_name: Name of the undefined thing, used in error messages
"""
undefined_type_error_message = (
f'`{cls.__name__}` is not fully defined; you should define {undefined_name},'
f' then call `{cls.__name__}.model_rebuild()`.'
)
def attempt_rebuild_fn(attr_fn: Callable[[type[BaseModel]], T]) -> Callable[[], T | None]:
def handler() -> T | None:
if cls.model_rebuild(raise_errors=False, _parent_namespace_depth=5) is not False:
return attr_fn(cls)
return None
return handler
cls.__pydantic_core_schema__ = MockCoreSchema( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_core_schema__),
)
cls.__pydantic_validator__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='validator',
attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_validator__),
)
cls.__pydantic_serializer__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='serializer',
attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_serializer__),
)
def set_dataclass_mocks(cls: type[PydanticDataclass], undefined_name: str = 'all referenced types') -> None:
"""Set `__pydantic_validator__` and `__pydantic_serializer__` to `MockValSer`s on a dataclass.
Args:
cls: The model class to set the mocks on
undefined_name: Name of the undefined thing, used in error messages
"""
from ..dataclasses import rebuild_dataclass
undefined_type_error_message = (
f'`{cls.__name__}` is not fully defined; you should define {undefined_name},'
f' then call `pydantic.dataclasses.rebuild_dataclass({cls.__name__})`.'
)
def attempt_rebuild_fn(attr_fn: Callable[[type[PydanticDataclass]], T]) -> Callable[[], T | None]:
def handler() -> T | None:
if rebuild_dataclass(cls, raise_errors=False, _parent_namespace_depth=5) is not False:
return attr_fn(cls)
return None
return handler
cls.__pydantic_core_schema__ = MockCoreSchema( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_core_schema__),
)
cls.__pydantic_validator__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='validator',
attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_validator__),
)
cls.__pydantic_serializer__ = MockValSer( # pyright: ignore[reportAttributeAccessIssue]
undefined_type_error_message,
code='class-not-fully-defined',
val_or_ser='serializer',
attempt_rebuild=attempt_rebuild_fn(lambda c: c.__pydantic_serializer__),
)

View File

@@ -0,0 +1,868 @@
"""Private logic for creating models."""
from __future__ import annotations as _annotations
import operator
import sys
import typing
import warnings
import weakref
from abc import ABCMeta
from functools import cache, partial, wraps
from types import FunctionType
from typing import TYPE_CHECKING, Any, Callable, Generic, Literal, NoReturn, TypeVar, cast
from pydantic_core import PydanticUndefined, SchemaSerializer
from typing_extensions import TypeAliasType, dataclass_transform, deprecated, get_args, get_origin
from typing_inspection import typing_objects
from ..errors import PydanticUndefinedAnnotation, PydanticUserError
from ..plugin._schema_validator import create_schema_validator
from ..warnings import GenericBeforeBaseModelWarning, PydanticDeprecatedSince20
from ._config import ConfigWrapper
from ._decorators import DecoratorInfos, PydanticDescriptorProxy, get_attribute_from_bases, unwrap_wrapped_function
from ._fields import collect_model_fields, is_valid_field_name, is_valid_privateattr_name, rebuild_model_fields
from ._generate_schema import GenerateSchema, InvalidSchemaError
from ._generics import PydanticGenericMetadata, get_model_typevars_map
from ._import_utils import import_cached_base_model, import_cached_field_info
from ._mock_val_ser import set_model_mocks
from ._namespace_utils import NsResolver
from ._signature import generate_pydantic_signature
from ._typing_extra import (
_make_forward_ref,
eval_type_backport,
is_classvar_annotation,
parent_frame_namespace,
)
from ._utils import LazyClassAttribute, SafeGetItemProxy
if TYPE_CHECKING:
from ..fields import Field as PydanticModelField
from ..fields import FieldInfo, ModelPrivateAttr
from ..fields import PrivateAttr as PydanticModelPrivateAttr
from ..main import BaseModel
from ._fields import PydanticExtraInfo
else:
PydanticModelField = object()
PydanticModelPrivateAttr = object()
object_setattr = object.__setattr__
class _ModelNamespaceDict(dict):
"""A dictionary subclass that intercepts attribute setting on model classes and
warns about overriding of decorators.
"""
def __setitem__(self, k: str, v: object) -> None:
existing: Any = self.get(k, None)
if existing and v is not existing and isinstance(existing, PydanticDescriptorProxy):
warnings.warn(
f'`{k}` overrides an existing Pydantic `{existing.decorator_info.decorator_repr}` decorator',
stacklevel=2,
)
return super().__setitem__(k, v)
def NoInitField(
*,
init: Literal[False] = False,
) -> Any:
"""Only for typing purposes. Used as default value of `__pydantic_fields_set__`,
`__pydantic_extra__`, `__pydantic_private__`, so they could be ignored when
synthesizing the `__init__` signature.
"""
# For ModelMetaclass.register():
_T = TypeVar('_T')
@dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField, PydanticModelPrivateAttr, NoInitField))
class ModelMetaclass(ABCMeta):
def __new__(
mcs,
cls_name: str,
bases: tuple[type[Any], ...],
namespace: dict[str, Any],
__pydantic_generic_metadata__: PydanticGenericMetadata | None = None,
__pydantic_reset_parent_namespace__: bool = True,
_create_model_module: str | None = None,
**kwargs: Any,
) -> type:
"""Metaclass for creating Pydantic models.
Args:
cls_name: The name of the class to be created.
bases: The base classes of the class to be created.
namespace: The attribute dictionary of the class to be created.
__pydantic_generic_metadata__: Metadata for generic models.
__pydantic_reset_parent_namespace__: Reset parent namespace.
_create_model_module: The module of the class to be created, if created by `create_model`.
**kwargs: Catch-all for any other keyword arguments.
Returns:
The new class created by the metaclass.
"""
# Note `ModelMetaclass` refers to `BaseModel`, but is also used to *create* `BaseModel`, so we rely on the fact
# 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, raw_annotations, kwargs)
namespace['model_config'] = config_wrapper.config_dict
private_attributes = inspect_namespace(
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)
if original_model_post_init is not None:
# if there are private attributes and a model_post_init function, we handle both
@wraps(original_model_post_init)
def wrapped_model_post_init(self: BaseModel, context: Any, /) -> None:
"""We need to both initialize private attributes and call the user-defined model_post_init
method.
"""
init_private_attributes(self, context)
original_model_post_init(self, context)
namespace['model_post_init'] = wrapped_model_post_init
else:
namespace['model_post_init'] = init_private_attributes
namespace['__class_vars__'] = class_vars
namespace['__private_attributes__'] = {**base_private_attributes, **private_attributes}
cls = cast('type[BaseModel]', super().__new__(mcs, cls_name, bases, namespace, **kwargs))
BaseModel_ = import_cached_base_model()
mro = cls.__mro__
if Generic in mro and mro.index(Generic) < mro.index(BaseModel_):
warnings.warn(
GenericBeforeBaseModelWarning(
'Classes should inherit from `BaseModel` before generic classes (e.g. `typing.Generic[T]`) '
'for pydantic generics to work properly.'
),
stacklevel=2,
)
cls.__pydantic_custom_init__ = not getattr(cls.__init__, '__pydantic_base_init__', False)
cls.__pydantic_post_init__ = (
None if cls.model_post_init is BaseModel_.model_post_init else 'model_post_init'
)
cls.__pydantic_setattr_handlers__ = {}
cls.__pydantic_decorators__ = DecoratorInfos.build(cls, replace_wrapped_methods=True)
cls.__pydantic_decorators__.update_from_config(config_wrapper)
# Use the getattr below to grab the __parameters__ from the `typing.Generic` parent class
if __pydantic_generic_metadata__:
cls.__pydantic_generic_metadata__ = __pydantic_generic_metadata__
else:
parent_parameters = getattr(cls, '__pydantic_generic_metadata__', {}).get('parameters', ())
parameters = getattr(cls, '__parameters__', None) or parent_parameters
if parameters and parent_parameters and not all(x in parameters for x in parent_parameters):
from ..root_model import RootModelRootType
missing_parameters = tuple(x for x in parameters if x not in parent_parameters)
if RootModelRootType in parent_parameters and RootModelRootType not in parameters:
# This is a special case where the user has subclassed RootModel, but has not parameterized
# RootModel with the generic type identifiers being used. Ex:
# class MyModel(RootModel, Generic[T]):
# root: T
# Should instead just be:
# class MyModel(RootModel[T]):
# root: T
parameters_str = ', '.join([x.__name__ for x in missing_parameters])
error_message = (
f'{cls.__name__} is a subclass of `RootModel`, but does not include the generic type identifier(s) '
f'{parameters_str} in its parameters. '
f'You should parametrize RootModel directly, e.g., `class {cls.__name__}(RootModel[{parameters_str}]): ...`.'
)
else:
combined_parameters = parent_parameters + missing_parameters
parameters_str = ', '.join([str(x) for x in combined_parameters])
generic_type_label = f'typing.Generic[{parameters_str}]'
error_message = (
f'All parameters must be present on typing.Generic;'
f' you should inherit from {generic_type_label}.'
)
if Generic not in bases: # pragma: no cover
# We raise an error here not because it is desirable, but because some cases are mishandled.
# It would be nice to remove this error and still have things behave as expected, it's just
# challenging because we are using a custom `__class_getitem__` to parametrize generic models,
# and not returning a typing._GenericAlias from it.
bases_str = ', '.join([x.__name__ for x in bases] + [generic_type_label])
error_message += (
f' Note: `typing.Generic` must go last: `class {cls.__name__}({bases_str}): ...`)'
)
raise TypeError(error_message)
cls.__pydantic_generic_metadata__ = {
'origin': None,
'args': (),
'parameters': parameters,
}
cls.__pydantic_complete__ = False # Ensure this specific class gets completed
# preserve `__set_name__` protocol defined in https://peps.python.org/pep-0487
# for attributes not in `new_namespace` (e.g. private attributes)
for name, obj in private_attributes.items():
obj.__set_name__(cls, name)
if __pydantic_reset_parent_namespace__:
cls.__pydantic_parent_namespace__ = build_lenient_weakvaluedict(parent_frame_namespace())
parent_namespace: dict[str, Any] | None = getattr(cls, '__pydantic_parent_namespace__', None)
if isinstance(parent_namespace, dict):
parent_namespace = unpack_lenient_weakvaluedict(parent_namespace)
ns_resolver = NsResolver(parent_namespace=parent_namespace)
set_model_fields(cls, config_wrapper=config_wrapper, ns_resolver=ns_resolver)
# This is also set in `complete_model_class()`, after schema gen because they are recreated.
# We set them here as well for backwards compatibility:
cls.__pydantic_computed_fields__ = {
k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()
}
if config_wrapper.defer_build:
set_model_mocks(cls)
else:
# Any operation that requires accessing the field infos instances should be put inside
# `complete_model_class()`:
complete_model_class(
cls,
config_wrapper,
ns_resolver,
raise_errors=False,
create_model_module=_create_model_module,
)
if config_wrapper.frozen and '__hash__' not in namespace:
set_default_hash_func(cls, bases)
# using super(cls, cls) on the next line ensures we only call the parent class's __pydantic_init_subclass__
# I believe the `type: ignore` is only necessary because mypy doesn't realize that this code branch is
# only hit for _proper_ subclasses of BaseModel
super(cls, cls).__pydantic_init_subclass__(**kwargs) # type: ignore[misc]
return cls
else:
# These are instance variables, but have been assigned to `NoInitField` to trick the type checker.
for instance_slot in '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__':
namespace.pop(
instance_slot,
None, # In case the metaclass is used with a class other than `BaseModel`.
)
namespace.get('__annotations__', {}).clear()
return super().__new__(mcs, cls_name, bases, namespace, **kwargs)
if not TYPE_CHECKING: # pragma: no branch
# We put `__getattr__` in a non-TYPE_CHECKING block because otherwise, mypy allows arbitrary attribute access
def __getattr__(self, item: str) -> Any:
"""This is necessary to keep attribute access working for class attribute access."""
private_attributes = self.__dict__.get('__private_attributes__')
if private_attributes and item in private_attributes:
return private_attributes[item]
raise AttributeError(item)
@classmethod
def __prepare__(cls, *args: Any, **kwargs: Any) -> dict[str, object]:
return _ModelNamespaceDict()
# Due to performance and memory issues, in the ABCMeta.__subclasscheck__ implementation, we don't support
# registered virtual subclasses. See https://github.com/python/cpython/issues/92810#issuecomment-2762454345.
# This may change once CPython is fixed (possibly in 3.15), in which case we should conditionally
# define `register()`.
def register(self, subclass: type[_T]) -> type[_T]:
warnings.warn(
f"For performance reasons, virtual subclasses registered using '{self.__qualname__}.register()' "
"are not supported in 'isinstance()' and 'issubclass()' checks.",
stacklevel=2,
)
return super().register(subclass)
__instancecheck__ = type.__instancecheck__ # pyright: ignore[reportAssignmentType]
__subclasscheck__ = type.__subclasscheck__ # pyright: ignore[reportAssignmentType]
@staticmethod
def _collect_bases_data(bases: tuple[type[Any], ...]) -> tuple[set[str], set[str], dict[str, ModelPrivateAttr]]:
BaseModel = import_cached_base_model()
field_names: set[str] = set()
class_vars: set[str] = set()
private_attributes: dict[str, ModelPrivateAttr] = {}
for base in bases:
if issubclass(base, BaseModel) and base is not BaseModel:
# model_fields might not be defined yet in the case of generics, so we use getattr here:
field_names.update(getattr(base, '__pydantic_fields__', {}).keys())
class_vars.update(base.__class_vars__)
private_attributes.update(base.__private_attributes__)
return field_names, class_vars, private_attributes
@property
@deprecated(
'The `__fields__` attribute is deprecated, use the `model_fields` class property instead.', category=None
)
def __fields__(self) -> dict[str, FieldInfo]:
warnings.warn(
'The `__fields__` attribute is deprecated, use the `model_fields` class property instead.',
PydanticDeprecatedSince20,
stacklevel=2,
)
return getattr(self, '__pydantic_fields__', {})
@property
def __pydantic_fields_complete__(self) -> bool:
"""Whether the fields were successfully collected (i.e. type hints were successfully resolved).
This is a private attribute, not meant to be used outside Pydantic.
"""
if '__pydantic_fields__' not in self.__dict__:
return False
field_infos = cast('dict[str, FieldInfo]', self.__pydantic_fields__) # pyright: ignore[reportAttributeAccessIssue]
pydantic_extra_info = cast('PydanticExtraInfo | None', self.__pydantic_extra_info__) # pyright: ignore[reportAttributeAccessIssue]
if pydantic_extra_info is not None:
extra_complete = pydantic_extra_info.complete
else:
extra_complete = True
return all(field_info._complete for field_info in field_infos.values()) and extra_complete
def __dir__(self) -> list[str]:
attributes = list(super().__dir__())
if '__fields__' in attributes:
attributes.remove('__fields__')
return attributes
def init_private_attributes(self: BaseModel, context: Any, /) -> None:
"""This function is meant to behave like a BaseModel method to initialize private attributes.
It takes context as an argument since that's what pydantic-core passes when calling it.
Args:
self: The BaseModel instance.
context: The context.
"""
if getattr(self, '__pydantic_private__', None) is None:
pydantic_private = {}
for name, private_attr in self.__private_attributes__.items():
# Avoid needlessly creating a new dict for the validated data:
if private_attr.default_factory_takes_validated_data:
default = private_attr.get_default(
call_default_factory=True, validated_data={**self.__dict__, **pydantic_private}
)
else:
default = private_attr.get_default(call_default_factory=True)
if default is not PydanticUndefined:
pydantic_private[name] = default
object_setattr(self, '__pydantic_private__', pydantic_private)
def get_model_post_init(namespace: dict[str, Any], bases: tuple[type[Any], ...]) -> Callable[..., Any] | None:
"""Get the `model_post_init` method from the namespace or the class bases, or `None` if not defined."""
if 'model_post_init' in namespace:
return namespace['model_post_init']
BaseModel = import_cached_base_model()
model_post_init = get_attribute_from_bases(bases, 'model_post_init')
if model_post_init is not BaseModel.model_post_init:
return model_post_init
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],
) -> dict[str, ModelPrivateAttr]:
"""Iterate over the namespace and:
* gather private attributes
* check for items which look like fields but are not (e.g. have no annotation) and warn.
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.
Returns:
A dict containing private attributes info.
Raises:
TypeError: If there is a `__root__` field in model.
NameError: If private attribute name is invalid.
PydanticUserError:
- If a field does not have a type annotation.
- If a field on base class was overridden by a non-annotated attribute.
"""
from ..fields import ModelPrivateAttr, PrivateAttr
FieldInfo = import_cached_field_info()
all_ignored_types = ignored_types + default_ignored_types()
private_attributes: dict[str, ModelPrivateAttr] = {}
if '__root__' in raw_annotations or '__root__' in namespace:
raise TypeError("To define root models, use `pydantic.RootModel` rather than a field called '__root__'")
ignored_names: set[str] = set()
for var_name, value in list(namespace.items()):
if var_name == 'model_config' or var_name == '__pydantic_extra__':
continue
elif (
isinstance(value, type)
and value.__module__ == namespace['__module__']
and '__qualname__' in namespace
and value.__qualname__.startswith(f'{namespace["__qualname__"]}.')
):
# `value` is a nested type defined in this namespace; don't error
continue
elif isinstance(value, all_ignored_types) or value.__class__.__module__ == 'functools':
ignored_names.add(var_name)
continue
elif isinstance(value, ModelPrivateAttr):
if var_name.startswith('__'):
raise NameError(
'Private attributes must not use dunder names;'
f' use a single underscore prefix instead of {var_name!r}.'
)
elif is_valid_field_name(var_name):
raise NameError(
'Private attributes must not use valid field names;'
f' use sunder names, e.g. {"_" + var_name!r} instead of {var_name!r}.'
)
private_attributes[var_name] = value
del namespace[var_name]
elif isinstance(value, FieldInfo) and not is_valid_field_name(var_name):
suggested_name = var_name.lstrip('_') or 'my_field' # don't suggest '' for all-underscore name
raise NameError(
f'Fields must not use names with leading underscores;'
f' e.g., use {suggested_name!r} instead of {var_name!r}.'
)
elif var_name.startswith('__'):
continue
elif is_valid_privateattr_name(var_name):
if var_name not in raw_annotations or not is_classvar_annotation(raw_annotations[var_name]):
private_attributes[var_name] = cast(ModelPrivateAttr, PrivateAttr(default=value))
del namespace[var_name]
elif var_name in base_class_vars:
continue
elif var_name not in raw_annotations:
if var_name in base_class_fields:
raise PydanticUserError(
f'Field {var_name!r} defined on a base class was overridden by a non-annotated attribute. '
f'All field definitions, including overrides, require a type annotation.',
code='model-field-overridden',
)
elif isinstance(value, FieldInfo):
raise PydanticUserError(
f'Field {var_name!r} requires a type annotation', code='model-field-missing-annotation'
)
else:
raise PydanticUserError(
f'A non-annotated attribute was detected: `{var_name} = {value!r}`. All model fields require a '
f'type annotation; if `{var_name}` is not meant to be a field, you may be able to resolve this '
f"error by annotating it as a `ClassVar` or updating `model_config['ignored_types']`.",
code='model-field-missing-annotation',
)
for ann_name, ann_type in raw_annotations.items():
if (
is_valid_privateattr_name(ann_name)
and ann_name not in private_attributes
and ann_name not in ignored_names
# This condition can be a false negative when `ann_type` is stringified,
# but it is handled in most cases in `set_model_fields`:
and not is_classvar_annotation(ann_type)
and ann_type not in all_ignored_types
and getattr(ann_type, '__module__', None) != 'functools'
):
if isinstance(ann_type, str):
# Walking up the frames to get the module namespace where the model is defined
# (as the model class wasn't created yet, we unfortunately can't use `cls.__module__`):
frame = sys._getframe(2)
if frame is not None:
try:
ann_type = eval_type_backport(
_make_forward_ref(ann_type, is_argument=False, is_class=True),
globalns=frame.f_globals,
localns=frame.f_locals,
)
except (NameError, TypeError):
pass
if typing_objects.is_annotated(get_origin(ann_type)):
_, *metadata = get_args(ann_type)
private_attr = next((v for v in metadata if isinstance(v, ModelPrivateAttr)), None)
if private_attr is not None:
private_attributes[ann_name] = private_attr
continue
private_attributes[ann_name] = PrivateAttr()
return private_attributes
def set_default_hash_func(cls: type[BaseModel], bases: tuple[type[Any], ...]) -> None:
base_hash_func = get_attribute_from_bases(bases, '__hash__')
new_hash_func = make_hash_func(cls)
if base_hash_func in {None, object.__hash__} or getattr(base_hash_func, '__code__', None) == new_hash_func.__code__:
# If `__hash__` is some default, we generate a hash function.
# It will be `None` if not overridden from BaseModel.
# It may be `object.__hash__` if there is another
# parent class earlier in the bases which doesn't override `__hash__` (e.g. `typing.Generic`).
# It may be a value set by `set_default_hash_func` if `cls` is a subclass of another frozen model.
# In the last case we still need a new hash function to account for new `model_fields`.
cls.__hash__ = new_hash_func
def make_hash_func(cls: type[BaseModel]) -> Any:
getter = operator.itemgetter(*cls.__pydantic_fields__.keys()) if cls.__pydantic_fields__ else lambda _: 0
def hash_func(self: Any) -> int:
try:
return hash(getter(self.__dict__))
except KeyError:
# In rare cases (such as when using the deprecated copy method), the __dict__ may not contain
# all model fields, which is how we can get here.
# getter(self.__dict__) is much faster than any 'safe' method that accounts for missing keys,
# and wrapping it in a `try` doesn't slow things down much in the common case.
return hash(getter(SafeGetItemProxy(self.__dict__)))
return hash_func
def set_model_fields(
cls: type[BaseModel],
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
) -> None:
"""Collect and set `cls.__pydantic_fields__` and `cls.__class_vars__`.
Args:
cls: BaseModel or dataclass.
config_wrapper: The config wrapper instance.
ns_resolver: Namespace resolver to use when getting model annotations.
"""
typevars_map = get_model_typevars_map(cls)
fields, pydantic_extra_info, class_vars = collect_model_fields(
cls, config_wrapper, ns_resolver, typevars_map=typevars_map
)
cls.__pydantic_fields__ = fields
cls.__pydantic_extra_info__ = pydantic_extra_info
cls.__class_vars__.update(class_vars)
for k in class_vars:
# Class vars should not be private attributes
# We remove them _here_ and not earlier because we rely on inspecting the class to determine its classvars,
# but private attributes are determined by inspecting the namespace _prior_ to class creation.
# In the case that a classvar with a leading-'_' is defined via a ForwardRef (e.g., when using
# `__future__.annotations`), we want to remove the private attribute which was detected _before_ we knew it
# evaluated to a classvar
value = cls.__private_attributes__.pop(k, None)
if value is not None and value.default is not PydanticUndefined:
setattr(cls, k, value.default)
def complete_model_class(
cls: type[BaseModel],
config_wrapper: ConfigWrapper,
ns_resolver: NsResolver,
*,
raise_errors: bool = True,
call_on_complete_hook: bool = True,
create_model_module: str | None = None,
is_force_rebuild: bool = False,
) -> bool:
"""Finish building a model class.
This logic must be called after class has been created since validation functions must be bound
and `get_type_hints` requires a class object.
Args:
cls: BaseModel or dataclass.
config_wrapper: The config wrapper instance.
ns_resolver: The namespace resolver instance to use during schema building.
raise_errors: Whether to raise errors.
call_on_complete_hook: Whether to call the `__pydantic_on_complete__` hook.
create_model_module: The module of the class to be created, if created by `create_model`.
is_force_rebuild: Whether the model is being force-rebuilt (if True, pre-built serializers and
validators are not used, to avoid stale references).
Returns:
`True` if the model is successfully completed, else `False`.
Raises:
PydanticUndefinedAnnotation: If PydanticUndefinedAnnotation occurs in __get_pydantic_core_schema__
and `raise_errors=True`.
"""
typevars_map = get_model_typevars_map(cls)
if not cls.__pydantic_fields_complete__:
# Note: when coming from `ModelMetaclass.__new__()`, this results in fields being built twice.
# We do so a second time here so that we can get the ``NameError`` for the specific undefined annotation.
# Alternatively, we could let `GenerateSchema()` raise the error, but there are cases where incomplete
# fields are inherited in `collect_model_fields()` and can actually have their annotation resolved in the
# generate schema process. As we want to avoid having `__pydantic_fields_complete__` set to `False`
# when `__pydantic_complete__` is `True`, we rebuild here:
try:
cls.__pydantic_fields__, cls.__pydantic_extra_info__ = rebuild_model_fields(
cls,
config_wrapper=config_wrapper,
ns_resolver=ns_resolver,
typevars_map=typevars_map,
)
except NameError as e:
exc = PydanticUndefinedAnnotation.from_name_error(e)
set_model_mocks(cls, f'`{exc.name}`')
if raise_errors:
raise exc from e
if not raise_errors and not cls.__pydantic_fields_complete__:
# No need to continue with schema gen, it is guaranteed to fail
return False
assert cls.__pydantic_fields_complete__
gen_schema = GenerateSchema(
config_wrapper,
ns_resolver,
typevars_map,
)
try:
schema = gen_schema.generate_schema(cls)
except PydanticUndefinedAnnotation as e:
if raise_errors:
raise
set_model_mocks(cls, f'`{e.name}`')
return False
core_config = config_wrapper.core_config(title=cls.__name__)
try:
schema = gen_schema.clean_schema(schema)
except InvalidSchemaError:
set_model_mocks(cls)
return False
# This needs to happen *after* model schema generation, as the return types
# of the properties are evaluated and the `ComputedFieldInfo` are recreated:
cls.__pydantic_computed_fields__ = {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()}
set_deprecated_descriptors(cls)
cls.__pydantic_core_schema__ = schema
cls.__pydantic_validator__ = create_schema_validator(
schema,
cls,
create_model_module or cls.__module__,
cls.__qualname__,
'create_model' if create_model_module else 'BaseModel',
core_config,
config_wrapper.plugin_settings,
_use_prebuilt=not is_force_rebuild,
)
cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config, _use_prebuilt=not is_force_rebuild)
# set __signature__ attr only for model class, but not for its instances
# (because instances can define `__call__`, and `inspect.signature` shouldn't
# use the `__signature__` attribute and instead generate from `__call__`).
cls.__signature__ = LazyClassAttribute(
'__signature__',
partial(
generate_pydantic_signature,
init=cls.__init__,
fields=cls.__pydantic_fields__,
validate_by_name=config_wrapper.validate_by_name,
extra=config_wrapper.extra,
),
)
cls.__pydantic_complete__ = True
if call_on_complete_hook:
cls.__pydantic_on_complete__()
return True
def set_deprecated_descriptors(cls: type[BaseModel]) -> None:
"""Set data descriptors on the class for deprecated fields."""
for field, field_info in cls.__pydantic_fields__.items():
if (msg := field_info.deprecation_message) is not None:
desc = _DeprecatedFieldDescriptor(msg)
desc.__set_name__(cls, field)
setattr(cls, field, desc)
for field, computed_field_info in cls.__pydantic_computed_fields__.items():
if (
(msg := computed_field_info.deprecation_message) is not None
# Avoid having two warnings emitted:
and not hasattr(unwrap_wrapped_function(computed_field_info.wrapped_property), '__deprecated__')
):
desc = _DeprecatedFieldDescriptor(msg, computed_field_info.wrapped_property)
desc.__set_name__(cls, field)
setattr(cls, field, desc)
class _DeprecatedFieldDescriptor:
"""Read-only data descriptor used to emit a runtime deprecation warning before accessing a deprecated field.
Attributes:
msg: The deprecation message to be emitted.
wrapped_property: The property instance if the deprecated field is a computed field, or `None`.
field_name: The name of the field being deprecated.
"""
field_name: str
def __init__(self, msg: str, wrapped_property: property | None = None) -> None:
self.msg = msg
self.wrapped_property = wrapped_property
def __set_name__(self, cls: type[BaseModel], name: str) -> None:
self.field_name = name
def __get__(self, obj: BaseModel | None, obj_type: type[BaseModel] | None = None) -> Any:
if obj is None:
if self.wrapped_property is not None:
return self.wrapped_property.__get__(None, obj_type)
raise AttributeError(self.field_name)
warnings.warn(self.msg, DeprecationWarning, stacklevel=2)
if self.wrapped_property is not None:
return self.wrapped_property.__get__(obj, obj_type)
return obj.__dict__[self.field_name]
# Defined to make it a data descriptor and take precedence over the instance's dictionary.
# Note that it will not be called when setting a value on a model instance
# as `BaseModel.__setattr__` is defined and takes priority.
def __set__(self, obj: Any, value: Any) -> NoReturn:
raise AttributeError(self.field_name)
class _PydanticWeakRef:
"""Wrapper for `weakref.ref` that enables `pickle` serialization.
Cloudpickle fails to serialize weakref.ref objects due to an arcane error related to
to abstract base classes (`abc.ABC`). This class works around the issue by wrapping
`weakref.ref` instead of subclassing it.
See https://github.com/pydantic/pydantic/issues/6763 for context.
Semantics:
- If not pickled, behaves the same as a `weakref.ref`.
- If pickled along with the referenced object, the same `weakref.ref` behavior
will be maintained between them after unpickling.
- If pickled without the referenced object, after unpickling the underlying
reference will be cleared (`__call__` will always return `None`).
"""
def __init__(self, obj: Any):
if obj is None:
# The object will be `None` upon deserialization if the serialized weakref
# had lost its underlying object.
self._wr = None
else:
self._wr = weakref.ref(obj)
def __call__(self) -> Any:
if self._wr is None:
return None
else:
return self._wr()
def __reduce__(self) -> tuple[Callable, tuple[weakref.ReferenceType | None]]:
return _PydanticWeakRef, (self(),)
def build_lenient_weakvaluedict(d: dict[str, Any] | None) -> dict[str, Any] | None:
"""Takes an input dictionary, and produces a new value that (invertibly) replaces the values with weakrefs.
We can't just use a WeakValueDictionary because many types (including int, str, etc.) can't be stored as values
in a WeakValueDictionary.
The `unpack_lenient_weakvaluedict` function can be used to reverse this operation.
"""
if d is None:
return None
result = {}
for k, v in d.items():
try:
proxy = _PydanticWeakRef(v)
except TypeError:
proxy = v
result[k] = proxy
return result
def unpack_lenient_weakvaluedict(d: dict[str, Any] | None) -> dict[str, Any] | None:
"""Inverts the transform performed by `build_lenient_weakvaluedict`."""
if d is None:
return None
result = {}
for k, v in d.items():
if isinstance(v, _PydanticWeakRef):
v = v()
if v is not None:
result[k] = v
else:
result[k] = v
return result
@cache
def default_ignored_types() -> tuple[type[Any], ...]:
from ..fields import ComputedFieldInfo
ignored_types = [
FunctionType,
property,
classmethod,
staticmethod,
PydanticDescriptorProxy,
ComputedFieldInfo,
TypeAliasType, # from `typing_extensions`
]
if sys.version_info >= (3, 12):
ignored_types.append(typing.TypeAliasType)
return tuple(ignored_types)

View File

@@ -0,0 +1,293 @@
from __future__ import annotations
import sys
from collections.abc import Generator, Iterator, Mapping
from contextlib import contextmanager
from functools import cached_property
from typing import Any, Callable, NamedTuple, TypeVar
from typing_extensions import ParamSpec, TypeAlias, TypeAliasType, TypeVarTuple
GlobalsNamespace: TypeAlias = 'dict[str, Any]'
"""A global namespace.
In most cases, this is a reference to the `__dict__` attribute of a module.
This namespace type is expected as the `globals` argument during annotations evaluation.
"""
MappingNamespace: TypeAlias = Mapping[str, Any]
"""Any kind of namespace.
In most cases, this is a local namespace (e.g. the `__dict__` attribute of a class,
the [`f_locals`][frame.f_locals] attribute of a frame object, when dealing with types
defined inside functions).
This namespace type is expected as the `locals` argument during annotations evaluation.
"""
_TypeVarLike: TypeAlias = 'TypeVar | ParamSpec | TypeVarTuple'
class NamespacesTuple(NamedTuple):
"""A tuple of globals and locals to be used during annotations evaluation.
This datastructure is defined as a named tuple so that it can easily be unpacked:
```python {lint="skip" test="skip"}
def eval_type(typ: type[Any], ns: NamespacesTuple) -> None:
return eval(typ, *ns)
```
"""
globals: GlobalsNamespace
"""The namespace to be used as the `globals` argument during annotations evaluation."""
locals: MappingNamespace
"""The namespace to be used as the `locals` argument during annotations evaluation."""
def get_module_ns_of(obj: Any) -> dict[str, Any]:
"""Get the namespace of the module where the object is defined.
Caution: this function does not return a copy of the module namespace, so the result
should not be mutated. The burden of enforcing this is on the caller.
"""
module_name = getattr(obj, '__module__', None)
if module_name:
try:
return sys.modules[module_name].__dict__
except KeyError:
# happens occasionally, see https://github.com/pydantic/pydantic/issues/2363
return {}
return {}
# Note that this class is almost identical to `collections.ChainMap`, but need to enforce
# immutable mappings here:
class LazyLocalNamespace(Mapping[str, Any]):
"""A lazily evaluated mapping, to be used as the `locals` argument during annotations evaluation.
While the [`eval`][eval] function expects a mapping as the `locals` argument, it only
performs `__getitem__` calls. The [`Mapping`][collections.abc.Mapping] abstract base class
is fully implemented only for type checking purposes.
Args:
*namespaces: The namespaces to consider, in ascending order of priority.
Example:
```python {lint="skip" test="skip"}
ns = LazyLocalNamespace({'a': 1, 'b': 2}, {'a': 3})
ns['a']
#> 3
ns['b']
#> 2
```
"""
def __init__(self, *namespaces: MappingNamespace) -> None:
self._namespaces = namespaces
@cached_property
def data(self) -> dict[str, Any]:
return {k: v for ns in self._namespaces for k, v in ns.items()}
def __len__(self) -> int:
return len(self.data)
def __getitem__(self, key: str) -> Any:
return self.data[key]
def __contains__(self, key: object) -> bool:
return key in self.data
def __iter__(self) -> Iterator[str]:
return iter(self.data)
def ns_for_function(obj: Callable[..., Any], parent_namespace: MappingNamespace | None = None) -> NamespacesTuple:
"""Return the global and local namespaces to be used when evaluating annotations for the provided function.
The global namespace will be the `__dict__` attribute of the module the function was defined in.
The local namespace will contain the `__type_params__` introduced by PEP 695.
Args:
obj: The object to use when building namespaces.
parent_namespace: Optional namespace to be added with the lowest priority in the local namespace.
If the passed function is a method, the `parent_namespace` will be the namespace of the class
the method is defined in. Thus, we also fetch type `__type_params__` from there (i.e. the
class-scoped type variables).
"""
locals_list: list[MappingNamespace] = []
if parent_namespace is not None:
locals_list.append(parent_namespace)
# Get the `__type_params__` attribute introduced by PEP 695.
# Note that the `typing._eval_type` function expects type params to be
# passed as a separate argument. However, internally, `_eval_type` calls
# `ForwardRef._evaluate` which will merge type params with the localns,
# essentially mimicking what we do here.
type_params: tuple[_TypeVarLike, ...] = getattr(obj, '__type_params__', ())
if parent_namespace is not None:
# We also fetch type params from the parent namespace. If present, it probably
# means the function was defined in a class. This is to support the following:
# https://github.com/python/cpython/issues/124089.
type_params += parent_namespace.get('__type_params__', ())
locals_list.append({t.__name__: t for t in type_params})
# What about short-circuiting to `obj.__globals__`?
globalns = get_module_ns_of(obj)
return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list))
class NsResolver:
"""A class responsible for the namespaces resolving logic for annotations evaluation.
This class handles the namespace logic when evaluating annotations mainly for class objects.
It holds a stack of classes that are being inspected during the core schema building,
and the `types_namespace` property exposes the globals and locals to be used for
type annotation evaluation. Additionally -- if no class is present in the stack -- a
fallback globals and locals can be provided using the `namespaces_tuple` argument
(this is useful when generating a schema for a simple annotation, e.g. when using
`TypeAdapter`).
The namespace creation logic is unfortunately flawed in some cases, for backwards
compatibility reasons and to better support valid edge cases. See the description
for the `parent_namespace` argument and the example for more details.
Args:
namespaces_tuple: The default globals and locals to use if no class is present
on the stack. This can be useful when using the `GenerateSchema` class
with `TypeAdapter`, where the "type" being analyzed is a simple annotation.
parent_namespace: An optional parent namespace that will be added to the locals
with the lowest priority. For a given class defined in a function, the locals
of this function are usually used as the parent namespace:
```python {lint="skip" test="skip"}
from pydantic import BaseModel
def func() -> None:
SomeType = int
class Model(BaseModel):
f: 'SomeType'
# when collecting fields, an namespace resolver instance will be created
# this way:
# ns_resolver = NsResolver(parent_namespace={'SomeType': SomeType})
```
For backwards compatibility reasons and to support valid edge cases, this parent
namespace will be used for *every* type being pushed to the stack. In the future,
we might want to be smarter by only doing so when the type being pushed is defined
in the same module as the parent namespace.
Example:
```python {lint="skip" test="skip"}
ns_resolver = NsResolver(
parent_namespace={'fallback': 1},
)
class Sub:
m: 'Model'
class Model:
some_local = 1
sub: Sub
ns_resolver = NsResolver()
# This is roughly what happens when we build a core schema for `Model`:
with ns_resolver.push(Model):
ns_resolver.types_namespace
#> NamespacesTuple({'Sub': Sub}, {'Model': Model, 'some_local': 1})
# First thing to notice here, the model being pushed is added to the locals.
# Because `NsResolver` is being used during the model definition, it is not
# yet added to the globals. This is useful when resolving self-referencing annotations.
with ns_resolver.push(Sub):
ns_resolver.types_namespace
#> NamespacesTuple({'Sub': Sub}, {'Sub': Sub, 'Model': Model})
# Second thing to notice: `Sub` is present in both the globals and locals.
# This is not an issue, just that as described above, the model being pushed
# is added to the locals, but it happens to be present in the globals as well
# because it is already defined.
# Third thing to notice: `Model` is also added in locals. This is a backwards
# compatibility workaround that allows for `Sub` to be able to resolve `'Model'`
# correctly (as otherwise models would have to be rebuilt even though this
# doesn't look necessary).
```
"""
def __init__(
self,
namespaces_tuple: NamespacesTuple | None = None,
parent_namespace: MappingNamespace | None = None,
) -> None:
self._base_ns_tuple = namespaces_tuple or NamespacesTuple({}, {})
self._parent_ns = parent_namespace
self._types_stack: list[type[Any] | TypeAliasType] = []
@cached_property
def types_namespace(self) -> NamespacesTuple:
"""The current global and local namespaces to be used for annotations evaluation."""
if not self._types_stack:
# TODO: should we merge the parent namespace here?
# This is relevant for TypeAdapter, where there are no types on the stack, and we might
# need access to the parent_ns. Right now, we sidestep this in `type_adapter.py` by passing
# locals to both parent_ns and the base_ns_tuple, but this is a bit hacky.
# we might consider something like:
# if self._parent_ns is not None:
# # Hacky workarounds, see class docstring:
# # An optional parent namespace that will be added to the locals with the lowest priority
# locals_list: list[MappingNamespace] = [self._parent_ns, self._base_ns_tuple.locals]
# return NamespacesTuple(self._base_ns_tuple.globals, LazyLocalNamespace(*locals_list))
return self._base_ns_tuple
typ = self._types_stack[-1]
globalns = get_module_ns_of(typ)
locals_list: list[MappingNamespace] = []
# Hacky workarounds, see class docstring:
# An optional parent namespace that will be added to the locals with the lowest priority
if self._parent_ns is not None:
locals_list.append(self._parent_ns)
if len(self._types_stack) > 1:
first_type = self._types_stack[0]
locals_list.append({first_type.__name__: first_type})
# Adding `__type_params__` *before* `vars(typ)`, as the latter takes priority
# (see https://github.com/python/cpython/pull/120272).
# TODO `typ.__type_params__` when we drop support for Python 3.11:
type_params: tuple[_TypeVarLike, ...] = getattr(typ, '__type_params__', ())
if type_params:
# Adding `__type_params__` is mostly useful for generic classes defined using
# PEP 695 syntax *and* using forward annotations (see the example in
# https://github.com/python/cpython/issues/114053). For TypeAliasType instances,
# it is way less common, but still required if using a string annotation in the alias
# value, e.g. `type A[T] = 'T'` (which is not necessary in most cases).
locals_list.append({t.__name__: t for t in type_params})
# TypeAliasType instances don't have a `__dict__` attribute, so the check
# is necessary:
if hasattr(typ, '__dict__'):
locals_list.append(vars(typ))
# The `len(self._types_stack) > 1` check above prevents this from being added twice:
locals_list.append({typ.__name__: typ})
return NamespacesTuple(globalns, LazyLocalNamespace(*locals_list))
@contextmanager
def push(self, typ: type[Any] | TypeAliasType, /) -> Generator[None]:
"""Push a type to the stack."""
self._types_stack.append(typ)
# Reset the cached property:
self.__dict__.pop('types_namespace', None)
try:
yield
finally:
self._types_stack.pop()
self.__dict__.pop('types_namespace', None)