diff --git a/venv/Lib/site-packages/pydantic/_internal/_decorators.py b/venv/Lib/site-packages/pydantic/_internal/_decorators.py new file mode 100644 index 0000000..e24ad2c --- /dev/null +++ b/venv/Lib/site-packages/pydantic/_internal/_decorators.py @@ -0,0 +1,873 @@ +"""Logic related to validators applied to models etc. via the `@field_validator` and `@model_validator` decorators.""" + +from __future__ import annotations as _annotations + +import types +from collections import deque +from collections.abc import Iterable +from copy import copy +from dataclasses import dataclass, field +from functools import cached_property, partial, partialmethod +from inspect import Parameter, Signature, isdatadescriptor, ismethoddescriptor +from itertools import islice +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Literal, TypeVar, Union + +from pydantic_core import PydanticUndefined, PydanticUndefinedType, core_schema +from typing_extensions import Self, TypeAlias, is_typeddict + +from ..errors import PydanticUserError +from ._core_utils import get_type_ref +from ._internal_dataclass import slots_true +from ._namespace_utils import GlobalsNamespace, MappingNamespace +from ._typing_extra import get_function_type_hints, signature_no_eval +from ._utils import can_be_positional + +if TYPE_CHECKING: + from ..fields import ComputedFieldInfo + from ..functional_validators import FieldValidatorModes + from ._config import ConfigWrapper + + +@dataclass(**slots_true) +class ValidatorDecoratorInfo: + """A container for data from `@validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@validator'. + fields: A tuple of field names the validator should be called on. + mode: The proposed validator mode. + each_item: For complex objects (sets, lists etc.) whether to validate individual + elements rather than the whole object. + always: Whether this method and other validators should be called even if the value is missing. + check_fields: Whether to check that the fields actually exist on the model. + """ + + decorator_repr: ClassVar[str] = '@validator' + + fields: tuple[str, ...] + mode: Literal['before', 'after'] + each_item: bool + always: bool + check_fields: bool | None + + +@dataclass(**slots_true) +class FieldValidatorDecoratorInfo: + """A container for data from `@field_validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@field_validator'. + fields: A tuple of field names the validator should be called on. + mode: The proposed validator mode. + check_fields: Whether to check that the fields actually exist on the model. + json_schema_input_type: The input type of the function. This is only used to generate + the appropriate JSON Schema (in validation mode) and can only specified + when `mode` is either `'before'`, `'plain'` or `'wrap'`. + """ + + decorator_repr: ClassVar[str] = '@field_validator' + + fields: tuple[str, ...] + mode: FieldValidatorModes + check_fields: bool | None + json_schema_input_type: Any + + +@dataclass(**slots_true) +class RootValidatorDecoratorInfo: + """A container for data from `@root_validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@root_validator'. + mode: The proposed validator mode. + """ + + decorator_repr: ClassVar[str] = '@root_validator' + mode: Literal['before', 'after'] + + +@dataclass(**slots_true) +class FieldSerializerDecoratorInfo: + """A container for data from `@field_serializer` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@field_serializer'. + fields: A tuple of field names the serializer should be called on. + mode: The proposed serializer mode. + return_type: The type of the serializer's return value. + when_used: The serialization condition. Accepts a string with values `'always'`, `'unless-none'`, `'json'`, + and `'json-unless-none'`. + check_fields: Whether to check that the fields actually exist on the model. + """ + + decorator_repr: ClassVar[str] = '@field_serializer' + fields: tuple[str, ...] + mode: Literal['plain', 'wrap'] + return_type: Any + when_used: core_schema.WhenUsed + check_fields: bool | None + + +@dataclass(**slots_true) +class ModelSerializerDecoratorInfo: + """A container for data from `@model_serializer` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@model_serializer'. + mode: The proposed serializer mode. + return_type: The type of the serializer's return value. + when_used: The serialization condition. Accepts a string with values `'always'`, `'unless-none'`, `'json'`, + and `'json-unless-none'`. + """ + + decorator_repr: ClassVar[str] = '@model_serializer' + mode: Literal['plain', 'wrap'] + return_type: Any + when_used: core_schema.WhenUsed + + +@dataclass(**slots_true) +class ModelValidatorDecoratorInfo: + """A container for data from `@model_validator` so that we can access it + while building the pydantic-core schema. + + Attributes: + decorator_repr: A class variable representing the decorator string, '@model_validator'. + mode: The proposed serializer mode. + """ + + decorator_repr: ClassVar[str] = '@model_validator' + mode: Literal['wrap', 'before', 'after'] + + +DecoratorInfo: TypeAlias = """Union[ + ValidatorDecoratorInfo, + FieldValidatorDecoratorInfo, + RootValidatorDecoratorInfo, + FieldSerializerDecoratorInfo, + ModelSerializerDecoratorInfo, + ModelValidatorDecoratorInfo, + ComputedFieldInfo, +]""" + +ReturnType = TypeVar('ReturnType') +DecoratedType: TypeAlias = ( + 'Union[classmethod[Any, Any, ReturnType], staticmethod[Any, ReturnType], Callable[..., ReturnType], property]' +) + + +@dataclass # can't use slots here since we set attributes on `__post_init__` +class PydanticDescriptorProxy(Generic[ReturnType]): + """Wrap a classmethod, staticmethod, property or unbound function + and act as a descriptor that allows us to detect decorated items + from the class' attributes. + + This class' __get__ returns the wrapped item's __get__ result, + which makes it transparent for classmethods and staticmethods. + + Attributes: + wrapped: The decorator that has to be wrapped. + decorator_info: The decorator info. + shim: A wrapper function to wrap V1 style function. + """ + + wrapped: DecoratedType[ReturnType] + decorator_info: DecoratorInfo + shim: Callable[[Callable[..., Any]], Callable[..., Any]] | None = None + + def __post_init__(self): + for attr in 'setter', 'deleter': + if hasattr(self.wrapped, attr): + f = partial(self._call_wrapped_attr, name=attr) + setattr(self, attr, f) + + def _call_wrapped_attr(self, func: Callable[[Any], None], *, name: str) -> PydanticDescriptorProxy[ReturnType]: + self.wrapped = getattr(self.wrapped, name)(func) + if isinstance(self.wrapped, property): + # update ComputedFieldInfo.wrapped_property + from ..fields import ComputedFieldInfo + + if isinstance(self.decorator_info, ComputedFieldInfo): + self.decorator_info.wrapped_property = self.wrapped + return self + + def __get__(self, obj: object | None, obj_type: type[object] | None = None) -> PydanticDescriptorProxy[ReturnType]: + try: + return self.wrapped.__get__(obj, obj_type) # pyright: ignore[reportReturnType] + except AttributeError: + # not a descriptor, e.g. a partial object + return self.wrapped # type: ignore[return-value] + + def __set_name__(self, instance: Any, name: str) -> None: + if hasattr(self.wrapped, '__set_name__'): + self.wrapped.__set_name__(instance, name) # pyright: ignore[reportFunctionMemberAccess] + + def __getattr__(self, name: str, /) -> Any: + """Forward checks for __isabstractmethod__ and such.""" + return getattr(self.wrapped, name) + + +DecoratorInfoType = TypeVar('DecoratorInfoType', bound=DecoratorInfo) + + +@dataclass(**slots_true) +class Decorator(Generic[DecoratorInfoType]): + """A generic container class to join together the decorator metadata + (metadata from decorator itself, which we have when the + decorator is called but not when we are building the core-schema) + and the bound function (which we have after the class itself is created). + + Attributes: + cls_ref: The class ref. + cls_var_name: The decorated function name. + func: The decorated function. + shim: A wrapper function to wrap V1 style function. + info: The decorator info. + """ + + cls_ref: str + cls_var_name: str + func: Callable[..., Any] + shim: Callable[[Any], Any] | None + info: DecoratorInfoType + + @staticmethod + def build( + cls_: Any, + *, + cls_var_name: str, + shim: Callable[[Any], Any] | None, + info: DecoratorInfoType, + ) -> Decorator[DecoratorInfoType]: + """Build a new decorator. + + Args: + cls_: The class. + cls_var_name: The decorated function name. + shim: A wrapper function to wrap V1 style function. + info: The decorator info. + + Returns: + The new decorator instance. + """ + func = get_attribute_from_bases(cls_, cls_var_name) + if shim is not None: + func = shim(func) + func = unwrap_wrapped_function(func, unwrap_partial=False) + if not callable(func): + # TODO most likely this branch can be removed when we drop support for Python 3.12: + # This branch will get hit for classmethod properties + attribute = get_attribute_from_base_dicts(cls_, cls_var_name) # prevents the binding call to `__get__` + if isinstance(attribute, PydanticDescriptorProxy): + func = unwrap_wrapped_function(attribute.wrapped) + return Decorator( + cls_ref=get_type_ref(cls_), + cls_var_name=cls_var_name, + func=func, + shim=shim, + info=info, + ) + + def bind_to_cls(self, cls: Any) -> Decorator[DecoratorInfoType]: + """Bind the decorator to a class. + + Args: + cls: the class. + + Returns: + The new decorator instance. + """ + return self.build( + cls, + cls_var_name=self.cls_var_name, + shim=self.shim, + info=copy(self.info), + ) + + +def get_bases(tp: type[Any]) -> tuple[type[Any], ...]: + """Get the base classes of a class or typeddict. + + Args: + tp: The type or class to get the bases. + + Returns: + The base classes. + """ + if is_typeddict(tp): + return tp.__orig_bases__ # type: ignore + try: + return tp.__bases__ + except AttributeError: + return () + + +def mro(tp: type[Any]) -> tuple[type[Any], ...]: + """Calculate the Method Resolution Order of bases using the C3 algorithm. + + See https://www.python.org/download/releases/2.3/mro/ + """ + # try to use the existing mro, for performance mainly + # but also because it helps verify the implementation below + if not is_typeddict(tp): + try: + return tp.__mro__ + except AttributeError: + # GenericAlias and some other cases + pass + + bases = get_bases(tp) + return (tp,) + mro_for_bases(bases) + + +def mro_for_bases(bases: tuple[type[Any], ...]) -> tuple[type[Any], ...]: + def merge_seqs(seqs: list[deque[type[Any]]]) -> Iterable[type[Any]]: + while True: + non_empty = [seq for seq in seqs if seq] + if not non_empty: + # Nothing left to process, we're done. + return + candidate: type[Any] | None = None + for seq in non_empty: # Find merge candidates among seq heads. + candidate = seq[0] + not_head = [s for s in non_empty if candidate in islice(s, 1, None)] + if not_head: + # Reject the candidate. + candidate = None + else: + break + if not candidate: + raise TypeError('Inconsistent hierarchy, no C3 MRO is possible') + yield candidate + for seq in non_empty: + # Remove candidate. + if seq[0] == candidate: + seq.popleft() + + seqs = [deque(mro(base)) for base in bases] + [deque(bases)] + return tuple(merge_seqs(seqs)) + + +_sentinel = object() + + +def get_attribute_from_bases(tp: type[Any] | tuple[type[Any], ...], name: str) -> Any: + """Get the attribute from the next class in the MRO that has it, + aiming to simulate calling the method on the actual class. + + The reason for iterating over the mro instead of just getting + the attribute (which would do that for us) is to support TypedDict, + which lacks a real __mro__, but can have a virtual one constructed + from its bases (as done here). + + Args: + tp: The type or class to search for the attribute. If a tuple, this is treated as a set of base classes. + name: The name of the attribute to retrieve. + + Returns: + Any: The attribute value, if found. + + Raises: + AttributeError: If the attribute is not found in any class in the MRO. + """ + if isinstance(tp, tuple): + for base in mro_for_bases(tp): + attribute = base.__dict__.get(name, _sentinel) + if attribute is not _sentinel: + attribute_get = getattr(attribute, '__get__', None) + if attribute_get is not None: + return attribute_get(None, tp) + return attribute + raise AttributeError(f'{name} not found in {tp}') + else: + try: + return getattr(tp, name) + except AttributeError: + return get_attribute_from_bases(mro(tp), name) + + +def get_attribute_from_base_dicts(tp: type[Any], name: str) -> Any: + """Get an attribute out of the `__dict__` following the MRO. + This prevents the call to `__get__` on the descriptor, and allows + us to get the original function for classmethod properties. + + Args: + tp: The type or class to search for the attribute. + name: The name of the attribute to retrieve. + + Returns: + Any: The attribute value, if found. + + Raises: + KeyError: If the attribute is not found in any class's `__dict__` in the MRO. + """ + for base in reversed(mro(tp)): + if name in base.__dict__: + return base.__dict__[name] + return tp.__dict__[name] # raise the error + + +@dataclass(**slots_true) +class DecoratorInfos: + """Mapping of name in the class namespace to decorator info. + + note that the name in the class namespace is the function or attribute name + not the field name! + """ + + validators: dict[str, Decorator[ValidatorDecoratorInfo]] = field(default_factory=dict) + field_validators: dict[str, Decorator[FieldValidatorDecoratorInfo]] = field(default_factory=dict) + root_validators: dict[str, Decorator[RootValidatorDecoratorInfo]] = field(default_factory=dict) + field_serializers: dict[str, Decorator[FieldSerializerDecoratorInfo]] = field(default_factory=dict) + model_serializers: dict[str, Decorator[ModelSerializerDecoratorInfo]] = field(default_factory=dict) + model_validators: dict[str, Decorator[ModelValidatorDecoratorInfo]] = field(default_factory=dict) + computed_fields: dict[str, Decorator[ComputedFieldInfo]] = field(default_factory=dict) + + @classmethod + def build( + cls, + typ: type[Any], + # Default to `True` for backwards compatibility: + replace_wrapped_methods: bool = True, + ) -> Self: + """Build a `DecoratorInfos` instance for the given model, dataclass or `TypedDict` type. + + Decorators from parent classes are included, including "bare" classes (e.g. if `typ` + is a Pydantic model, non Pydantic parent model classes are also taken into account). + The collection of the decorators happens by respecting the MRO. + + If one of the bases has an `__pydantic_decorators__` attribute set, it is assumed to be + a `DecoratorInfos` instance and is used as-is. The `__pydantic_decorators__` attribute + is *not* being set on the provided `typ`. + + Args: + typ: The model, dataclass or `TypedDict` type to use when building the `DecoratorInfos` instance. + replace_wrapped_methods: Whether to replace the decorator's wrapped methods on `typ`. + This is useful e.g. for field validators which are initially class methods. This should + only be set to `True` if `typ` is a Pydantic model or dataclass (otherwise this results + in mutations of classes Pydantic doesn't "own"). + """ + # reminder: dicts are ordered and replacement does not alter the order + res = cls() + # Iterate over the bases, without the actual `typ`. + # `1:-1` because we don't need to include `object`/`TypedDict`: + for base in reversed(mro(typ)[1:-1]): + existing: DecoratorInfos | None = base.__dict__.get('__pydantic_decorators__') + if existing is None: + existing, _ = _decorator_infos_for_class(base, collect_to_replace=False) + res.validators.update({k: v.bind_to_cls(typ) for k, v in existing.validators.items()}) + res.field_validators.update({k: v.bind_to_cls(typ) for k, v in existing.field_validators.items()}) + res.root_validators.update({k: v.bind_to_cls(typ) for k, v in existing.root_validators.items()}) + res.field_serializers.update({k: v.bind_to_cls(typ) for k, v in existing.field_serializers.items()}) + res.model_serializers.update({k: v.bind_to_cls(typ) for k, v in existing.model_serializers.items()}) + res.model_validators.update({k: v.bind_to_cls(typ) for k, v in existing.model_validators.items()}) + res.computed_fields.update({k: v.bind_to_cls(typ) for k, v in existing.computed_fields.items()}) + + decorator_infos, to_replace = _decorator_infos_for_class(typ, collect_to_replace=True) + + res.validators.update(decorator_infos.validators) + res.field_validators.update(decorator_infos.field_validators) + res.root_validators.update(decorator_infos.root_validators) + res.field_serializers.update(decorator_infos.field_serializers) + res.model_serializers.update(decorator_infos.model_serializers) + res.model_validators.update(decorator_infos.model_validators) + res.computed_fields.update(decorator_infos.computed_fields) + + if replace_wrapped_methods and to_replace: + for name, value in to_replace: + setattr(typ, name, value) + + res._validate() + return res + + def _validate(self) -> None: + seen: set[str] = set() + for field_ser in self.field_serializers.values(): + for f_name in field_ser.info.fields: + if f_name in seen: + raise PydanticUserError( + f'Multiple field serializer functions were defined for field {f_name!r}, this is not allowed.', + code='multiple-field-serializers', + ) + seen.add(f_name) + + def update_from_config(self, config_wrapper: ConfigWrapper) -> None: + """Update the decorator infos from the configuration of the class they are attached to.""" + for name, computed_field_dec in self.computed_fields.items(): + computed_field_dec.info._update_from_config(config_wrapper, name) + + +def _decorator_infos_for_class( + typ: type[Any], + *, + collect_to_replace: bool, +) -> tuple[DecoratorInfos, list[tuple[str, Any]]]: + """Collect a `DecoratorInfos` for class, without looking into bases.""" + res = DecoratorInfos() + to_replace: list[tuple[str, Any]] = [] + + for var_name, var_value in vars(typ).items(): + if isinstance(var_value, PydanticDescriptorProxy): + info = var_value.decorator_info + if isinstance(info, ValidatorDecoratorInfo): + res.validators[var_name] = Decorator.build(typ, cls_var_name=var_name, shim=var_value.shim, info=info) + elif isinstance(info, FieldValidatorDecoratorInfo): + res.field_validators[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, RootValidatorDecoratorInfo): + res.root_validators[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, FieldSerializerDecoratorInfo): + res.field_serializers[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, ModelValidatorDecoratorInfo): + res.model_validators[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + elif isinstance(info, ModelSerializerDecoratorInfo): + res.model_serializers[var_name] = Decorator.build( + typ, cls_var_name=var_name, shim=var_value.shim, info=info + ) + else: + from ..fields import ComputedFieldInfo + + isinstance(var_value, ComputedFieldInfo) + res.computed_fields[var_name] = Decorator.build(typ, cls_var_name=var_name, shim=None, info=info) + if collect_to_replace: + to_replace.append((var_name, var_value.wrapped)) + + return res, to_replace + + +def inspect_validator( + validator: Callable[..., Any], *, mode: FieldValidatorModes, type: Literal['field', 'model'] +) -> bool: + """Look at a field or model validator function and determine whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + validator: The validator function to inspect. + mode: The proposed validator mode. + type: The type of validator, either 'field' or 'model'. + + Returns: + Whether the validator takes an info argument. + """ + try: + sig = signature_no_eval(validator) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no info argument is present: + return False + n_positional = count_positional_required_params(sig) + if mode == 'wrap': + if n_positional == 3: + return True + elif n_positional == 2: + return False + else: + assert mode in {'before', 'after', 'plain'}, f"invalid mode: {mode!r}, expected 'before', 'after' or 'plain" + if n_positional == 2: + return True + elif n_positional == 1: + return False + + raise PydanticUserError( + f'Unrecognized {type} validator function signature for {validator} with `mode={mode}`: {sig}', + code='validator-signature', + ) + + +def inspect_field_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> tuple[bool, bool]: + """Look at a field serializer function and determine if it is a field serializer, + and whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + serializer: The serializer function to inspect. + mode: The serializer mode, either 'plain' or 'wrap'. + + Returns: + Tuple of (is_field_serializer, info_arg). + """ + try: + sig = signature_no_eval(serializer) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no info argument is present and this is not a method: + return (False, False) + + first = next(iter(sig.parameters.values()), None) + is_field_serializer = first is not None and first.name == 'self' + + n_positional = count_positional_required_params(sig) + if is_field_serializer: + # -1 to correct for self parameter + info_arg = _serializer_info_arg(mode, n_positional - 1) + else: + info_arg = _serializer_info_arg(mode, n_positional) + + if info_arg is None: + raise PydanticUserError( + f'Unrecognized field_serializer function signature for {serializer} with `mode={mode}`:{sig}', + code='field-serializer-signature', + ) + + return is_field_serializer, info_arg + + +def inspect_annotated_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> bool: + """Look at a serializer function used via `Annotated` and determine whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + serializer: The serializer function to check. + mode: The serializer mode, either 'plain' or 'wrap'. + + Returns: + info_arg + """ + try: + sig = signature_no_eval(serializer) + except (ValueError, TypeError): + # `inspect.signature` might not be able to infer a signature, e.g. with C objects. + # In this case, we assume no info argument is present: + return False + info_arg = _serializer_info_arg(mode, count_positional_required_params(sig)) + if info_arg is None: + raise PydanticUserError( + f'Unrecognized field_serializer function signature for {serializer} with `mode={mode}`:{sig}', + code='field-serializer-signature', + ) + else: + return info_arg + + +def inspect_model_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> bool: + """Look at a model serializer function and determine whether it takes an info argument. + + An error is raised if the function has an invalid signature. + + Args: + serializer: The serializer function to check. + mode: The serializer mode, either 'plain' or 'wrap'. + + Returns: + `info_arg` - whether the function expects an info argument. + """ + if isinstance(serializer, (staticmethod, classmethod)) or not is_instance_method_from_sig(serializer): + raise PydanticUserError( + '`@model_serializer` must be applied to instance methods', code='model-serializer-instance-method' + ) + + sig = signature_no_eval(serializer) + info_arg = _serializer_info_arg(mode, count_positional_required_params(sig)) + if info_arg is None: + raise PydanticUserError( + f'Unrecognized model_serializer function signature for {serializer} with `mode={mode}`:{sig}', + code='model-serializer-signature', + ) + else: + return info_arg + + +def _serializer_info_arg(mode: Literal['plain', 'wrap'], n_positional: int) -> bool | None: + if mode == 'plain': + if n_positional == 1: + # (input_value: Any, /) -> Any + return False + elif n_positional == 2: + # (model: Any, input_value: Any, /) -> Any + return True + else: + assert mode == 'wrap', f"invalid mode: {mode!r}, expected 'plain' or 'wrap'" + if n_positional == 2: + # (input_value: Any, serializer: SerializerFunctionWrapHandler, /) -> Any + return False + elif n_positional == 3: + # (input_value: Any, serializer: SerializerFunctionWrapHandler, info: SerializationInfo, /) -> Any + return True + + return None + + +AnyDecoratorCallable: TypeAlias = ( + 'Union[classmethod[Any, Any, Any], staticmethod[Any, Any], partialmethod[Any], Callable[..., Any]]' +) + + +def is_instance_method_from_sig(function: AnyDecoratorCallable) -> bool: + """Whether the function is an instance method. + + It will consider a function as instance method if the first parameter of + function is `self`. + + Args: + function: The function to check. + + Returns: + `True` if the function is an instance method, `False` otherwise. + """ + sig = signature_no_eval(unwrap_wrapped_function(function)) + first = next(iter(sig.parameters.values()), None) + if first and first.name == 'self': + return True + return False + + +def ensure_classmethod_based_on_signature(function: AnyDecoratorCallable) -> Any: + """Apply the `@classmethod` decorator on the function. + + Args: + function: The function to apply the decorator on. + + Return: + The `@classmethod` decorator applied function. + """ + if not isinstance( + unwrap_wrapped_function(function, unwrap_class_static_method=False), classmethod + ) and _is_classmethod_from_sig(function): + return classmethod(function) # type: ignore[arg-type] + return function + + +def _is_classmethod_from_sig(function: AnyDecoratorCallable) -> bool: + sig = signature_no_eval(unwrap_wrapped_function(function)) + first = next(iter(sig.parameters.values()), None) + if first and first.name == 'cls': + return True + return False + + +def unwrap_wrapped_function( + func: Any, + *, + unwrap_partial: bool = True, + unwrap_class_static_method: bool = True, +) -> Any: + """Recursively unwraps a wrapped function until the underlying function is reached. + This handles property, functools.partial, functools.partialmethod, staticmethod, and classmethod. + + Args: + func: The function to unwrap. + unwrap_partial: If True (default), unwrap partial and partialmethod decorators. + unwrap_class_static_method: If True (default), also unwrap classmethod and staticmethod + decorators. If False, only unwrap partial and partialmethod decorators. + + Returns: + The underlying function of the wrapped function. + """ + # Define the types we want to check against as a single tuple. + unwrap_types = ( + (property, cached_property) + + ((partial, partialmethod) if unwrap_partial else ()) + + ((staticmethod, classmethod) if unwrap_class_static_method else ()) + ) + + while isinstance(func, unwrap_types): + if unwrap_class_static_method and isinstance(func, (classmethod, staticmethod)): + func = func.__func__ + elif isinstance(func, (partial, partialmethod)): + func = func.func + elif isinstance(func, property): + func = func.fget # arbitrary choice, convenient for computed fields + else: + # Make coverage happy as it can only get here in the last possible case + assert isinstance(func, cached_property) + func = func.func # type: ignore + + return func + + +_function_like = ( + partial, + partialmethod, + types.FunctionType, + types.BuiltinFunctionType, + types.MethodType, + types.WrapperDescriptorType, + types.MethodWrapperType, + types.MemberDescriptorType, +) + + +def get_callable_return_type( + callable_obj: Any, + globalns: GlobalsNamespace | None = None, + localns: MappingNamespace | None = None, +) -> Any | PydanticUndefinedType: + """Get the callable return type. + + Args: + callable_obj: The callable to analyze. + globalns: The globals namespace to use during type annotation evaluation. + localns: The locals namespace to use during type annotation evaluation. + + Returns: + The function return type. + """ + if isinstance(callable_obj, type): + # types are callables, and we assume the return type + # is the type itself (e.g. `int()` results in an instance of `int`). + return callable_obj + + if not isinstance(callable_obj, _function_like): + call_func = getattr(type(callable_obj), '__call__', None) # noqa: B004 + if call_func is not None: + callable_obj = call_func + + hints = get_function_type_hints( + unwrap_wrapped_function(callable_obj), + include_keys={'return'}, + globalns=globalns, + localns=localns, + ) + return hints.get('return', PydanticUndefined) + + +def count_positional_required_params(sig: Signature) -> int: + """Get the number of positional (required) arguments of a signature. + + This function should only be used to inspect signatures of validation and serialization functions. + The first argument (the value being serialized or validated) is counted as a required argument + even if a default value exists. + + Returns: + The number of positional arguments of a signature. + """ + parameters = list(sig.parameters.values()) + return sum( + 1 + for param in parameters + if can_be_positional(param) + # First argument is the value being validated/serialized, and can have a default value + # (e.g. `float`, which has signature `(x=0, /)`). We assume other parameters (the info arg + # for instance) should be required, and thus without any default value. + and (param.default is Parameter.empty or param is parameters[0]) + ) + + +def ensure_property(f: Any) -> Any: + """Ensure that a function is a `property` or `cached_property`, or is a valid descriptor. + + Args: + f: The function to check. + + Returns: + The function, or a `property` or `cached_property` instance wrapping the function. + """ + if ismethoddescriptor(f) or isdatadescriptor(f): + return f + else: + return property(f) diff --git a/venv/Lib/site-packages/pydantic/_internal/_decorators_v1.py b/venv/Lib/site-packages/pydantic/_internal/_decorators_v1.py new file mode 100644 index 0000000..3427377 --- /dev/null +++ b/venv/Lib/site-packages/pydantic/_internal/_decorators_v1.py @@ -0,0 +1,174 @@ +"""Logic for V1 validators, e.g. `@validator` and `@root_validator`.""" + +from __future__ import annotations as _annotations + +from inspect import Parameter, signature +from typing import Any, Union, cast + +from pydantic_core import core_schema +from typing_extensions import Protocol + +from ..errors import PydanticUserError +from ._utils import can_be_positional + + +class V1OnlyValueValidator(Protocol): + """A simple validator, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any) -> Any: ... + + +class V1ValidatorWithValues(Protocol): + """A validator with `values` argument, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, values: dict[str, Any]) -> Any: ... + + +class V1ValidatorWithValuesKwOnly(Protocol): + """A validator with keyword only `values` argument, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, *, values: dict[str, Any]) -> Any: ... + + +class V1ValidatorWithKwargs(Protocol): + """A validator with `kwargs` argument, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, **kwargs: Any) -> Any: ... + + +class V1ValidatorWithValuesAndKwargs(Protocol): + """A validator with `values` and `kwargs` arguments, supported for V1 validators and V2 validators.""" + + def __call__(self, __value: Any, values: dict[str, Any], **kwargs: Any) -> Any: ... + + +V1Validator = Union[ + V1ValidatorWithValues, V1ValidatorWithValuesKwOnly, V1ValidatorWithKwargs, V1ValidatorWithValuesAndKwargs +] + + +def can_be_keyword(param: Parameter) -> bool: + return param.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) + + +def make_generic_v1_field_validator(validator: V1Validator) -> core_schema.WithInfoValidatorFunction: + """Wrap a V1 style field validator for V2 compatibility. + + Args: + validator: The V1 style field validator. + + Returns: + A wrapped V2 style field validator. + + Raises: + PydanticUserError: If the signature is not supported or the parameters are + not available in Pydantic V2. + """ + sig = signature(validator) + + needs_values_kw = False + + for param_num, (param_name, parameter) in enumerate(sig.parameters.items()): + if can_be_keyword(parameter) and param_name in ('field', 'config'): + raise PydanticUserError( + 'The `field` and `config` parameters are not available in Pydantic V2, ' + 'please use the `info` parameter instead.', + code='validator-field-config-info', + ) + if parameter.kind is Parameter.VAR_KEYWORD: + needs_values_kw = True + elif can_be_keyword(parameter) and param_name == 'values': + needs_values_kw = True + elif can_be_positional(parameter) and param_num == 0: + # value + continue + elif parameter.default is Parameter.empty: # ignore params with defaults e.g. bound by functools.partial + raise PydanticUserError( + f'Unsupported signature for V1 style validator {validator}: {sig} is not supported.', + code='validator-v1-signature', + ) + + if needs_values_kw: + # (v, **kwargs), (v, values, **kwargs), (v, *, values, **kwargs) or (v, *, values) + val1 = cast(V1ValidatorWithValues, validator) + + def wrapper1(value: Any, info: core_schema.ValidationInfo) -> Any: + return val1(value, values=info.data) + + return wrapper1 + else: + val2 = cast(V1OnlyValueValidator, validator) + + def wrapper2(value: Any, _: core_schema.ValidationInfo) -> Any: + return val2(value) + + return wrapper2 + + +RootValidatorValues = dict[str, Any] +# technically tuple[model_dict, model_extra, fields_set] | tuple[dataclass_dict, init_vars] +RootValidatorFieldsTuple = tuple[Any, ...] + + +class V1RootValidatorFunction(Protocol): + """A simple root validator, supported for V1 validators and V2 validators.""" + + def __call__(self, __values: RootValidatorValues) -> RootValidatorValues: ... + + +class V2CoreBeforeRootValidator(Protocol): + """V2 validator with mode='before'.""" + + def __call__(self, __values: RootValidatorValues, __info: core_schema.ValidationInfo) -> RootValidatorValues: ... + + +class V2CoreAfterRootValidator(Protocol): + """V2 validator with mode='after'.""" + + def __call__( + self, __fields_tuple: RootValidatorFieldsTuple, __info: core_schema.ValidationInfo + ) -> RootValidatorFieldsTuple: ... + + +def make_v1_generic_root_validator( + validator: V1RootValidatorFunction, pre: bool +) -> V2CoreBeforeRootValidator | V2CoreAfterRootValidator: + """Wrap a V1 style root validator for V2 compatibility. + + Args: + validator: The V1 style field validator. + pre: Whether the validator is a pre validator. + + Returns: + A wrapped V2 style validator. + """ + if pre is True: + # mode='before' for pydantic-core + def _wrapper1(values: RootValidatorValues, _: core_schema.ValidationInfo) -> RootValidatorValues: + return validator(values) + + return _wrapper1 + + # mode='after' for pydantic-core + def _wrapper2(fields_tuple: RootValidatorFieldsTuple, _: core_schema.ValidationInfo) -> RootValidatorFieldsTuple: + if len(fields_tuple) == 2: + # dataclass, this is easy + values, init_vars = fields_tuple + values = validator(values) + return values, init_vars + else: + # ugly hack: to match v1 behaviour, we merge values and model_extra, then split them up based on fields + # afterwards + model_dict, model_extra, fields_set = fields_tuple + if model_extra: + fields = set(model_dict.keys()) + model_dict.update(model_extra) + model_dict_new = validator(model_dict) + for k in list(model_dict_new.keys()): + if k not in fields: + model_extra[k] = model_dict_new.pop(k) + else: + model_dict_new = validator(model_dict) + return model_dict_new, model_extra, fields_set + + return _wrapper2 diff --git a/venv/Lib/site-packages/pydantic/_internal/_discriminated_union.py b/venv/Lib/site-packages/pydantic/_internal/_discriminated_union.py new file mode 100644 index 0000000..fc52f30 --- /dev/null +++ b/venv/Lib/site-packages/pydantic/_internal/_discriminated_union.py @@ -0,0 +1,494 @@ +from __future__ import annotations as _annotations + +from collections.abc import Hashable, Sequence +from typing import TYPE_CHECKING, Any, cast + +from pydantic_core import CoreSchema, core_schema + +from ..errors import PydanticUserError +from . import _core_utils +from ._core_utils import ( + CoreSchemaField, +) + +if TYPE_CHECKING: + from ..types import Discriminator + from ._core_metadata import CoreMetadata + + +class MissingDefinitionForUnionRef(Exception): + """Raised when applying a discriminated union discriminator to a schema + requires a definition that is not yet defined + """ + + def __init__(self, ref: str) -> None: + self.ref = ref + super().__init__(f'Missing definition for ref {self.ref!r}') + + +def set_discriminator_in_metadata(schema: CoreSchema, discriminator: Any) -> None: + metadata = cast('CoreMetadata', schema.setdefault('metadata', {})) + metadata['pydantic_internal_union_discriminator'] = discriminator + + +def apply_discriminator( + schema: core_schema.CoreSchema, + discriminator: str | Discriminator, + definitions: dict[str, core_schema.CoreSchema] | None = None, +) -> core_schema.CoreSchema: + """Applies the discriminator and returns a new core schema. + + Args: + schema: The input schema. + discriminator: The name of the field which will serve as the discriminator. + definitions: A mapping of schema ref to schema. + + Returns: + The new core schema. + + Raises: + TypeError: + - If `discriminator` is used with invalid union variant. + - If `discriminator` is used with `Union` type with one variant. + - If `discriminator` value mapped to multiple choices. + MissingDefinitionForUnionRef: + If the definition for ref is missing. + PydanticUserError: + - If a model in union doesn't have a discriminator field. + - If discriminator field has a non-string alias. + - If discriminator fields have different aliases. + - If discriminator field not of type `Literal`. + """ + from ..types import Discriminator + + if isinstance(discriminator, Discriminator): + if isinstance(discriminator.discriminator, str): + discriminator = discriminator.discriminator + else: + return discriminator._convert_schema(schema) + + return _ApplyInferredDiscriminator(discriminator, definitions or {}).apply(schema) + + +class _ApplyInferredDiscriminator: + """This class is used to convert an input schema containing a union schema into one where that union is + replaced with a tagged-union, with all the associated debugging and performance benefits. + + This is done by: + * Validating that the input schema is compatible with the provided discriminator + * Introspecting the schema to determine which discriminator values should map to which union choices + * Handling various edge cases such as 'definitions', 'default', 'nullable' schemas, and more + + I have chosen to implement the conversion algorithm in this class, rather than a function, + to make it easier to maintain state while recursively walking the provided CoreSchema. + """ + + def __init__(self, discriminator: str, definitions: dict[str, core_schema.CoreSchema]): + # `discriminator` should be the name of the field which will serve as the discriminator. + # It must be the python name of the field, and *not* the field's alias. Note that as of now, + # all members of a discriminated union _must_ use a field with the same name as the discriminator. + # This may change if/when we expose a way to manually specify the TaggedUnionSchema's choices. + self.discriminator = discriminator + + # `definitions` should contain a mapping of schema ref to schema for all schemas which might + # be referenced by some choice + self.definitions = definitions + + # `_discriminator_alias` will hold the value, if present, of the alias for the discriminator + # + # Note: following the v1 implementation, we currently disallow the use of different aliases + # for different choices. This is not a limitation of pydantic_core, but if we try to handle + # this, the inference logic gets complicated very quickly, and could result in confusing + # debugging challenges for users making subtle mistakes. + # + # Rather than trying to do the most powerful inference possible, I think we should eventually + # expose a way to more-manually control the way the TaggedUnionSchema is constructed through + # the use of a new type which would be placed as an Annotation on the Union type. This would + # provide the full flexibility/power of pydantic_core's TaggedUnionSchema where necessary for + # more complex cases, without over-complicating the inference logic for the common cases. + self._discriminator_alias: str | None = None + + # `_should_be_nullable` indicates whether the converted union has `None` as an allowed value. + # If `None` is an acceptable value of the (possibly-wrapped) union, we ignore it while + # constructing the TaggedUnionSchema, but set the `_should_be_nullable` attribute to True. + # Once we have constructed the TaggedUnionSchema, if `_should_be_nullable` is True, we ensure + # that the final schema gets wrapped as a NullableSchema. This has the same semantics on the + # python side, but resolves the issue that `None` cannot correspond to any discriminator values. + self._should_be_nullable = False + + # `_is_nullable` is used to track if the final produced schema will definitely be nullable; + # we set it to True if the input schema is wrapped in a nullable schema that we know will be preserved + # as an indication that, even if None is discovered as one of the union choices, we will not need to wrap + # the final value in another nullable schema. + # + # This is more complicated than just checking for the final outermost schema having type 'nullable' thanks + # to the possible presence of other wrapper schemas such as DefinitionsSchema, WithDefaultSchema, etc. + self._is_nullable = False + + # `_choices_to_handle` serves as a stack of choices to add to the tagged union. Initially, choices + # from the union in the wrapped schema will be appended to this list, and the recursive choice-handling + # algorithm may add more choices to this stack as (nested) unions are encountered. + self._choices_to_handle: list[core_schema.CoreSchema] = [] + + # `_tagged_union_choices` is built during the call to `apply`, and will hold the choices to be included + # in the output TaggedUnionSchema that will replace the union from the input schema + self._tagged_union_choices: dict[Hashable, core_schema.CoreSchema] = {} + + # `_used` is changed to True after applying the discriminator to prevent accidental reuse + self._used = False + + def apply(self, schema: core_schema.CoreSchema) -> core_schema.CoreSchema: + """Return a new CoreSchema based on `schema` that uses a tagged-union with the discriminator provided + to this class. + + Args: + schema: The input schema. + + Returns: + The new core schema. + + Raises: + TypeError: + - If `discriminator` is used with invalid union variant. + - If `discriminator` is used with `Union` type with one variant. + - If `discriminator` value mapped to multiple choices. + ValueError: + If the definition for ref is missing. + PydanticUserError: + - If a model in union doesn't have a discriminator field. + - If discriminator field has a non-string alias. + - If discriminator fields have different aliases. + - If discriminator field not of type `Literal`. + """ + assert not self._used + schema = self._apply_to_root(schema) + if self._should_be_nullable and not self._is_nullable: + schema = core_schema.nullable_schema(schema) + self._used = True + return schema + + def _apply_to_root(self, schema: core_schema.CoreSchema) -> core_schema.CoreSchema: + """This method handles the outer-most stage of recursion over the input schema: + unwrapping nullable or definitions schemas, and calling the `_handle_choice` + method iteratively on the choices extracted (recursively) from the possibly-wrapped union. + """ + if schema['type'] == 'nullable': + self._is_nullable = True + wrapped = self._apply_to_root(schema['schema']) + nullable_wrapper = schema.copy() + nullable_wrapper['schema'] = wrapped + return nullable_wrapper + + if schema['type'] == 'definitions': + wrapped = self._apply_to_root(schema['schema']) + definitions_wrapper = schema.copy() + definitions_wrapper['schema'] = wrapped + return definitions_wrapper + + if schema['type'] == 'definition-ref': + schema_ref = schema['schema_ref'] + if schema_ref not in self.definitions: # pragma: no cover + raise MissingDefinitionForUnionRef(schema_ref) + + def_schema = self.definitions[schema_ref] + # If using a referenceable union as discriminated (e.g. `type Pet = Cat | Dog; field: Pet = Field(discriminator=...)`): + if def_schema['type'] == 'union': + schema = def_schema.copy() + schema.pop('ref') + + if schema['type'] != 'union': + # If the schema is not a union, it probably means it just had a single member and + # was flattened by pydantic_core. + # However, it still may make sense to apply the discriminator to this schema, + # as a way to get discriminated-union-style error messages, so we allow this here. + schema = core_schema.union_schema([schema]) + + # Reverse the choices list before extending the stack so that they get handled in the order they occur + choices_schemas = [v[0] if isinstance(v, tuple) else v for v in schema['choices'][::-1]] + self._choices_to_handle.extend(choices_schemas) + while self._choices_to_handle: + choice = self._choices_to_handle.pop() + self._handle_choice(choice) + + if self._discriminator_alias is not None and self._discriminator_alias != self.discriminator: + # * We need to annotate `discriminator` as a union here to handle both branches of this conditional + # * We need to annotate `discriminator` as list[list[str | int]] and not list[list[str]] due to the + # invariance of list, and because list[list[str | int]] is the type of the discriminator argument + # to tagged_union_schema below + # * See the docstring of pydantic_core.core_schema.tagged_union_schema for more details about how to + # interpret the value of the discriminator argument to tagged_union_schema. (The list[list[str]] here + # is the appropriate way to provide a list of fallback attributes to check for a discriminator value.) + discriminator: str | list[list[str | int]] = [[self.discriminator], [self._discriminator_alias]] + else: + discriminator = self.discriminator + return core_schema.tagged_union_schema( + choices=self._tagged_union_choices, + discriminator=discriminator, + custom_error_type=schema.get('custom_error_type'), + custom_error_message=schema.get('custom_error_message'), + custom_error_context=schema.get('custom_error_context'), + strict=False, + from_attributes=True, + ref=schema.get('ref'), + metadata=schema.get('metadata'), + serialization=schema.get('serialization'), + ) + + def _handle_choice(self, choice: core_schema.CoreSchema) -> None: + """This method handles the "middle" stage of recursion over the input schema. + Specifically, it is responsible for handling each choice of the outermost union + (and any "coalesced" choices obtained from inner unions). + + Here, "handling" entails: + * Coalescing nested unions and compatible tagged-unions + * Tracking the presence of 'none' and 'nullable' schemas occurring as choices + * Validating that each allowed discriminator value maps to a unique choice + * Updating the _tagged_union_choices mapping that will ultimately be used to build the TaggedUnionSchema. + """ + if choice['type'] == 'definition-ref': + if choice['schema_ref'] not in self.definitions: + raise MissingDefinitionForUnionRef(choice['schema_ref']) + + if choice['type'] == 'none': + self._should_be_nullable = True + elif choice['type'] == 'definitions': + self._handle_choice(choice['schema']) + elif choice['type'] == 'nullable': + self._should_be_nullable = True + self._handle_choice(choice['schema']) # unwrap the nullable schema + elif choice['type'] == 'union': + # Reverse the choices list before extending the stack so that they get handled in the order they occur + choices_schemas = [v[0] if isinstance(v, tuple) else v for v in choice['choices'][::-1]] + self._choices_to_handle.extend(choices_schemas) + elif choice['type'] not in { + 'model', + 'typed-dict', + 'tagged-union', + 'lax-or-strict', + 'dataclass', + 'dataclass-args', + 'definition-ref', + } and not _core_utils.is_function_with_inner_schema(choice): + # We should eventually handle 'definition-ref' as well + err_str = f'The core schema type {choice["type"]!r} is not a valid discriminated union variant.' + if choice['type'] == 'list': + err_str += ( + ' If you are making use of a list of union types, make sure the discriminator is applied to the ' + 'union type and not the list (e.g. `list[Annotated[ | , Field(discriminator=...)]]`).' + ) + raise TypeError(err_str) + else: + if choice['type'] == 'tagged-union' and self._is_discriminator_shared(choice): + # In this case, this inner tagged-union is compatible with the outer tagged-union, + # and its choices can be coalesced into the outer TaggedUnionSchema. + subchoices = [x for x in choice['choices'].values() if not isinstance(x, (str, int))] + # Reverse the choices list before extending the stack so that they get handled in the order they occur + self._choices_to_handle.extend(subchoices[::-1]) + return + + inferred_discriminator_values = self._infer_discriminator_values_for_choice(choice, source_name=None) + self._set_unique_choice_for_values(choice, inferred_discriminator_values) + + def _is_discriminator_shared(self, choice: core_schema.TaggedUnionSchema) -> bool: + """This method returns a boolean indicating whether the discriminator for the `choice` + is the same as that being used for the outermost tagged union. This is used to + determine whether this TaggedUnionSchema choice should be "coalesced" into the top level, + or whether it should be treated as a separate (nested) choice. + """ + inner_discriminator = choice['discriminator'] + return inner_discriminator == self.discriminator or ( + isinstance(inner_discriminator, list) + and (self.discriminator in inner_discriminator or [self.discriminator] in inner_discriminator) + ) + + def _infer_discriminator_values_for_choice( # noqa C901 + self, choice: core_schema.CoreSchema, source_name: str | None + ) -> list[str | int]: + """This function recurses over `choice`, extracting all discriminator values that should map to this choice. + + `model_name` is accepted for the purpose of producing useful error messages. + """ + if choice['type'] == 'definitions': + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=source_name) + + elif _core_utils.is_function_with_inner_schema(choice): + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=source_name) + + elif choice['type'] == 'lax-or-strict': + return sorted( + set( + self._infer_discriminator_values_for_choice(choice['lax_schema'], source_name=None) + + self._infer_discriminator_values_for_choice(choice['strict_schema'], source_name=None) + ) + ) + + elif choice['type'] == 'tagged-union': + values: list[str | int] = [] + # Ignore str/int "choices" since these are just references to other choices + subchoices = [x for x in choice['choices'].values() if not isinstance(x, (str, int))] + for subchoice in subchoices: + subchoice_values = self._infer_discriminator_values_for_choice(subchoice, source_name=None) + values.extend(subchoice_values) + return values + + elif choice['type'] == 'union': + values = [] + for subchoice in choice['choices']: + subchoice_schema = subchoice[0] if isinstance(subchoice, tuple) else subchoice + subchoice_values = self._infer_discriminator_values_for_choice(subchoice_schema, source_name=None) + values.extend(subchoice_values) + return values + + elif choice['type'] == 'nullable': + self._should_be_nullable = True + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=None) + + elif choice['type'] == 'model': + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=choice['cls'].__name__) + + elif choice['type'] == 'dataclass': + return self._infer_discriminator_values_for_choice(choice['schema'], source_name=choice['cls'].__name__) + + elif choice['type'] == 'model-fields': + return self._infer_discriminator_values_for_model_choice(choice, source_name=source_name) + + elif choice['type'] == 'dataclass-args': + return self._infer_discriminator_values_for_dataclass_choice(choice, source_name=source_name) + + elif choice['type'] == 'typed-dict': + return self._infer_discriminator_values_for_typed_dict_choice(choice, source_name=source_name) + + elif choice['type'] == 'definition-ref': + schema_ref = choice['schema_ref'] + if schema_ref not in self.definitions: + raise MissingDefinitionForUnionRef(schema_ref) + return self._infer_discriminator_values_for_choice(self.definitions[schema_ref], source_name=source_name) + else: + err_str = f'The core schema type {choice["type"]!r} is not a valid discriminated union variant.' + if choice['type'] == 'list': + err_str += ( + ' If you are making use of a list of union types, make sure the discriminator is applied to the ' + 'union type and not the list (e.g. `list[Annotated[ | , Field(discriminator=...)]]`).' + ) + raise TypeError(err_str) + + def _infer_discriminator_values_for_typed_dict_choice( + self, choice: core_schema.TypedDictSchema, source_name: str | None = None + ) -> list[str | int]: + """This method just extracts the _infer_discriminator_values_for_choice logic specific to TypedDictSchema + for the sake of readability. + """ + source = 'TypedDict' if source_name is None else f'TypedDict {source_name!r}' + field = choice['fields'].get(self.discriminator) + if field is None: + raise PydanticUserError( + f'{source} needs a discriminator field for key {self.discriminator!r}', code='discriminator-no-field' + ) + return self._infer_discriminator_values_for_field(field, source) + + def _infer_discriminator_values_for_model_choice( + self, choice: core_schema.ModelFieldsSchema, source_name: str | None = None + ) -> list[str | int]: + source = 'ModelFields' if source_name is None else f'Model {source_name!r}' + field = choice['fields'].get(self.discriminator) + if field is None: + raise PydanticUserError( + f'{source} needs a discriminator field for key {self.discriminator!r}', code='discriminator-no-field' + ) + return self._infer_discriminator_values_for_field(field, source) + + def _infer_discriminator_values_for_dataclass_choice( + self, choice: core_schema.DataclassArgsSchema, source_name: str | None = None + ) -> list[str | int]: + source = 'DataclassArgs' if source_name is None else f'Dataclass {source_name!r}' + for field in choice['fields']: + if field['name'] == self.discriminator: + break + else: + raise PydanticUserError( + f'{source} needs a discriminator field for key {self.discriminator!r}', code='discriminator-no-field' + ) + return self._infer_discriminator_values_for_field(field, source) + + def _infer_discriminator_values_for_field(self, field: CoreSchemaField, source: str) -> list[str | int]: + if field['type'] == 'computed-field': + # This should never occur as a discriminator, as it is only relevant to serialization + return [] + alias = field.get('validation_alias', self.discriminator) + if not isinstance(alias, str): + raise PydanticUserError( + f'Alias {alias!r} is not supported in a discriminated union', code='discriminator-alias-type' + ) + if self._discriminator_alias is None: + self._discriminator_alias = alias + elif self._discriminator_alias != alias: + raise PydanticUserError( + f'Aliases for discriminator {self.discriminator!r} must be the same ' + f'(got {alias}, {self._discriminator_alias})', + code='discriminator-alias', + ) + return self._infer_discriminator_values_for_inner_schema(field['schema'], source) + + def _infer_discriminator_values_for_inner_schema( + self, schema: core_schema.CoreSchema, source: str + ) -> list[str | int]: + """When inferring discriminator values for a field, we typically extract the expected values from a literal + schema. This function does that, but also handles nested unions and defaults. + """ + if schema['type'] == 'literal': + return schema['expected'] + + elif schema['type'] == 'union': + # Generally when multiple values are allowed they should be placed in a single `Literal`, but + # we add this case to handle the situation where a field is annotated as a `Union` of `Literal`s. + # For example, this lets us handle `Union[Literal['key'], Union[Literal['Key'], Literal['KEY']]]` + values: list[Any] = [] + for choice in schema['choices']: + choice_schema = choice[0] if isinstance(choice, tuple) else choice + choice_values = self._infer_discriminator_values_for_inner_schema(choice_schema, source) + values.extend(choice_values) + return values + + elif schema['type'] == 'default': + # This will happen if the field has a default value; we ignore it while extracting the discriminator values + return self._infer_discriminator_values_for_inner_schema(schema['schema'], source) + + elif schema['type'] == 'function-after': + # After validators don't affect the discriminator values + return self._infer_discriminator_values_for_inner_schema(schema['schema'], source) + + elif schema['type'] == 'model' and schema.get('root_model'): + # Support RootModel[Literal[...]] as discriminator field type + return self._infer_discriminator_values_for_inner_schema(schema['schema'], source) + + elif schema['type'] in {'function-before', 'function-wrap', 'function-plain'}: + validator_type = repr(schema['type'].split('-')[1]) + raise PydanticUserError( + f'Cannot use a mode={validator_type} validator in the' + f' discriminator field {self.discriminator!r} of {source}', + code='discriminator-validator', + ) + + else: + raise PydanticUserError( + f'{source} needs field {self.discriminator!r} to be of type `Literal`', + code='discriminator-needs-literal', + ) + + def _set_unique_choice_for_values(self, choice: core_schema.CoreSchema, values: Sequence[str | int]) -> None: + """This method updates `self.tagged_union_choices` so that all provided (discriminator) `values` map to the + provided `choice`, validating that none of these values already map to another (different) choice. + """ + for discriminator_value in values: + if discriminator_value in self._tagged_union_choices: + # It is okay if `value` is already in tagged_union_choices as long as it maps to the same value. + # Because tagged_union_choices may map values to other values, we need to walk the choices dict + # until we get to a "real" choice, and confirm that is equal to the one assigned. + existing_choice = self._tagged_union_choices[discriminator_value] + if existing_choice != choice: + raise TypeError( + f'Value {discriminator_value!r} for discriminator ' + f'{self.discriminator!r} mapped to multiple choices' + ) + else: + self._tagged_union_choices[discriminator_value] = choice diff --git a/venv/Lib/site-packages/pydantic/_internal/_docs_extraction.py b/venv/Lib/site-packages/pydantic/_internal/_docs_extraction.py new file mode 100644 index 0000000..6df77bf --- /dev/null +++ b/venv/Lib/site-packages/pydantic/_internal/_docs_extraction.py @@ -0,0 +1,113 @@ +"""Utilities related to attribute docstring extraction.""" + +from __future__ import annotations + +import ast +import inspect +import sys +import textwrap +from typing import Any + + +class DocstringVisitor(ast.NodeVisitor): + def __init__(self) -> None: + super().__init__() + + self.target: str | None = None + self.attrs: dict[str, str] = {} + self.previous_node_type: type[ast.AST] | None = None + + def visit(self, node: ast.AST) -> Any: + node_result = super().visit(node) + self.previous_node_type = type(node) + return node_result + + def visit_AnnAssign(self, node: ast.AnnAssign) -> Any: + if isinstance(node.target, ast.Name): + self.target = node.target.id + + def visit_Expr(self, node: ast.Expr) -> Any: + if ( + isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + and self.previous_node_type is ast.AnnAssign + ): + docstring = inspect.cleandoc(node.value.value) + if self.target: + self.attrs[self.target] = docstring + self.target = None + + +def _dedent_source_lines(source: list[str]) -> str: + # Required for nested class definitions, e.g. in a function block + dedent_source = textwrap.dedent(''.join(source)) + if dedent_source.startswith((' ', '\t')): + # We are in the case where there's a dedented (usually multiline) string + # at a lower indentation level than the class itself. We wrap our class + # in a function as a workaround. + dedent_source = f'def dedent_workaround():\n{dedent_source}' + return dedent_source + + +def _extract_source_from_frame(cls: type[Any]) -> list[str] | None: + frame = inspect.currentframe() + + while frame: + if inspect.getmodule(frame) is inspect.getmodule(cls): + lnum = frame.f_lineno + try: + lines, _ = inspect.findsource(frame) + except OSError: # pragma: no cover + # Source can't be retrieved (maybe because running in an interactive terminal), + # we don't want to error here. + pass + else: + block_lines = inspect.getblock(lines[lnum - 1 :]) + dedent_source = _dedent_source_lines(block_lines) + try: + block_tree = ast.parse(dedent_source) + except SyntaxError: + pass + else: + stmt = block_tree.body[0] + if isinstance(stmt, ast.FunctionDef) and stmt.name == 'dedent_workaround': + # `_dedent_source_lines` wrapped the class around the workaround function + stmt = stmt.body[0] + if isinstance(stmt, ast.ClassDef) and stmt.name == cls.__name__: + return block_lines + + frame = frame.f_back + + +def extract_docstrings_from_cls(cls: type[Any], use_inspect: bool = False) -> dict[str, str]: + """Map model attributes and their corresponding docstring. + + Args: + cls: The class of the Pydantic model to inspect. + use_inspect: Whether to skip usage of frames to find the object and use + the `inspect` module instead. + + Returns: + A mapping containing attribute names and their corresponding docstring. + """ + if use_inspect or sys.version_info >= (3, 13): + # On Python < 3.13, `inspect.getsourcelines()` might not work as expected + # if two classes have the same name in the same source file. + # On Python 3.13+, it will use the new `__firstlineno__` class attribute, + # making it way more robust. + try: + source, _ = inspect.getsourcelines(cls) + except OSError: # pragma: no cover + return {} + else: + # TODO remove this implementation when we drop support for Python 3.12: + source = _extract_source_from_frame(cls) + + if not source: + return {} + + dedent_source = _dedent_source_lines(source) + + visitor = DocstringVisitor() + visitor.visit(ast.parse(dedent_source)) + return visitor.attrs diff --git a/venv/Lib/site-packages/pydantic/_internal/_fields.py b/venv/Lib/site-packages/pydantic/_internal/_fields.py new file mode 100644 index 0000000..9a2a96b --- /dev/null +++ b/venv/Lib/site-packages/pydantic/_internal/_fields.py @@ -0,0 +1,729 @@ +"""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 +from re import Pattern +from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast + +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 ._internal_dataclass import slots_true +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__ = () + + +@dataclasses.dataclass(**slots_true) # TODO: make kw_only when we drop support for 3.9. +class PydanticExtraInfo: + # TODO: make use of PEP 747: + annotation: Any + complete: bool + + +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, + *, + typevars_map: Mapping[TypeVar, Any] | None = None, +) -> tuple[dict[str, FieldInfo], PydanticExtraInfo | None, 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 three-tuple containing the model fields, the `PydanticExtraInfo` instance if the `__pydantic_extra__` annotation is set, + 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) + + # `cls_annotations` is only used to determine if an annotation comes from a parent class + cls_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`" + ) + + for base in bases: + if hasattr(base, ann_name): + if ann_name not in cls_annotations: + # Don't warn when a field exists in a parent class but has not been defined in the current class + continue + + # 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') + if base is generic_origin: + # Don't warn when "shadowing" of attributes in parametrized generics + continue + + dataclass_fields = { + field.name for field in (dataclasses.fields(base) if dataclasses.is_dataclass(base) else ()) + } + 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 + + 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 cls_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 not 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) + field_info._original_annotation = ann_type + if not evaluated: + field_info._complete = False + # Store the original annotation that should be used to rebuild + # the field info later: + else: + # The field was present on one of the (possibly multiple) base classes, we make a copy directly from it. + parent_field_info = parent_fields_lookup[ann_name]._copy() + + # The only case where substituting the type variables is relevant (i.e. when `typevars_map` is not empty) + # is when a generic class is parameterized (e.g. `MyGenericModel[int, str]`), which creates a new class object + # (unlike the stdlib genercis that create a generic alias). In this case, we are guaranteed to only have to copy + # from the origin/parent model (e.g. `MyGenericModel`). + if typevars_map: + field_info = _recreate_field_info( + parent_field_info, ns_resolver=ns_resolver, typevars_map=typevars_map, lenient=True + ) + else: + field_info = parent_field_info + + 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 could be used to rebuild the field info later. + field_info._original_assignment = assigned_value + field_info._original_annotation = ann_type + if not evaluated: + field_info._complete = False + 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 config_wrapper.use_attribute_docstrings: + _update_fields_from_docstrings(cls, fields) + + pydantic_extra_info: PydanticExtraInfo | None = None + if '__pydantic_extra__' in type_hints: + ann, complete = type_hints['__pydantic_extra__'] + pydantic_extra_info = PydanticExtraInfo( + annotation=ann, + complete=complete, + ) + + return fields, pydantic_extra_info, class_vars + + +def rebuild_model_fields( + cls: type[BaseModel], + *, + config_wrapper: ConfigWrapper, + ns_resolver: NsResolver, + typevars_map: Mapping[TypeVar, Any], +) -> tuple[dict[str, FieldInfo], PydanticExtraInfo | None]: + """Rebuild the (already present) model fields by trying to reevaluate annotations. + + This function should be called whenever a model with incomplete fields is encountered. + + Returns: + A two-tuple, the first element being the rebuilt fields, the second element being + the rebuild `PydanticExtraInfo` instance, if available. + + 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. + """ + 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: + new_field = _recreate_field_info( + field_info, ns_resolver=ns_resolver, typevars_map=typevars_map, lenient=False + ) + update_field_from_config(config_wrapper, f_name, new_field) + rebuilt_fields[f_name] = new_field + + if cls.__pydantic_extra_info__ is not None and not cls.__pydantic_extra_info__.complete: + rebuilt_extra_info = PydanticExtraInfo( + annotation=_typing_extra.eval_type( + cls.__pydantic_extra_info__.annotation, *ns_resolver.types_namespace + ), + complete=True, + ) + else: + rebuilt_extra_info = cls.__pydantic_extra_info__ + + return rebuilt_fields, rebuilt_extra_info + + +def _recreate_field_info( + field_info: FieldInfo, + ns_resolver: NsResolver, + typevars_map: Mapping[TypeVar, Any], + *, + lenient: bool, +) -> FieldInfo: + FieldInfo_ = import_cached_field_info() + + existing_desc = field_info.description + if lenient: + ann = _generics.replace_types(field_info._original_annotation, typevars_map) + ann, evaluated = _typing_extra.try_eval_type( + ann, + *ns_resolver.types_namespace, + ) + else: + # Not the best pattern, maybe we could ship our own `eval_type()`, + # that would replace the type variables on the fly during evaluation. + ann = _typing_extra.eval_type( + field_info._original_annotation, + *ns_resolver.types_namespace, + ) + ann = _generics.replace_types(ann, typevars_map) + ann = _typing_extra.eval_type( + ann, + *ns_resolver.types_namespace, + ) + evaluated = True + + 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) + new_field._original_assignment = assign + new_field._original_annotation = ann + # 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 + if not evaluated: + new_field._complete = False + + return new_field + + +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 = _typing_extra.signature_no_eval(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 + + +def resolve_default_value( + default: Any, + default_factory: Callable[[], Any] | Callable[[dict[str, Any]], Any] | None, + *, + validated_data: dict[str, Any] | None = None, + call_default_factory: bool = False, +) -> Any: + """Resolve the default value using either a static default or a default_factory.""" + from ._utils import smart_deepcopy + + if default_factory is None: + return smart_deepcopy(default) + if call_default_factory: + if takes_validated_data_argument(default_factory=default_factory): + fac = cast('Callable[[dict[str, Any]], Any]', default_factory) + if validated_data is None: + raise ValueError( + "The default factory requires the 'validated_data' argument, which was not provided when calling 'get_default()'." + ) + return fac(validated_data) + else: + fac = cast('Callable[[], Any]', default_factory) + return fac() + + return PydanticUndefined