관리-도구
편집 파일: _model_construction.py
"""Private logic for creating models.""" from __future__ import annotations as _annotations import builtins import operator import typing import warnings import weakref from abc import ABCMeta from functools import partial from types import FunctionType from typing import Any, Callable, Generic, NoReturn import typing_extensions from pydantic_core import PydanticUndefined, SchemaSerializer from typing_extensions import dataclass_transform, deprecated 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 from ._generate_schema import GenerateSchema from ._generics import PydanticGenericMetadata, get_model_typevars_map from ._mock_val_ser import set_model_mocks from ._schema_generation_shared import CallbackGetCoreSchemaHandler from ._signature import generate_pydantic_signature from ._typing_extra import get_cls_types_namespace, is_annotated, is_classvar, parent_frame_namespace from ._utils import ClassAttribute, SafeGetItemProxy from ._validate_call import ValidateCallWrapper if typing.TYPE_CHECKING: from ..fields import Field as PydanticModelField from ..fields import FieldInfo, ModelPrivateAttr from ..fields import PrivateAttr as PydanticModelPrivateAttr from ..main import BaseModel else: # See PyCharm issues https://youtrack.jetbrains.com/issue/PY-21915 # and https://youtrack.jetbrains.com/issue/PY-51428 DeprecationWarning = PydanticDeprecatedSince20 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') return super().__setitem__(k, v) @dataclass_transform(kw_only_default=True, field_specifiers=(PydanticModelField, PydanticModelPrivateAttr)) 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: base_field_names, class_vars, base_private_attributes = mcs._collect_bases_data(bases) config_wrapper = ConfigWrapper.for_model(bases, namespace, kwargs) namespace['model_config'] = config_wrapper.config_dict private_attributes = inspect_namespace( namespace, 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 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: type[BaseModel] = super().__new__(mcs, cls_name, bases, namespace, **kwargs) # type: ignore from ..main import BaseModel 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_decorators__ = DecoratorInfos.build(cls) # 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 parametrized # 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 = getattr(cls, '__pydantic_parent_namespace__', None) if isinstance(parent_namespace, dict): parent_namespace = unpack_lenient_weakvaluedict(parent_namespace) types_namespace = get_cls_types_namespace(cls, parent_namespace) set_model_fields(cls, bases, config_wrapper, types_namespace) if config_wrapper.frozen and '__hash__' not in namespace: set_default_hash_func(cls, bases) complete_model_class( cls, cls_name, config_wrapper, raise_errors=False, types_namespace=types_namespace, create_model_module=_create_model_module, ) # If this is placed before the complete_model_class call above, # the generic computed fields return type is set to PydanticUndefined cls.model_computed_fields = {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()} set_deprecated_descriptors(cls) # 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: # this is the BaseModel class itself being created, no logic required return super().__new__(mcs, cls_name, bases, namespace, **kwargs) if not typing.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() def __instancecheck__(self, instance: Any) -> bool: """Avoid calling ABC _abc_subclasscheck unless we're pretty sure. See #3829 and python/cpython#92810 """ return hasattr(instance, '__pydantic_validator__') and super().__instancecheck__(instance) @staticmethod def _collect_bases_data(bases: tuple[type[Any], ...]) -> tuple[set[str], set[str], dict[str, ModelPrivateAttr]]: from ..main import BaseModel 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, 'model_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 `model_fields` instead.', category=None) def __fields__(self) -> dict[str, FieldInfo]: warnings.warn( 'The `__fields__` attribute is deprecated, use `model_fields` instead.', PydanticDeprecatedSince20 ) return self.model_fields # type: ignore 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 initialise 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(): default = private_attr.get_default() 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'] from ..main import BaseModel 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], 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. 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 contains 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 FieldInfo, ModelPrivateAttr, PrivateAttr all_ignored_types = ignored_types + default_ignored_types() private_attributes: dict[str, ModelPrivateAttr] = {} raw_annotations = namespace.get('__annotations__', {}) if '__root__' in raw_annotations or '__root__' in namespace: raise TypeError("To define root models, use `pydantic.RootModel` rather than a field called '__root__'") 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(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(raw_annotations[var_name]): private_attributes[var_name] = 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 and not is_classvar(ann_type) and ann_type not in all_ignored_types and getattr(ann_type, '__module__', None) != 'functools' ): if is_annotated(ann_type): _, *metadata = typing_extensions.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.model_fields.keys()) if cls.model_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], bases: tuple[type[Any], ...], config_wrapper: ConfigWrapper, types_namespace: dict[str, Any] ) -> None: """Collect and set `cls.model_fields` and `cls.__class_vars__`. Args: cls: BaseModel or dataclass. bases: Parents of the class, generally `cls.__bases__`. config_wrapper: The config wrapper instance. types_namespace: Optional extra namespace to look for types in. """ typevars_map = get_model_typevars_map(cls) fields, class_vars = collect_model_fields(cls, bases, config_wrapper, types_namespace, typevars_map=typevars_map) cls.model_fields = fields 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], cls_name: str, config_wrapper: ConfigWrapper, *, raise_errors: bool = True, types_namespace: dict[str, Any] | None, create_model_module: str | None = None, ) -> 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. cls_name: The model or dataclass name. config_wrapper: The config wrapper instance. raise_errors: Whether to raise errors. types_namespace: Optional extra namespace to look for types in. create_model_module: The module of the class to be created, if created by `create_model`. 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) gen_schema = GenerateSchema( config_wrapper, types_namespace, typevars_map, ) handler = CallbackGetCoreSchemaHandler( partial(gen_schema.generate_schema, from_dunder_get_core_schema=False), gen_schema, ref_mode='unpack', ) if config_wrapper.defer_build and 'model' in config_wrapper.experimental_defer_build_mode: set_model_mocks(cls, cls_name) return False try: schema = cls.__get_pydantic_core_schema__(cls, handler) except PydanticUndefinedAnnotation as e: if raise_errors: raise set_model_mocks(cls, cls_name, f'`{e.name}`') return False core_config = config_wrapper.core_config(cls) try: schema = gen_schema.clean_schema(schema) except gen_schema.CollectedInvalid: set_model_mocks(cls, cls_name) return False # debug(schema) 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, ) cls.__pydantic_serializer__ = SchemaSerializer(schema, core_config) cls.__pydantic_complete__ = True # set __signature__ attr only for model class, but not for its instances cls.__signature__ = ClassAttribute( '__signature__', generate_pydantic_signature(init=cls.__init__, fields=cls.model_fields, config_wrapper=config_wrapper), ) 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.model_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.model_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: """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: raise AttributeError(self.field_name) warnings.warn(self.msg, builtins.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 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 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 def default_ignored_types() -> tuple[type[Any], ...]: from ..fields import ComputedFieldInfo return ( FunctionType, property, classmethod, staticmethod, PydanticDescriptorProxy, ComputedFieldInfo, ValidateCallWrapper, )