Update 2025-04-13_16:26:04
This commit is contained in:
459
venv/lib/python3.11/site-packages/pydantic/_internal/_fields.py
Normal file
459
venv/lib/python3.11/site-packages/pydantic/_internal/_fields.py
Normal file
@ -0,0 +1,459 @@
|
||||
"""Private logic related to fields (the `Field()` function and `FieldInfo` class), and arguments to `Annotated`."""
|
||||
|
||||
from __future__ import annotations as _annotations
|
||||
|
||||
import dataclasses
|
||||
import warnings
|
||||
from collections.abc import Mapping
|
||||
from copy import copy
|
||||
from functools import cache
|
||||
from inspect import Parameter, ismethoddescriptor, signature
|
||||
from re import Pattern
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
||||
|
||||
from pydantic_core import PydanticUndefined
|
||||
from typing_extensions import TypeIs, get_origin
|
||||
from typing_inspection import typing_objects
|
||||
from typing_inspection.introspection import AnnotationSource
|
||||
|
||||
from pydantic import PydanticDeprecatedSince211
|
||||
from pydantic.errors import PydanticUserError
|
||||
|
||||
from . import _generics, _typing_extra
|
||||
from ._config import ConfigWrapper
|
||||
from ._docs_extraction import extract_docstrings_from_cls
|
||||
from ._import_utils import import_cached_base_model, import_cached_field_info
|
||||
from ._namespace_utils import NsResolver
|
||||
from ._repr import Representation
|
||||
from ._utils import can_be_positional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from annotated_types import BaseMetadata
|
||||
|
||||
from ..fields import FieldInfo
|
||||
from ..main import BaseModel
|
||||
from ._dataclasses import StandardDataclass
|
||||
from ._decorators import DecoratorInfos
|
||||
|
||||
|
||||
class PydanticMetadata(Representation):
|
||||
"""Base class for annotation markers like `Strict`."""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
def pydantic_general_metadata(**metadata: Any) -> BaseMetadata:
|
||||
"""Create a new `_PydanticGeneralMetadata` class with the given metadata.
|
||||
|
||||
Args:
|
||||
**metadata: The metadata to add.
|
||||
|
||||
Returns:
|
||||
The new `_PydanticGeneralMetadata` class.
|
||||
"""
|
||||
return _general_metadata_cls()(metadata) # type: ignore
|
||||
|
||||
|
||||
@cache
|
||||
def _general_metadata_cls() -> type[BaseMetadata]:
|
||||
"""Do it this way to avoid importing `annotated_types` at import time."""
|
||||
from annotated_types import BaseMetadata
|
||||
|
||||
class _PydanticGeneralMetadata(PydanticMetadata, BaseMetadata):
|
||||
"""Pydantic general metadata like `max_digits`."""
|
||||
|
||||
def __init__(self, metadata: Any):
|
||||
self.__dict__ = metadata
|
||||
|
||||
return _PydanticGeneralMetadata # type: ignore
|
||||
|
||||
|
||||
def _update_fields_from_docstrings(cls: type[Any], fields: dict[str, FieldInfo], use_inspect: bool = False) -> None:
|
||||
fields_docs = extract_docstrings_from_cls(cls, use_inspect=use_inspect)
|
||||
for ann_name, field_info in fields.items():
|
||||
if field_info.description is None and ann_name in fields_docs:
|
||||
field_info.description = fields_docs[ann_name]
|
||||
|
||||
|
||||
def collect_model_fields( # noqa: C901
|
||||
cls: type[BaseModel],
|
||||
config_wrapper: ConfigWrapper,
|
||||
ns_resolver: NsResolver | None,
|
||||
*,
|
||||
typevars_map: Mapping[TypeVar, Any] | None = None,
|
||||
) -> tuple[dict[str, FieldInfo], set[str]]:
|
||||
"""Collect the fields and class variables names of a nascent Pydantic model.
|
||||
|
||||
The fields collection process is *lenient*, meaning it won't error if string annotations
|
||||
fail to evaluate. If this happens, the original annotation (and assigned value, if any)
|
||||
is stored on the created `FieldInfo` instance.
|
||||
|
||||
The `rebuild_model_fields()` should be called at a later point (e.g. when rebuilding the model),
|
||||
and will make use of these stored attributes.
|
||||
|
||||
Args:
|
||||
cls: BaseModel or dataclass.
|
||||
config_wrapper: The config wrapper instance.
|
||||
ns_resolver: Namespace resolver to use when getting model annotations.
|
||||
typevars_map: A dictionary mapping type variables to their concrete types.
|
||||
|
||||
Returns:
|
||||
A two-tuple containing model fields and class variables names.
|
||||
|
||||
Raises:
|
||||
NameError:
|
||||
- If there is a conflict between a field name and protected namespaces.
|
||||
- If there is a field other than `root` in `RootModel`.
|
||||
- If a field shadows an attribute in the parent model.
|
||||
"""
|
||||
BaseModel = import_cached_base_model()
|
||||
FieldInfo_ = import_cached_field_info()
|
||||
|
||||
bases = cls.__bases__
|
||||
parent_fields_lookup: dict[str, FieldInfo] = {}
|
||||
for base in reversed(bases):
|
||||
if model_fields := getattr(base, '__pydantic_fields__', None):
|
||||
parent_fields_lookup.update(model_fields)
|
||||
|
||||
type_hints = _typing_extra.get_model_type_hints(cls, ns_resolver=ns_resolver)
|
||||
|
||||
# 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__', {})
|
||||
fields: dict[str, FieldInfo] = {}
|
||||
|
||||
class_vars: set[str] = set()
|
||||
for ann_name, (ann_type, evaluated) in type_hints.items():
|
||||
if ann_name == 'model_config':
|
||||
# We never want to treat `model_config` as a field
|
||||
# Note: we may need to change this logic if/when we introduce a `BareModel` class with no
|
||||
# protected namespaces (where `model_config` might be allowed as a field name)
|
||||
continue
|
||||
|
||||
for protected_namespace in config_wrapper.protected_namespaces:
|
||||
ns_violation: bool = False
|
||||
if isinstance(protected_namespace, Pattern):
|
||||
ns_violation = protected_namespace.match(ann_name) is not None
|
||||
elif isinstance(protected_namespace, str):
|
||||
ns_violation = ann_name.startswith(protected_namespace)
|
||||
|
||||
if ns_violation:
|
||||
for b in bases:
|
||||
if hasattr(b, ann_name):
|
||||
if not (issubclass(b, BaseModel) and ann_name in getattr(b, '__pydantic_fields__', {})):
|
||||
raise NameError(
|
||||
f'Field "{ann_name}" conflicts with member {getattr(b, ann_name)}'
|
||||
f' of protected namespace "{protected_namespace}".'
|
||||
)
|
||||
else:
|
||||
valid_namespaces = ()
|
||||
for pn in config_wrapper.protected_namespaces:
|
||||
if isinstance(pn, Pattern):
|
||||
if not pn.match(ann_name):
|
||||
valid_namespaces += (f're.compile({pn.pattern})',)
|
||||
else:
|
||||
if not ann_name.startswith(pn):
|
||||
valid_namespaces += (pn,)
|
||||
|
||||
warnings.warn(
|
||||
f'Field "{ann_name}" in {cls.__name__} has conflict with protected namespace "{protected_namespace}".'
|
||||
'\n\nYou may be able to resolve this warning by setting'
|
||||
f" `model_config['protected_namespaces'] = {valid_namespaces}`.",
|
||||
UserWarning,
|
||||
)
|
||||
if _typing_extra.is_classvar_annotation(ann_type):
|
||||
class_vars.add(ann_name)
|
||||
continue
|
||||
|
||||
assigned_value = getattr(cls, ann_name, PydanticUndefined)
|
||||
|
||||
if not is_valid_field_name(ann_name):
|
||||
continue
|
||||
if cls.__pydantic_root_model__ and ann_name != 'root':
|
||||
raise NameError(
|
||||
f"Unexpected field with name {ann_name!r}; only 'root' is allowed as a field of a `RootModel`"
|
||||
)
|
||||
|
||||
# when building a generic model with `MyModel[int]`, the generic_origin check makes sure we don't get
|
||||
# "... shadows an attribute" warnings
|
||||
generic_origin = getattr(cls, '__pydantic_generic_metadata__', {}).get('origin')
|
||||
for base in bases:
|
||||
dataclass_fields = {
|
||||
field.name for field in (dataclasses.fields(base) if dataclasses.is_dataclass(base) else ())
|
||||
}
|
||||
if hasattr(base, ann_name):
|
||||
if base is generic_origin:
|
||||
# Don't warn when "shadowing" of attributes in parametrized generics
|
||||
continue
|
||||
|
||||
if ann_name in dataclass_fields:
|
||||
# Don't warn when inheriting stdlib dataclasses whose fields are "shadowed" by defaults being set
|
||||
# on the class instance.
|
||||
continue
|
||||
|
||||
if ann_name not in annotations:
|
||||
# Don't warn when a field exists in a parent class but has not been defined in the current class
|
||||
continue
|
||||
|
||||
warnings.warn(
|
||||
f'Field name "{ann_name}" in "{cls.__qualname__}" shadows an attribute in parent '
|
||||
f'"{base.__qualname__}"',
|
||||
UserWarning,
|
||||
)
|
||||
|
||||
if assigned_value is PydanticUndefined: # no assignment, just a plain annotation
|
||||
if ann_name in annotations:
|
||||
# field is present in the current model's annotations (and *not* from parent classes)
|
||||
field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS)
|
||||
elif ann_name in parent_fields_lookup:
|
||||
# The field was present on one of the (possibly multiple) base classes
|
||||
# copy the field to make sure typevar substitutions don't cause issues with the base classes
|
||||
field_info = copy(parent_fields_lookup[ann_name])
|
||||
else:
|
||||
# The field was not found on any base classes; this seems to be caused by fields not getting
|
||||
# generated thanks to models not being fully defined while initializing recursive models.
|
||||
# Nothing stops us from just creating a new FieldInfo for this type hint, so we do this.
|
||||
field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS)
|
||||
|
||||
if not evaluated:
|
||||
field_info._complete = False
|
||||
# Store the original annotation that should be used to rebuild
|
||||
# the field info later:
|
||||
field_info._original_annotation = ann_type
|
||||
else: # An assigned value is present (either the default value, or a `Field()` function)
|
||||
_warn_on_nested_alias_in_annotation(ann_type, ann_name)
|
||||
if isinstance(assigned_value, FieldInfo_) and ismethoddescriptor(assigned_value.default):
|
||||
# `assigned_value` was fetched using `getattr`, which triggers a call to `__get__`
|
||||
# for descriptors, so we do the same if the `= field(default=...)` form is used.
|
||||
# Note that we only do this for method descriptors for now, we might want to
|
||||
# extend this to any descriptor in the future (by simply checking for
|
||||
# `hasattr(assigned_value.default, '__get__')`).
|
||||
assigned_value.default = assigned_value.default.__get__(None, cls)
|
||||
|
||||
# The `from_annotated_attribute()` call below mutates the assigned `Field()`, so make a copy:
|
||||
original_assignment = (
|
||||
copy(assigned_value) if not evaluated and isinstance(assigned_value, FieldInfo_) else assigned_value
|
||||
)
|
||||
|
||||
field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value, _source=AnnotationSource.CLASS)
|
||||
if not evaluated:
|
||||
field_info._complete = False
|
||||
# Store the original annotation and assignment value that should be used to rebuild
|
||||
# the field info later:
|
||||
field_info._original_annotation = ann_type
|
||||
field_info._original_assignment = original_assignment
|
||||
elif 'final' in field_info._qualifiers and not field_info.is_required():
|
||||
warnings.warn(
|
||||
f'Annotation {ann_name!r} is marked as final and has a default value. Pydantic treats {ann_name!r} as a '
|
||||
'class variable, but it will be considered as a normal field in V3 to be aligned with dataclasses. If you '
|
||||
f'still want {ann_name!r} to be considered as a class variable, annotate it as: `ClassVar[<type>] = <default>.`',
|
||||
category=PydanticDeprecatedSince211,
|
||||
# Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case:
|
||||
stacklevel=4,
|
||||
)
|
||||
class_vars.add(ann_name)
|
||||
continue
|
||||
|
||||
# attributes which are fields are removed from the class namespace:
|
||||
# 1. To match the behaviour of annotation-only fields
|
||||
# 2. To avoid false positives in the NameError check above
|
||||
try:
|
||||
delattr(cls, ann_name)
|
||||
except AttributeError:
|
||||
pass # indicates the attribute was on a parent class
|
||||
|
||||
# Use cls.__dict__['__pydantic_decorators__'] instead of cls.__pydantic_decorators__
|
||||
# to make sure the decorators have already been built for this exact class
|
||||
decorators: DecoratorInfos = cls.__dict__['__pydantic_decorators__']
|
||||
if ann_name in decorators.computed_fields:
|
||||
raise TypeError(
|
||||
f'Field {ann_name!r} of class {cls.__name__!r} overrides symbol of same name in a parent class. '
|
||||
'This override with a computed_field is incompatible.'
|
||||
)
|
||||
fields[ann_name] = field_info
|
||||
|
||||
if typevars_map:
|
||||
for field in fields.values():
|
||||
if field._complete:
|
||||
field.apply_typevars_map(typevars_map)
|
||||
|
||||
if config_wrapper.use_attribute_docstrings:
|
||||
_update_fields_from_docstrings(cls, fields)
|
||||
return fields, class_vars
|
||||
|
||||
|
||||
def _warn_on_nested_alias_in_annotation(ann_type: type[Any], ann_name: str) -> None:
|
||||
FieldInfo = import_cached_field_info()
|
||||
|
||||
args = getattr(ann_type, '__args__', None)
|
||||
if args:
|
||||
for anno_arg in args:
|
||||
if typing_objects.is_annotated(get_origin(anno_arg)):
|
||||
for anno_type_arg in _typing_extra.get_args(anno_arg):
|
||||
if isinstance(anno_type_arg, FieldInfo) and anno_type_arg.alias is not None:
|
||||
warnings.warn(
|
||||
f'`alias` specification on field "{ann_name}" must be set on outermost annotation to take effect.',
|
||||
UserWarning,
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
def rebuild_model_fields(
|
||||
cls: type[BaseModel],
|
||||
*,
|
||||
ns_resolver: NsResolver,
|
||||
typevars_map: Mapping[TypeVar, Any],
|
||||
) -> dict[str, FieldInfo]:
|
||||
"""Rebuild the (already present) model fields by trying to reevaluate annotations.
|
||||
|
||||
This function should be called whenever a model with incomplete fields is encountered.
|
||||
|
||||
Note:
|
||||
This function *doesn't* mutate the model fields in place, as it can be called during
|
||||
schema generation, where you don't want to mutate other model's fields.
|
||||
"""
|
||||
FieldInfo_ = import_cached_field_info()
|
||||
|
||||
rebuilt_fields: dict[str, FieldInfo] = {}
|
||||
with ns_resolver.push(cls):
|
||||
for f_name, field_info in cls.__pydantic_fields__.items():
|
||||
if field_info._complete:
|
||||
rebuilt_fields[f_name] = field_info
|
||||
else:
|
||||
ann = _typing_extra.eval_type(
|
||||
field_info._original_annotation,
|
||||
*ns_resolver.types_namespace,
|
||||
)
|
||||
ann = _generics.replace_types(ann, typevars_map)
|
||||
|
||||
if (assign := field_info._original_assignment) is PydanticUndefined:
|
||||
rebuilt_fields[f_name] = FieldInfo_.from_annotation(ann, _source=AnnotationSource.CLASS)
|
||||
else:
|
||||
rebuilt_fields[f_name] = FieldInfo_.from_annotated_attribute(
|
||||
ann, assign, _source=AnnotationSource.CLASS
|
||||
)
|
||||
|
||||
return rebuilt_fields
|
||||
|
||||
|
||||
def collect_dataclass_fields(
|
||||
cls: type[StandardDataclass],
|
||||
*,
|
||||
ns_resolver: NsResolver | None = None,
|
||||
typevars_map: dict[Any, Any] | None = None,
|
||||
config_wrapper: ConfigWrapper | None = None,
|
||||
) -> dict[str, FieldInfo]:
|
||||
"""Collect the fields of a dataclass.
|
||||
|
||||
Args:
|
||||
cls: dataclass.
|
||||
ns_resolver: Namespace resolver to use when getting dataclass annotations.
|
||||
Defaults to an empty instance.
|
||||
typevars_map: A dictionary mapping type variables to their concrete types.
|
||||
config_wrapper: The config wrapper instance.
|
||||
|
||||
Returns:
|
||||
The dataclass fields.
|
||||
"""
|
||||
FieldInfo_ = import_cached_field_info()
|
||||
|
||||
fields: dict[str, FieldInfo] = {}
|
||||
ns_resolver = ns_resolver or NsResolver()
|
||||
dataclass_fields = cls.__dataclass_fields__
|
||||
|
||||
# The logic here is similar to `_typing_extra.get_cls_type_hints`,
|
||||
# although we do it manually as stdlib dataclasses already have annotations
|
||||
# collected in each class:
|
||||
for base in reversed(cls.__mro__):
|
||||
if not dataclasses.is_dataclass(base):
|
||||
continue
|
||||
|
||||
with ns_resolver.push(base):
|
||||
for ann_name, dataclass_field in dataclass_fields.items():
|
||||
if ann_name not in base.__dict__.get('__annotations__', {}):
|
||||
# `__dataclass_fields__`contains every field, even the ones from base classes.
|
||||
# Only collect the ones defined on `base`.
|
||||
continue
|
||||
|
||||
globalns, localns = ns_resolver.types_namespace
|
||||
ann_type, _ = _typing_extra.try_eval_type(dataclass_field.type, globalns, localns)
|
||||
|
||||
if _typing_extra.is_classvar_annotation(ann_type):
|
||||
continue
|
||||
|
||||
if (
|
||||
not dataclass_field.init
|
||||
and dataclass_field.default is dataclasses.MISSING
|
||||
and dataclass_field.default_factory is dataclasses.MISSING
|
||||
):
|
||||
# TODO: We should probably do something with this so that validate_assignment behaves properly
|
||||
# Issue: https://github.com/pydantic/pydantic/issues/5470
|
||||
continue
|
||||
|
||||
if isinstance(dataclass_field.default, FieldInfo_):
|
||||
if dataclass_field.default.init_var:
|
||||
if dataclass_field.default.init is False:
|
||||
raise PydanticUserError(
|
||||
f'Dataclass field {ann_name} has init=False and init_var=True, but these are mutually exclusive.',
|
||||
code='clashing-init-and-init-var',
|
||||
)
|
||||
|
||||
# TODO: same note as above re validate_assignment
|
||||
continue
|
||||
field_info = FieldInfo_.from_annotated_attribute(
|
||||
ann_type, dataclass_field.default, _source=AnnotationSource.DATACLASS
|
||||
)
|
||||
else:
|
||||
field_info = FieldInfo_.from_annotated_attribute(
|
||||
ann_type, dataclass_field, _source=AnnotationSource.DATACLASS
|
||||
)
|
||||
|
||||
fields[ann_name] = field_info
|
||||
|
||||
if field_info.default is not PydanticUndefined and isinstance(
|
||||
getattr(cls, ann_name, field_info), FieldInfo_
|
||||
):
|
||||
# We need this to fix the default when the "default" from __dataclass_fields__ is a pydantic.FieldInfo
|
||||
setattr(cls, ann_name, field_info.default)
|
||||
|
||||
if typevars_map:
|
||||
for field in fields.values():
|
||||
# We don't pass any ns, as `field.annotation`
|
||||
# was already evaluated. TODO: is this method relevant?
|
||||
# Can't we juste use `_generics.replace_types`?
|
||||
field.apply_typevars_map(typevars_map)
|
||||
|
||||
if config_wrapper is not None and config_wrapper.use_attribute_docstrings:
|
||||
_update_fields_from_docstrings(
|
||||
cls,
|
||||
fields,
|
||||
# We can't rely on the (more reliable) frame inspection method
|
||||
# for stdlib dataclasses:
|
||||
use_inspect=not hasattr(cls, '__is_pydantic_dataclass__'),
|
||||
)
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def is_valid_field_name(name: str) -> bool:
|
||||
return not name.startswith('_')
|
||||
|
||||
|
||||
def is_valid_privateattr_name(name: str) -> bool:
|
||||
return name.startswith('_') and not name.startswith('__')
|
||||
|
||||
|
||||
def takes_validated_data_argument(
|
||||
default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any],
|
||||
) -> TypeIs[Callable[[dict[str, Any]], Any]]:
|
||||
"""Whether the provided default factory callable has a validated data parameter."""
|
||||
try:
|
||||
sig = signature(default_factory)
|
||||
except (ValueError, TypeError):
|
||||
# `inspect.signature` might not be able to infer a signature, e.g. with C objects.
|
||||
# In this case, we assume no data argument is present:
|
||||
return False
|
||||
|
||||
parameters = list(sig.parameters.values())
|
||||
|
||||
return len(parameters) == 1 and can_be_positional(parameters[0]) and parameters[0].default is Parameter.empty
|
Reference in New Issue
Block a user