Загрузить файлы в «venv/Lib/site-packages/pydantic/_internal»

This commit is contained in:
2026-07-02 20:00:46 +00:00
parent 32e458e67f
commit 8c29bd456b
5 changed files with 2383 additions and 0 deletions

View 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)

View 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

View File

@@ -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

View 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

View 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