Загрузить файлы в «venv/Lib/site-packages/pydantic/_internal»
This commit is contained in:
873
venv/Lib/site-packages/pydantic/_internal/_decorators.py
Normal file
873
venv/Lib/site-packages/pydantic/_internal/_decorators.py
Normal file
@@ -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)
|
||||||
174
venv/Lib/site-packages/pydantic/_internal/_decorators_v1.py
Normal file
174
venv/Lib/site-packages/pydantic/_internal/_decorators_v1.py
Normal file
@@ -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
|
||||||
@@ -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[<T> | <U>, 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[<T> | <U>, 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
|
||||||
113
venv/Lib/site-packages/pydantic/_internal/_docs_extraction.py
Normal file
113
venv/Lib/site-packages/pydantic/_internal/_docs_extraction.py
Normal file
@@ -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
|
||||||
729
venv/Lib/site-packages/pydantic/_internal/_fields.py
Normal file
729
venv/Lib/site-packages/pydantic/_internal/_fields.py
Normal file
@@ -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[<type>] = <default>.`',
|
||||||
|
category=PydanticDeprecatedSince211,
|
||||||
|
# Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case:
|
||||||
|
stacklevel=4,
|
||||||
|
)
|
||||||
|
class_vars.add(ann_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# attributes which are fields are removed from the class namespace:
|
||||||
|
# 1. To match the behaviour of annotation-only fields
|
||||||
|
# 2. To avoid false positives in the NameError check above
|
||||||
|
try:
|
||||||
|
delattr(cls, ann_name)
|
||||||
|
except AttributeError:
|
||||||
|
pass # indicates the attribute was on a parent class
|
||||||
|
|
||||||
|
# Use cls.__dict__['__pydantic_decorators__'] instead of cls.__pydantic_decorators__
|
||||||
|
# to make sure the decorators have already been built for this exact class
|
||||||
|
decorators: DecoratorInfos = cls.__dict__['__pydantic_decorators__']
|
||||||
|
if ann_name in decorators.computed_fields:
|
||||||
|
raise TypeError(
|
||||||
|
f'Field {ann_name!r} of class {cls.__name__!r} overrides symbol of same name in a parent class. '
|
||||||
|
'This override with a computed_field is incompatible.'
|
||||||
|
)
|
||||||
|
fields[ann_name] = field_info
|
||||||
|
|
||||||
|
if 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
|
||||||
Reference in New Issue
Block a user