"""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 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 from typing_inspection.introspection import AnnotationSource from pydantic import PydanticDeprecatedSince211 from pydantic.errors import PydanticUserError from ..aliases import AliasGenerator 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, get_first_not_none if TYPE_CHECKING: from annotated_types import BaseMetadata from ..fields import FieldInfo from ..main import BaseModel from ._dataclasses import PydanticDataclass, 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 _check_protected_namespaces( protected_namespaces: tuple[str | Pattern[str], ...], ann_name: str, bases: tuple[type[Any], ...], cls_name: str, ) -> None: BaseModel = import_cached_base_model() for protected_namespace in protected_namespaces: ns_violation = 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 ValueError( f'Field {ann_name!r} conflicts with member {getattr(b, ann_name)}' f' of protected namespace {protected_namespace!r}.' ) else: valid_namespaces: list[str] = [] for pn in protected_namespaces: if isinstance(pn, Pattern): if not pn.match(ann_name): valid_namespaces.append(f're.compile({pn.pattern!r})') else: if not ann_name.startswith(pn): valid_namespaces.append(f"'{pn}'") valid_namespaces_str = f'({", ".join(valid_namespaces)}{",)" if len(valid_namespaces) == 1 else ")"}' warnings.warn( f'Field {ann_name!r} in {cls_name!r} conflicts with protected namespace {protected_namespace!r}.\n\n' f"You may be able to solve this by setting the 'protected_namespaces' configuration to {valid_namespaces_str}.", UserWarning, stacklevel=5, ) 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 _apply_field_title_generator_to_field_info( title_generator: Callable[[str, FieldInfo], str], field_name: str, field_info: FieldInfo, ): if field_info.title is None: title = title_generator(field_name, field_info) if not isinstance(title, str): raise TypeError(f'field_title_generator {title_generator} must return str, not {title.__class__}') field_info.title = title def _apply_alias_generator_to_field_info( alias_generator: Callable[[str], str] | AliasGenerator, field_name: str, field_info: FieldInfo ): """Apply an alias generator to aliases on a `FieldInfo` instance if appropriate. Args: alias_generator: A callable that takes a string and returns a string, or an `AliasGenerator` instance. field_name: The name of the field from which to generate the alias. field_info: The `FieldInfo` instance to which the alias generator is (maybe) applied. """ # Apply an alias_generator if # 1. An alias is not specified # 2. An alias is specified, but the priority is <= 1 if ( field_info.alias_priority is None or field_info.alias_priority <= 1 or field_info.alias is None or field_info.validation_alias is None or field_info.serialization_alias is None ): alias, validation_alias, serialization_alias = None, None, None if isinstance(alias_generator, AliasGenerator): alias, validation_alias, serialization_alias = alias_generator.generate_aliases(field_name) elif callable(alias_generator): alias = alias_generator(field_name) if not isinstance(alias, str): raise TypeError(f'alias_generator {alias_generator} must return str, not {alias.__class__}') # if priority is not set, we set to 1 # which supports the case where the alias_generator from a child class is used # to generate an alias for a field in a parent class if field_info.alias_priority is None or field_info.alias_priority <= 1: field_info.alias_priority = 1 # if the priority is 1, then we set the aliases to the generated alias if field_info.alias_priority == 1: field_info.serialization_alias = get_first_not_none(serialization_alias, alias) field_info.validation_alias = get_first_not_none(validation_alias, alias) field_info.alias = alias # if any of the aliases are not set, then we set them to the corresponding generated alias if field_info.alias is None: field_info.alias = alias if field_info.serialization_alias is None: field_info.serialization_alias = get_first_not_none(serialization_alias, alias) if field_info.validation_alias is None: field_info.validation_alias = get_first_not_none(validation_alias, alias) def update_field_from_config(config_wrapper: ConfigWrapper, field_name: str, field_info: FieldInfo) -> None: """Update the `FieldInfo` instance from the configuration set on the model it belongs to. This will apply the title and alias generators from the configuration. Args: config_wrapper: The configuration from the model. field_name: The field name the `FieldInfo` instance is attached to. field_info: The `FieldInfo` instance to update. """ field_title_generator = field_info.field_title_generator or config_wrapper.field_title_generator if field_title_generator is not None: _apply_field_title_generator_to_field_info(field_title_generator, field_name, field_info) if config_wrapper.alias_generator is not None: _apply_alias_generator_to_field_info(config_wrapper.alias_generator, field_name, field_info) _deprecated_method_names = {'dict', 'json', 'copy', '_iter', '_copy_and_set_values', '_calculate_keys'} _deprecated_classmethod_names = { 'parse_obj', 'parse_raw', 'parse_file', 'from_orm', 'construct', 'schema', 'schema_json', 'validate', 'update_forward_refs', '_get_value', } 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. """ FieldInfo_ = import_cached_field_info() BaseModel_ = import_cached_base_model() 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 = _typing_extra.safe_get_annotations(cls) 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 _check_protected_namespaces( protected_namespaces=config_wrapper.protected_namespaces, ann_name=ann_name, bases=bases, cls_name=cls.__name__, ) if _typing_extra.is_classvar_annotation(ann_type): class_vars.add(ann_name) continue assigned_value = getattr(cls, ann_name, PydanticUndefined) if assigned_value is not PydanticUndefined and ( # One of the deprecated instance methods was used as a field name (e.g. `dict()`): any(getattr(BaseModel_, depr_name, None) is assigned_value for depr_name in _deprecated_method_names) # One of the deprecated class methods was used as a field name (e.g. `schema()`): or ( hasattr(assigned_value, '__func__') and any( getattr(getattr(BaseModel_, depr_name, None), '__func__', None) is assigned_value.__func__ # pyright: ignore[reportAttributeAccessIssue] for depr_name in _deprecated_classmethod_names ) ) ): # Then `assigned_value` would be the method, even though no default was specified: assigned_value = 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, stacklevel=4, ) if assigned_value is PydanticUndefined: # no assignment, just a plain annotation if ann_name in annotations or ann_name not in parent_fields_lookup: # field is either: # - present in the current model's annotations (and *not* from parent classes) # - not found on any base classes; this seems to be caused by fields bot getting # generated due to models not being fully defined while initializing recursive models. # Nothing stops us from just creating a `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: # 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 = parent_fields_lookup[ann_name]._copy() else: # An assigned value is present (either the default value, or a `Field()` function) 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__')`). default = assigned_value.default.__get__(None, cls) assigned_value.default = default assigned_value._attributes_set['default'] = default field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value, _source=AnnotationSource.CLASS) # Store the original annotation and assignment value that should be used to rebuild the field info later. # Note that the assignment is always stored as the annotation might contain a type var that is later # parameterized with an unknown forward reference (and we'll need it to rebuild the field info): field_info._original_assignment = assigned_value if not evaluated: field_info._complete = False field_info._original_annotation = ann_type 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[] = .`', 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 field_info._complete: # If not complete, this will be called in `rebuild_model_fields()`: update_field_from_config(config_wrapper, 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 rebuild_model_fields( cls: type[BaseModel], *, config_wrapper: ConfigWrapper, 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. Raises: NameError: If one of the annotations failed to evaluate. 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: existing_desc = field_info.description 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: new_field = FieldInfo_.from_annotation(ann, _source=AnnotationSource.CLASS) else: new_field = FieldInfo_.from_annotated_attribute(ann, assign, _source=AnnotationSource.CLASS) # The description might come from the docstring if `use_attribute_docstrings` was `True`: new_field.description = new_field.description if new_field.description is not None else existing_desc update_field_from_config(config_wrapper, f_name, new_field) rebuilt_fields[f_name] = new_field return rebuilt_fields def collect_dataclass_fields( cls: type[StandardDataclass], *, config_wrapper: ConfigWrapper, ns_resolver: NsResolver | None = None, typevars_map: dict[Any, Any] | None = None, ) -> dict[str, FieldInfo]: """Collect the fields of a dataclass. Args: cls: dataclass. config_wrapper: The config wrapper instance. 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. 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(): 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 globalns, localns = ns_resolver.types_namespace ann_type, evaluated = _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 ) field_info._original_assignment = dataclass_field.default else: field_info = FieldInfo_.from_annotated_attribute( ann_type, dataclass_field, _source=AnnotationSource.DATACLASS ) field_info._original_assignment = dataclass_field if not evaluated: field_info._complete = False field_info._original_annotation = ann_type fields[ann_name] = field_info update_field_from_config(config_wrapper, 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.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 rebuild_dataclass_fields( cls: type[PydanticDataclass], *, config_wrapper: ConfigWrapper, ns_resolver: NsResolver, typevars_map: Mapping[TypeVar, Any], ) -> dict[str, FieldInfo]: """Rebuild the (already present) dataclass fields by trying to reevaluate annotations. This function should be called whenever a dataclass with incomplete fields is encountered. Raises: NameError: If one of the annotations failed to evaluate. Note: This function *doesn't* mutate the dataclass fields in place, as it can be called during schema generation, where you don't want to mutate other dataclass'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: existing_desc = field_info.description ann = _typing_extra.eval_type( field_info._original_annotation, *ns_resolver.types_namespace, ) ann = _generics.replace_types(ann, typevars_map) new_field = FieldInfo_.from_annotated_attribute( ann, field_info._original_assignment, _source=AnnotationSource.DATACLASS, ) # The description might come from the docstring if `use_attribute_docstrings` was `True`: new_field.description = new_field.description if new_field.description is not None else existing_desc update_field_from_config(config_wrapper, f_name, new_field) rebuilt_fields[f_name] = new_field return rebuilt_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