1"""Logic related to validators applied to models etc. via the `@field_validator` and `@model_validator` decorators."""
2
3from __future__ import annotations as _annotations
4
5import types
6from collections import deque
7from collections.abc import Callable, Iterable
8from copy import copy
9from dataclasses import dataclass, field
10from functools import cached_property, partial, partialmethod
11from inspect import Parameter, Signature, isdatadescriptor, ismethoddescriptor
12from itertools import islice
13from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeAlias, TypeVar
14
15from pydantic_core import PydanticUndefined, PydanticUndefinedType, core_schema
16from typing_extensions import Self, TypeForm, is_typeddict
17
18from ..errors import PydanticUserError
19from . import _type_refs
20from ._namespace_utils import GlobalsNamespace, MappingNamespace
21from ._typing_extra import get_function_type_hints, signature_no_eval
22from ._utils import can_be_positional
23
24if TYPE_CHECKING:
25 from ..fields import ComputedFieldInfo
26 from ..functional_validators import FieldValidatorModes
27 from ._config import ConfigWrapper
28
29
30@dataclass(slots=True)
31class ValidatorDecoratorInfo:
32 """A container for data from `@validator` so that we can access it
33 while building the pydantic-core schema.
34
35 Attributes:
36 decorator_repr: A class variable representing the decorator string, '@validator'.
37 fields: A tuple of field names the validator should be called on.
38 mode: The proposed validator mode.
39 each_item: For complex objects (sets, lists etc.) whether to validate individual
40 elements rather than the whole object.
41 always: Whether this method and other validators should be called even if the value is missing.
42 check_fields: Whether to check that the fields actually exist on the model.
43 """
44
45 decorator_repr: ClassVar[str] = '@validator'
46
47 fields: tuple[str, ...]
48 mode: Literal['before', 'after']
49 each_item: bool
50 always: bool
51 check_fields: bool | None
52
53
54@dataclass(slots=True)
55class FieldValidatorDecoratorInfo:
56 """A container for data from `@field_validator` so that we can access it
57 while building the pydantic-core schema.
58
59 Attributes:
60 decorator_repr: A class variable representing the decorator string, '@field_validator'.
61 fields: A tuple of field names the validator should be called on.
62 mode: The proposed validator mode.
63 check_fields: Whether to check that the fields actually exist on the model.
64 json_schema_input_type: The input type of the function. This is only used to generate
65 the appropriate JSON Schema (in validation mode) and can only specified
66 when `mode` is either `'before'`, `'plain'` or `'wrap'`.
67 """
68
69 decorator_repr: ClassVar[str] = '@field_validator'
70
71 fields: tuple[str, ...]
72 mode: FieldValidatorModes
73 check_fields: bool | None
74 json_schema_input_type: Any
75
76
77@dataclass(slots=True)
78class RootValidatorDecoratorInfo:
79 """A container for data from `@root_validator` so that we can access it
80 while building the pydantic-core schema.
81
82 Attributes:
83 decorator_repr: A class variable representing the decorator string, '@root_validator'.
84 mode: The proposed validator mode.
85 """
86
87 decorator_repr: ClassVar[str] = '@root_validator'
88 mode: Literal['before', 'after']
89
90
91@dataclass(slots=True)
92class FieldSerializerDecoratorInfo:
93 """A container for data from `@field_serializer` so that we can access it
94 while building the pydantic-core schema.
95
96 Attributes:
97 decorator_repr: A class variable representing the decorator string, '@field_serializer'.
98 fields: A tuple of field names the serializer should be called on.
99 mode: The proposed serializer mode.
100 return_type: The type of the serializer's return value.
101 when_used: The serialization condition. Accepts a string with values `'always'`, `'unless-none'`, `'json'`,
102 and `'json-unless-none'`.
103 check_fields: Whether to check that the fields actually exist on the model.
104 """
105
106 decorator_repr: ClassVar[str] = '@field_serializer'
107 fields: tuple[str, ...]
108 mode: Literal['plain', 'wrap']
109 return_type: TypeForm[Any]
110 when_used: core_schema.WhenUsed
111 check_fields: bool | None
112
113
114@dataclass(slots=True)
115class ModelSerializerDecoratorInfo:
116 """A container for data from `@model_serializer` so that we can access it
117 while building the pydantic-core schema.
118
119 Attributes:
120 decorator_repr: A class variable representing the decorator string, '@model_serializer'.
121 mode: The proposed serializer mode.
122 return_type: The type of the serializer's return value.
123 when_used: The serialization condition. Accepts a string with values `'always'`, `'unless-none'`, `'json'`,
124 and `'json-unless-none'`.
125 """
126
127 decorator_repr: ClassVar[str] = '@model_serializer'
128 mode: Literal['plain', 'wrap']
129 return_type: TypeForm[Any]
130 when_used: core_schema.WhenUsed
131
132
133@dataclass(slots=True)
134class ModelValidatorDecoratorInfo:
135 """A container for data from `@model_validator` so that we can access it
136 while building the pydantic-core schema.
137
138 Attributes:
139 decorator_repr: A class variable representing the decorator string, '@model_validator'.
140 mode: The proposed serializer mode.
141 """
142
143 decorator_repr: ClassVar[str] = '@model_validator'
144 mode: Literal['wrap', 'before', 'after']
145
146
147DecoratorInfo: TypeAlias = 'ValidatorDecoratorInfo | FieldValidatorDecoratorInfo | RootValidatorDecoratorInfo | FieldSerializerDecoratorInfo | ModelSerializerDecoratorInfo | ModelValidatorDecoratorInfo | ComputedFieldInfo'
148
149ReturnType = TypeVar('ReturnType')
150DecoratedType: TypeAlias = (
151 'classmethod[Any, Any, ReturnType] | staticmethod[Any, ReturnType] | Callable[..., ReturnType] | property'
152)
153
154
155@dataclass # can't use slots here since we set attributes on `__post_init__`
156class PydanticDescriptorProxy(Generic[ReturnType]):
157 """Wrap a classmethod, staticmethod, property or unbound function
158 and act as a descriptor that allows us to detect decorated items
159 from the class' attributes.
160
161 This class' __get__ returns the wrapped item's __get__ result,
162 which makes it transparent for classmethods and staticmethods.
163
164 Attributes:
165 wrapped: The decorator that has to be wrapped.
166 decorator_info: The decorator info.
167 shim: A wrapper function to wrap V1 style function.
168 """
169
170 wrapped: DecoratedType[ReturnType]
171 decorator_info: DecoratorInfo
172 shim: Callable[[Callable[..., Any]], Callable[..., Any]] | None = None
173
174 def __post_init__(self):
175 for attr in 'setter', 'deleter':
176 if hasattr(self.wrapped, attr):
177 f = partial(self._call_wrapped_attr, name=attr)
178 setattr(self, attr, f)
179
180 def _call_wrapped_attr(self, func: Callable[[Any], None], *, name: str) -> PydanticDescriptorProxy[ReturnType]:
181 self.wrapped = getattr(self.wrapped, name)(func)
182 if isinstance(self.wrapped, property):
183 # update ComputedFieldInfo.wrapped_property
184 from ..fields import ComputedFieldInfo
185
186 if isinstance(self.decorator_info, ComputedFieldInfo):
187 self.decorator_info.wrapped_property = self.wrapped
188 return self
189
190 def __get__(self, obj: object | None, obj_type: type[object] | None = None) -> PydanticDescriptorProxy[ReturnType]:
191 try:
192 return self.wrapped.__get__(obj, obj_type) # pyright: ignore[reportReturnType]
193 except AttributeError:
194 # not a descriptor, e.g. a partial object
195 return self.wrapped # type: ignore[return-value]
196
197 def __set_name__(self, instance: Any, name: str) -> None:
198 if hasattr(self.wrapped, '__set_name__'):
199 self.wrapped.__set_name__(instance, name) # pyright: ignore[reportFunctionMemberAccess]
200
201 def __getattr__(self, name: str, /) -> Any:
202 """Forward checks for __isabstractmethod__ and such."""
203 return getattr(self.wrapped, name)
204
205
206DecoratorInfoType = TypeVar('DecoratorInfoType', bound=DecoratorInfo)
207
208
209@dataclass(slots=True)
210class Decorator(Generic[DecoratorInfoType]):
211 """A generic container class to join together the decorator metadata
212 (metadata from decorator itself, which we have when the
213 decorator is called but not when we are building the core-schema)
214 and the bound function (which we have after the class itself is created).
215
216 Attributes:
217 cls_ref: The class ref.
218 cls_var_name: The decorated function name.
219 func: The decorated function.
220 shim: A wrapper function to wrap V1 style function.
221 info: The decorator info.
222 """
223
224 cls_ref: str
225 cls_var_name: str
226 func: Callable[..., Any]
227 shim: Callable[[Any], Any] | None
228 info: DecoratorInfoType
229
230 @staticmethod
231 def build(
232 cls_: Any,
233 *,
234 cls_var_name: str,
235 cls_ref: str | None = None,
236 shim: Callable[[Any], Any] | None,
237 info: DecoratorInfoType,
238 ) -> Decorator[DecoratorInfoType]:
239 """Build a new decorator.
240
241 Args:
242 cls_: The class.
243 cls_ref: The type ref of the class.
244 cls_var_name: The decorated function name.
245 shim: A wrapper function to wrap V1 style function.
246 info: The decorator info.
247
248 Returns:
249 The new decorator instance.
250 """
251 # For backwards compatibility for users using this (private!) method. TODO v3: make `cls_ref` required:
252 cls_ref = cls_ref if cls_ref is not None else _type_refs.any_class_type_ref(cls_)
253
254 func = get_attribute_from_bases(cls_, cls_var_name)
255 if shim is not None:
256 func = shim(func)
257 func = unwrap_wrapped_function(func, unwrap_partial=False)
258 if not callable(func):
259 # TODO most likely this branch can be removed when we drop support for Python 3.12:
260 # This branch will get hit for classmethod properties
261 attribute = get_attribute_from_base_dicts(cls_, cls_var_name) # prevents the binding call to `__get__`
262 if isinstance(attribute, PydanticDescriptorProxy):
263 func = unwrap_wrapped_function(attribute.wrapped)
264 return Decorator(
265 cls_ref=cls_ref,
266 cls_var_name=cls_var_name,
267 func=func,
268 shim=shim,
269 info=info,
270 )
271
272 def bind_to_cls(self, cls: Any, cls_ref: str) -> Decorator[DecoratorInfoType]:
273 """Bind the decorator to a class.
274
275 Args:
276 cls: the class.
277 cls_ref: The type ref of the class.
278
279 Returns:
280 The new decorator instance.
281 """
282 return self.build(
283 cls,
284 cls_ref=cls_ref,
285 cls_var_name=self.cls_var_name,
286 shim=self.shim,
287 info=copy(self.info),
288 )
289
290
291def get_bases(tp: type[Any]) -> tuple[type[Any], ...]:
292 """Get the base classes of a class or typeddict.
293
294 Args:
295 tp: The type or class to get the bases.
296
297 Returns:
298 The base classes.
299 """
300 if is_typeddict(tp):
301 return tp.__orig_bases__ # type: ignore
302 try:
303 return tp.__bases__
304 except AttributeError:
305 return ()
306
307
308def mro(tp: type[Any]) -> tuple[type[Any], ...]:
309 """Calculate the Method Resolution Order of bases using the C3 algorithm.
310
311 See https://www.python.org/download/releases/2.3/mro/
312 """
313 # try to use the existing mro, for performance mainly
314 # but also because it helps verify the implementation below
315 if not is_typeddict(tp):
316 try:
317 return tp.__mro__
318 except AttributeError:
319 # GenericAlias and some other cases
320 pass
321
322 bases = get_bases(tp)
323 return (tp,) + mro_for_bases(bases)
324
325
326def mro_for_bases(bases: tuple[type[Any], ...]) -> tuple[type[Any], ...]:
327 def merge_seqs(seqs: list[deque[type[Any]]]) -> Iterable[type[Any]]:
328 while True:
329 non_empty = [seq for seq in seqs if seq]
330 if not non_empty:
331 # Nothing left to process, we're done.
332 return
333 candidate: type[Any] | None = None
334 for seq in non_empty: # Find merge candidates among seq heads.
335 candidate = seq[0]
336 not_head = [s for s in non_empty if candidate in islice(s, 1, None)]
337 if not_head:
338 # Reject the candidate.
339 candidate = None
340 else:
341 break
342 if not candidate:
343 raise TypeError('Inconsistent hierarchy, no C3 MRO is possible')
344 yield candidate
345 for seq in non_empty:
346 # Remove candidate.
347 if seq[0] == candidate:
348 seq.popleft()
349
350 seqs = [deque(mro(base)) for base in bases] + [deque(bases)]
351 return tuple(merge_seqs(seqs))
352
353
354_sentinel = object()
355
356
357def get_attribute_from_bases(tp: type[Any] | tuple[type[Any], ...], name: str) -> Any:
358 """Get the attribute from the next class in the MRO that has it,
359 aiming to simulate calling the method on the actual class.
360
361 The reason for iterating over the mro instead of just getting
362 the attribute (which would do that for us) is to support TypedDict,
363 which lacks a real __mro__, but can have a virtual one constructed
364 from its bases (as done here).
365
366 Args:
367 tp: The type or class to search for the attribute. If a tuple, this is treated as a set of base classes.
368 name: The name of the attribute to retrieve.
369
370 Returns:
371 Any: The attribute value, if found.
372
373 Raises:
374 AttributeError: If the attribute is not found in any class in the MRO.
375 """
376 if isinstance(tp, tuple):
377 for base in mro_for_bases(tp):
378 attribute = base.__dict__.get(name, _sentinel)
379 if attribute is not _sentinel:
380 attribute_get = getattr(attribute, '__get__', None)
381 if attribute_get is not None:
382 return attribute_get(None, tp)
383 return attribute
384 raise AttributeError(f'{name} not found in {tp}')
385 else:
386 try:
387 return getattr(tp, name)
388 except AttributeError:
389 return get_attribute_from_bases(mro(tp), name)
390
391
392def get_attribute_from_base_dicts(tp: type[Any], name: str) -> Any:
393 """Get an attribute out of the `__dict__` following the MRO.
394 This prevents the call to `__get__` on the descriptor, and allows
395 us to get the original function for classmethod properties.
396
397 Args:
398 tp: The type or class to search for the attribute.
399 name: The name of the attribute to retrieve.
400
401 Returns:
402 Any: The attribute value, if found.
403
404 Raises:
405 KeyError: If the attribute is not found in any class's `__dict__` in the MRO.
406 """
407 for base in reversed(mro(tp)):
408 if name in base.__dict__:
409 return base.__dict__[name]
410 return tp.__dict__[name] # raise the error
411
412
413@dataclass(slots=True)
414class DecoratorInfos:
415 """Mapping of name in the class namespace to decorator info.
416
417 note that the name in the class namespace is the function or attribute name
418 not the field name!
419 """
420
421 validators: dict[str, Decorator[ValidatorDecoratorInfo]] = field(default_factory=dict)
422 field_validators: dict[str, Decorator[FieldValidatorDecoratorInfo]] = field(default_factory=dict)
423 root_validators: dict[str, Decorator[RootValidatorDecoratorInfo]] = field(default_factory=dict)
424 field_serializers: dict[str, Decorator[FieldSerializerDecoratorInfo]] = field(default_factory=dict)
425 model_serializers: dict[str, Decorator[ModelSerializerDecoratorInfo]] = field(default_factory=dict)
426 model_validators: dict[str, Decorator[ModelValidatorDecoratorInfo]] = field(default_factory=dict)
427 computed_fields: dict[str, Decorator[ComputedFieldInfo]] = field(default_factory=dict)
428
429 @classmethod
430 def build(
431 cls,
432 typ: type[Any],
433 # Default to `True` for backwards compatibility:
434 replace_wrapped_methods: bool = True,
435 ) -> Self:
436 """Build a `DecoratorInfos` instance for the given model, dataclass or `TypedDict` type.
437
438 Decorators from parent classes are included, including "bare" classes (e.g. if `typ`
439 is a Pydantic model, non Pydantic parent model classes are also taken into account).
440 The collection of the decorators happens by respecting the MRO.
441
442 If one of the bases has an `__pydantic_decorators__` attribute set, it is assumed to be
443 a `DecoratorInfos` instance and is used as-is. The `__pydantic_decorators__` attribute
444 is *not* being set on the provided `typ`.
445
446 Args:
447 typ: The model, dataclass or `TypedDict` type to use when building the `DecoratorInfos` instance.
448 replace_wrapped_methods: Whether to replace the decorator's wrapped methods on `typ`.
449 This is useful e.g. for field validators which are initially class methods. This should
450 only be set to `True` if `typ` is a Pydantic model or dataclass (otherwise this results
451 in mutations of classes Pydantic doesn't "own").
452 """
453 # reminder: dicts are ordered and replacement does not alter the order
454 res = cls()
455
456 type_ref: str | None = None
457
458 def cls_ref() -> str:
459 # The type ref is only required if at least one decorator is collected,
460 # so only compute it lazily (and only once):
461 nonlocal type_ref
462 if type_ref is None:
463 type_ref = _type_refs.any_class_type_ref(typ)
464 return type_ref
465
466 # Iterate over the bases, without the actual `typ`.
467 # `1:-1` because we don't need to include `object`/`TypedDict`:
468 for base in reversed(mro(typ)[1:-1]):
469 existing: DecoratorInfos | None = base.__dict__.get('__pydantic_decorators__')
470 if existing is None:
471 existing, _ = _decorator_infos_for_class(
472 base,
473 # The class refs of the built decorators are irrelevant here, as `bind_to_cls()`
474 # below rebinds every decorator to `typ` with the correct ref:
475 cls_ref=lambda: '',
476 collect_to_replace=False,
477 )
478 res.validators.update({k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.validators.items()})
479 res.field_validators.update(
480 {k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.field_validators.items()}
481 )
482 res.root_validators.update({k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.root_validators.items()})
483 res.field_serializers.update(
484 {k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.field_serializers.items()}
485 )
486 res.model_serializers.update(
487 {k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.model_serializers.items()}
488 )
489 res.model_validators.update(
490 {k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.model_validators.items()}
491 )
492 res.computed_fields.update({k: v.bind_to_cls(typ, cls_ref()) for k, v in existing.computed_fields.items()})
493
494 decorator_infos, to_replace = _decorator_infos_for_class(typ, cls_ref=cls_ref, collect_to_replace=True)
495
496 res.validators.update(decorator_infos.validators)
497 res.field_validators.update(decorator_infos.field_validators)
498 res.root_validators.update(decorator_infos.root_validators)
499 res.field_serializers.update(decorator_infos.field_serializers)
500 res.model_serializers.update(decorator_infos.model_serializers)
501 res.model_validators.update(decorator_infos.model_validators)
502 res.computed_fields.update(decorator_infos.computed_fields)
503
504 if replace_wrapped_methods and to_replace:
505 for name, value in to_replace:
506 setattr(typ, name, value)
507
508 res._validate()
509 return res
510
511 def _validate(self) -> None:
512 seen: set[str] = set()
513 for field_ser in self.field_serializers.values():
514 for f_name in field_ser.info.fields:
515 if f_name in seen:
516 raise PydanticUserError(
517 f'Multiple field serializer functions were defined for field {f_name!r}, this is not allowed.',
518 code='multiple-field-serializers',
519 )
520 seen.add(f_name)
521
522 def update_from_config(self, config_wrapper: ConfigWrapper) -> None:
523 """Update the decorator infos from the configuration of the class they are attached to."""
524 for name, computed_field_dec in self.computed_fields.items():
525 computed_field_dec.info._update_from_config(config_wrapper, name)
526
527
528def _decorator_infos_for_class(
529 typ: type[Any],
530 *,
531 cls_ref: Callable[[], str],
532 collect_to_replace: bool,
533) -> tuple[DecoratorInfos, list[tuple[str, Any]]]:
534 """Collect a `DecoratorInfos` for class, without looking into bases."""
535 res = DecoratorInfos()
536 to_replace: list[tuple[str, Any]] = []
537
538 # Iterate on the vars with a copy to avoid changes in dictionary size during iteration.
539 # This can happen if a concurrent thread has Pydantic processing the same `typ` (e.g. two
540 # concurrent threads doing `TypeAdapter(SomeClassType)`), and such thread reads annotations
541 # for the first time during the iteration here in the other thread (since 3.14, reading annotations
542 # can set attributes such as `__annotations_cache__` or `__annotate_func__` to the type).
543 for var_name, var_value in vars(typ).copy().items():
544 if isinstance(var_value, PydanticDescriptorProxy):
545 info = var_value.decorator_info
546 if isinstance(info, ValidatorDecoratorInfo):
547 res.validators[var_name] = Decorator.build(
548 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=var_value.shim, info=info
549 )
550 elif isinstance(info, FieldValidatorDecoratorInfo):
551 res.field_validators[var_name] = Decorator.build(
552 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=var_value.shim, info=info
553 )
554 elif isinstance(info, RootValidatorDecoratorInfo):
555 res.root_validators[var_name] = Decorator.build(
556 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=var_value.shim, info=info
557 )
558 elif isinstance(info, FieldSerializerDecoratorInfo):
559 res.field_serializers[var_name] = Decorator.build(
560 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=var_value.shim, info=info
561 )
562 elif isinstance(info, ModelValidatorDecoratorInfo):
563 res.model_validators[var_name] = Decorator.build(
564 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=var_value.shim, info=info
565 )
566 elif isinstance(info, ModelSerializerDecoratorInfo):
567 res.model_serializers[var_name] = Decorator.build(
568 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=var_value.shim, info=info
569 )
570 else:
571 from ..fields import ComputedFieldInfo
572
573 assert isinstance(info, ComputedFieldInfo)
574 res.computed_fields[var_name] = Decorator.build(
575 typ, cls_ref=cls_ref(), cls_var_name=var_name, shim=None, info=info
576 )
577 if collect_to_replace:
578 to_replace.append((var_name, var_value.wrapped))
579
580 return res, to_replace
581
582
583def inspect_validator(
584 validator: Callable[..., Any], *, mode: FieldValidatorModes, type: Literal['field', 'model']
585) -> bool:
586 """Look at a field or model validator function and determine whether it takes an info argument.
587
588 An error is raised if the function has an invalid signature.
589
590 Args:
591 validator: The validator function to inspect.
592 mode: The proposed validator mode.
593 type: The type of validator, either 'field' or 'model'.
594
595 Returns:
596 Whether the validator takes an info argument.
597 """
598 try:
599 sig = signature_no_eval(validator)
600 except (ValueError, TypeError):
601 # `inspect.signature` might not be able to infer a signature, e.g. with C objects.
602 # In this case, we assume no info argument is present:
603 return False
604 n_positional = count_positional_required_params(sig)
605 if mode == 'wrap':
606 if n_positional == 3:
607 return True
608 elif n_positional == 2:
609 return False
610 else:
611 assert mode in {'before', 'after', 'plain'}, f"invalid mode: {mode!r}, expected 'before', 'after' or 'plain"
612 if n_positional == 2:
613 return True
614 elif n_positional == 1:
615 return False
616
617 raise PydanticUserError(
618 f'Unrecognized {type} validator function signature for {validator} with `mode={mode}`: {sig}',
619 code='validator-signature',
620 )
621
622
623def inspect_field_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> tuple[bool, bool]:
624 """Look at a field serializer function and determine if it is a field serializer,
625 and whether it takes an info argument.
626
627 An error is raised if the function has an invalid signature.
628
629 Args:
630 serializer: The serializer function to inspect.
631 mode: The serializer mode, either 'plain' or 'wrap'.
632
633 Returns:
634 Tuple of (is_field_serializer, info_arg).
635 """
636 try:
637 sig = signature_no_eval(serializer)
638 except (ValueError, TypeError):
639 # `inspect.signature` might not be able to infer a signature, e.g. with C objects.
640 # In this case, we assume no info argument is present and this is not a method:
641 return (False, False)
642
643 first = next(iter(sig.parameters.values()), None)
644 is_field_serializer = first is not None and first.name == 'self'
645
646 n_positional = count_positional_required_params(sig)
647 if is_field_serializer:
648 # -1 to correct for self parameter
649 info_arg = _serializer_info_arg(mode, n_positional - 1)
650 else:
651 info_arg = _serializer_info_arg(mode, n_positional)
652
653 if info_arg is None:
654 raise PydanticUserError(
655 f'Unrecognized field_serializer function signature for {serializer} with `mode={mode}`:{sig}',
656 code='field-serializer-signature',
657 )
658
659 return is_field_serializer, info_arg
660
661
662def inspect_annotated_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> bool:
663 """Look at a serializer function used via `Annotated` and determine whether it takes an info argument.
664
665 An error is raised if the function has an invalid signature.
666
667 Args:
668 serializer: The serializer function to check.
669 mode: The serializer mode, either 'plain' or 'wrap'.
670
671 Returns:
672 info_arg
673 """
674 try:
675 sig = signature_no_eval(serializer)
676 except (ValueError, TypeError):
677 # `inspect.signature` might not be able to infer a signature, e.g. with C objects.
678 # In this case, we assume no info argument is present:
679 return False
680 info_arg = _serializer_info_arg(mode, count_positional_required_params(sig))
681 if info_arg is None:
682 raise PydanticUserError(
683 f'Unrecognized field_serializer function signature for {serializer} with `mode={mode}`:{sig}',
684 code='field-serializer-signature',
685 )
686 else:
687 return info_arg
688
689
690def inspect_model_serializer(serializer: Callable[..., Any], mode: Literal['plain', 'wrap']) -> bool:
691 """Look at a model serializer function and determine whether it takes an info argument.
692
693 An error is raised if the function has an invalid signature.
694
695 Args:
696 serializer: The serializer function to check.
697 mode: The serializer mode, either 'plain' or 'wrap'.
698
699 Returns:
700 `info_arg` - whether the function expects an info argument.
701 """
702 if isinstance(serializer, (staticmethod, classmethod)) or not is_instance_method_from_sig(serializer):
703 raise PydanticUserError(
704 '`@model_serializer` must be applied to instance methods', code='model-serializer-instance-method'
705 )
706
707 sig = signature_no_eval(serializer)
708 info_arg = _serializer_info_arg(mode, count_positional_required_params(sig))
709 if info_arg is None:
710 raise PydanticUserError(
711 f'Unrecognized model_serializer function signature for {serializer} with `mode={mode}`:{sig}',
712 code='model-serializer-signature',
713 )
714 else:
715 return info_arg
716
717
718def _serializer_info_arg(mode: Literal['plain', 'wrap'], n_positional: int) -> bool | None:
719 if mode == 'plain':
720 if n_positional == 1:
721 # (input_value: Any, /) -> Any
722 return False
723 elif n_positional == 2:
724 # (model: Any, input_value: Any, /) -> Any
725 return True
726 else:
727 assert mode == 'wrap', f"invalid mode: {mode!r}, expected 'plain' or 'wrap'"
728 if n_positional == 2:
729 # (input_value: Any, serializer: SerializerFunctionWrapHandler, /) -> Any
730 return False
731 elif n_positional == 3:
732 # (input_value: Any, serializer: SerializerFunctionWrapHandler, info: SerializationInfo, /) -> Any
733 return True
734
735 return None
736
737
738AnyDecoratorCallable: TypeAlias = (
739 'classmethod[Any, Any, Any] | staticmethod[Any, Any] | partialmethod[Any] | Callable[..., Any]'
740)
741
742
743def is_instance_method_from_sig(function: AnyDecoratorCallable) -> bool:
744 """Whether the function is an instance method.
745
746 It will consider a function as instance method if the first parameter of
747 function is `self`.
748
749 Args:
750 function: The function to check.
751
752 Returns:
753 `True` if the function is an instance method, `False` otherwise.
754 """
755 sig = signature_no_eval(unwrap_wrapped_function(function))
756 first = next(iter(sig.parameters.values()), None)
757 if first and first.name == 'self':
758 return True
759 return False
760
761
762def ensure_classmethod_based_on_signature(function: AnyDecoratorCallable) -> Any:
763 """Apply the `@classmethod` decorator on the function.
764
765 Args:
766 function: The function to apply the decorator on.
767
768 Return:
769 The `@classmethod` decorator applied function.
770 """
771 if not isinstance(
772 unwrap_wrapped_function(function, unwrap_class_static_method=False), classmethod
773 ) and _is_classmethod_from_sig(function):
774 return classmethod(function) # type: ignore[arg-type]
775 return function
776
777
778def _is_classmethod_from_sig(function: AnyDecoratorCallable) -> bool:
779 sig = signature_no_eval(unwrap_wrapped_function(function))
780 first = next(iter(sig.parameters.values()), None)
781 if first and first.name == 'cls':
782 return True
783 return False
784
785
786def unwrap_wrapped_function(
787 func: Any,
788 *,
789 unwrap_partial: bool = True,
790 unwrap_class_static_method: bool = True,
791) -> Any:
792 """Recursively unwraps a wrapped function until the underlying function is reached.
793 This handles property, functools.partial, functools.partialmethod, staticmethod, and classmethod.
794
795 Args:
796 func: The function to unwrap.
797 unwrap_partial: If True (default), unwrap partial and partialmethod decorators.
798 unwrap_class_static_method: If True (default), also unwrap classmethod and staticmethod
799 decorators. If False, only unwrap partial and partialmethod decorators.
800
801 Returns:
802 The underlying function of the wrapped function.
803 """
804 # Define the types we want to check against as a single tuple.
805 unwrap_types = (
806 (property, cached_property)
807 + ((partial, partialmethod) if unwrap_partial else ())
808 + ((staticmethod, classmethod) if unwrap_class_static_method else ())
809 )
810
811 while isinstance(func, unwrap_types):
812 if unwrap_class_static_method and isinstance(func, (classmethod, staticmethod)):
813 func = func.__func__
814 elif isinstance(func, (partial, partialmethod)):
815 func = func.func
816 elif isinstance(func, property):
817 func = func.fget # arbitrary choice, convenient for computed fields
818 else:
819 # Make coverage happy as it can only get here in the last possible case
820 assert isinstance(func, cached_property)
821 func = func.func # type: ignore
822
823 return func
824
825
826_function_like = (
827 partial,
828 partialmethod,
829 types.FunctionType,
830 types.BuiltinFunctionType,
831 types.MethodType,
832 types.WrapperDescriptorType,
833 types.MethodWrapperType,
834 types.MemberDescriptorType,
835)
836
837
838def get_callable_return_type(
839 callable_obj: Any,
840 globalns: GlobalsNamespace | None = None,
841 localns: MappingNamespace | None = None,
842) -> Any | PydanticUndefinedType:
843 """Get the callable return type.
844
845 Args:
846 callable_obj: The callable to analyze.
847 globalns: The globals namespace to use during type annotation evaluation.
848 localns: The locals namespace to use during type annotation evaluation.
849
850 Returns:
851 The function return type.
852 """
853 if isinstance(callable_obj, type):
854 # types are callables, and we assume the return type
855 # is the type itself (e.g. `int()` results in an instance of `int`).
856 return callable_obj
857
858 if not isinstance(callable_obj, _function_like):
859 call_func = getattr(type(callable_obj), '__call__', None) # noqa: B004
860 if call_func is not None:
861 callable_obj = call_func
862
863 hints = get_function_type_hints(
864 unwrap_wrapped_function(callable_obj),
865 include_keys={'return'},
866 globalns=globalns,
867 localns=localns,
868 )
869 return hints.get('return', PydanticUndefined)
870
871
872def count_positional_required_params(sig: Signature) -> int:
873 """Get the number of positional (required) arguments of a signature.
874
875 This function should only be used to inspect signatures of validation and serialization functions.
876 The first argument (the value being serialized or validated) is counted as a required argument
877 even if a default value exists.
878
879 Returns:
880 The number of positional arguments of a signature.
881 """
882 parameters = list(sig.parameters.values())
883 return sum(
884 1
885 for param in parameters
886 if can_be_positional(param)
887 # First argument is the value being validated/serialized, and can have a default value
888 # (e.g. `float`, which has signature `(x=0, /)`). We assume other parameters (the info arg
889 # for instance) should be required, and thus without any default value.
890 and (param.default is Parameter.empty or param is parameters[0])
891 )
892
893
894def ensure_property(f: Any) -> Any:
895 """Ensure that a function is a `property` or `cached_property`, or is a valid descriptor.
896
897 Args:
898 f: The function to check.
899
900 Returns:
901 The function, or a `property` or `cached_property` instance wrapping the function.
902 """
903 if ismethoddescriptor(f) or isdatadescriptor(f):
904 return f
905 else:
906 return property(f)