mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
425fac6343
* Automatically add no-cover to TYPE_CHECKING blocks * Add changes file
307 lines
12 KiB
Python
307 lines
12 KiB
Python
import warnings
|
|
from collections import ChainMap
|
|
from functools import wraps
|
|
from inspect import Signature, signature
|
|
from itertools import chain
|
|
from types import FunctionType
|
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, Union
|
|
|
|
from .errors import ConfigError
|
|
from .typing import AnyCallable
|
|
from .utils import in_ipython
|
|
|
|
|
|
class Validator:
|
|
__slots__ = 'func', 'pre', 'each_item', 'always', 'check_fields'
|
|
|
|
def __init__(
|
|
self,
|
|
func: AnyCallable,
|
|
pre: bool = False,
|
|
each_item: bool = False,
|
|
always: bool = False,
|
|
check_fields: bool = False,
|
|
):
|
|
self.func = func
|
|
self.pre = pre
|
|
self.each_item = each_item
|
|
self.always = always
|
|
self.check_fields = check_fields
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from .main import BaseConfig
|
|
from .fields import ModelField
|
|
from .types import ModelOrDc
|
|
|
|
ValidatorCallable = Callable[[Optional[ModelOrDc], Any, Dict[str, Any], ModelField, Type[BaseConfig]], Any]
|
|
ValidatorsList = List[ValidatorCallable]
|
|
ValidatorListDict = Dict[str, List[Validator]]
|
|
|
|
_FUNCS: Set[str] = set()
|
|
ROOT_KEY = '__root__'
|
|
VALIDATOR_CONFIG_KEY = '__validator_config__'
|
|
ROOT_VALIDATOR_CONFIG_KEY = '__root_validator_config__'
|
|
|
|
|
|
def validator(
|
|
*fields: str,
|
|
pre: bool = False,
|
|
each_item: bool = False,
|
|
always: bool = False,
|
|
check_fields: bool = True,
|
|
whole: bool = None,
|
|
) -> Callable[[AnyCallable], classmethod]:
|
|
"""
|
|
Decorate methods on the class indicating that they should be used to validate fields
|
|
:param fields: which field(s) the method should be called on
|
|
:param pre: whether or not this validator should be called before the standard validators (else after)
|
|
:param each_item: for complex objects (sets, lists etc.) whether to validate individual elements rather than the
|
|
whole object
|
|
:param always: whether this method and other validators should be called even if the value is missing
|
|
:param check_fields: whether to check that the fields actually exist on the model
|
|
"""
|
|
if not fields:
|
|
raise ConfigError('validator with no fields specified')
|
|
elif isinstance(fields[0], FunctionType):
|
|
raise ConfigError(
|
|
"validators should be used with fields and keyword arguments, not bare. " # noqa: Q000
|
|
"E.g. usage should be `@validator('<field_name>', ...)`"
|
|
)
|
|
|
|
if whole is not None:
|
|
warnings.warn(
|
|
'The "whole" keyword argument is deprecated, use "each_item" (inverse meaning, default False) instead',
|
|
DeprecationWarning,
|
|
)
|
|
assert each_item is False, '"each_item" and "whole" conflict, remove "whole"'
|
|
each_item = not whole
|
|
|
|
def dec(f: AnyCallable) -> classmethod:
|
|
_check_validator_name(f)
|
|
f_cls = classmethod(f)
|
|
setattr(
|
|
f_cls,
|
|
VALIDATOR_CONFIG_KEY,
|
|
(fields, Validator(func=f, pre=pre, each_item=each_item, always=always, check_fields=check_fields)),
|
|
)
|
|
return f_cls
|
|
|
|
return dec
|
|
|
|
|
|
def root_validator(
|
|
_func: Optional[AnyCallable] = None, *, pre: bool = False
|
|
) -> Union[classmethod, Callable[[AnyCallable], classmethod]]:
|
|
if _func:
|
|
_check_validator_name(_func)
|
|
f_cls = classmethod(_func)
|
|
setattr(f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=_func, pre=pre))
|
|
return f_cls
|
|
|
|
def dec(f: AnyCallable) -> classmethod:
|
|
_check_validator_name(f)
|
|
f_cls = classmethod(f)
|
|
setattr(f_cls, ROOT_VALIDATOR_CONFIG_KEY, Validator(func=f, pre=pre))
|
|
return f_cls
|
|
|
|
return dec
|
|
|
|
|
|
def _check_validator_name(f: AnyCallable) -> None:
|
|
"""
|
|
avoid validators with duplicated names since without this, validators can be overwritten silently
|
|
which generally isn't the intended behaviour, don't run in ipython - see #312
|
|
"""
|
|
if not in_ipython(): # pragma: no branch
|
|
ref = f.__module__ + '.' + f.__qualname__
|
|
if ref in _FUNCS:
|
|
raise ConfigError(f'duplicate validator function "{ref}"')
|
|
_FUNCS.add(ref)
|
|
|
|
|
|
class ValidatorGroup:
|
|
def __init__(self, validators: 'ValidatorListDict') -> None:
|
|
self.validators = validators
|
|
self.used_validators = {'*'}
|
|
|
|
def get_validators(self, name: str) -> Optional[Dict[str, Validator]]:
|
|
self.used_validators.add(name)
|
|
validators = self.validators.get(name, [])
|
|
if name != ROOT_KEY:
|
|
validators += self.validators.get('*', [])
|
|
if validators:
|
|
return {v.func.__name__: v for v in validators}
|
|
else:
|
|
return None
|
|
|
|
def check_for_unused(self) -> None:
|
|
unused_validators = set(
|
|
chain(
|
|
*[
|
|
(v.func.__name__ for v in self.validators[f] if v.check_fields)
|
|
for f in (self.validators.keys() - self.used_validators)
|
|
]
|
|
)
|
|
)
|
|
if unused_validators:
|
|
fn = ', '.join(unused_validators)
|
|
raise ConfigError(
|
|
f"Validators defined with incorrect fields: {fn} " # noqa: Q000
|
|
f"(use check_fields=False if you're inheriting from the model and intended this)"
|
|
)
|
|
|
|
|
|
def extract_validators(namespace: Dict[str, Any]) -> Dict[str, List[Validator]]:
|
|
validators: Dict[str, List[Validator]] = {}
|
|
for var_name, value in namespace.items():
|
|
validator_config = getattr(value, VALIDATOR_CONFIG_KEY, None)
|
|
if validator_config:
|
|
fields, v = validator_config
|
|
for field in fields:
|
|
if field in validators:
|
|
validators[field].append(v)
|
|
else:
|
|
validators[field] = [v]
|
|
return validators
|
|
|
|
|
|
def extract_root_validators(namespace: Dict[str, Any]) -> Tuple[List[AnyCallable], List[AnyCallable]]:
|
|
pre_validators: List[AnyCallable] = []
|
|
post_validators: List[AnyCallable] = []
|
|
for name, value in namespace.items():
|
|
validator_config: Optional[Validator] = getattr(value, ROOT_VALIDATOR_CONFIG_KEY, None)
|
|
if validator_config:
|
|
sig = signature(validator_config.func)
|
|
args = list(sig.parameters.keys())
|
|
if args[0] == 'self':
|
|
raise ConfigError(
|
|
f'Invalid signature for root validator {name}: {sig}, "self" not permitted as first argument, '
|
|
f'should be: (cls, values).'
|
|
)
|
|
if len(args) != 2:
|
|
raise ConfigError(f'Invalid signature for root validator {name}: {sig}, should be: (cls, values).')
|
|
# check function signature
|
|
if validator_config.pre:
|
|
pre_validators.append(validator_config.func)
|
|
else:
|
|
post_validators.append(validator_config.func)
|
|
return pre_validators, post_validators
|
|
|
|
|
|
def inherit_validators(base_validators: 'ValidatorListDict', validators: 'ValidatorListDict') -> 'ValidatorListDict':
|
|
for field, field_validators in base_validators.items():
|
|
if field not in validators:
|
|
validators[field] = []
|
|
validators[field] += field_validators
|
|
return validators
|
|
|
|
|
|
def make_generic_validator(validator: AnyCallable) -> 'ValidatorCallable':
|
|
"""
|
|
Make a generic function which calls a validator with the right arguments.
|
|
|
|
Unfortunately other approaches (eg. return a partial of a function that builds the arguments) is slow,
|
|
hence this laborious way of doing things.
|
|
|
|
It's done like this so validators don't all need **kwargs in their signature, eg. any combination of
|
|
the arguments "values", "fields" and/or "config" are permitted.
|
|
"""
|
|
sig = signature(validator)
|
|
args = list(sig.parameters.keys())
|
|
first_arg = args.pop(0)
|
|
if first_arg == 'self':
|
|
raise ConfigError(
|
|
f'Invalid signature for validator {validator}: {sig}, "self" not permitted as first argument, '
|
|
f'should be: (cls, value, values, config, field), "values", "config" and "field" are all optional.'
|
|
)
|
|
elif first_arg == 'cls':
|
|
# assume the second argument is value
|
|
return wraps(validator)(_generic_validator_cls(validator, sig, set(args[1:])))
|
|
else:
|
|
# assume the first argument was value which has already been removed
|
|
return wraps(validator)(_generic_validator_basic(validator, sig, set(args)))
|
|
|
|
|
|
def prep_validators(v_funcs: Iterable[AnyCallable]) -> 'ValidatorsList':
|
|
return [make_generic_validator(f) for f in v_funcs if f]
|
|
|
|
|
|
all_kwargs = {'values', 'field', 'config'}
|
|
|
|
|
|
def _generic_validator_cls(validator: AnyCallable, sig: Signature, args: Set[str]) -> 'ValidatorCallable':
|
|
# assume the first argument is value
|
|
has_kwargs = False
|
|
if 'kwargs' in args:
|
|
has_kwargs = True
|
|
args -= {'kwargs'}
|
|
|
|
if not args.issubset(all_kwargs):
|
|
raise ConfigError(
|
|
f'Invalid signature for validator {validator}: {sig}, should be: '
|
|
f'(cls, value, values, config, field), "values", "config" and "field" are all optional.'
|
|
)
|
|
|
|
if has_kwargs:
|
|
return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field, config=config)
|
|
elif args == set():
|
|
return lambda cls, v, values, field, config: validator(cls, v)
|
|
elif args == {'values'}:
|
|
return lambda cls, v, values, field, config: validator(cls, v, values=values)
|
|
elif args == {'field'}:
|
|
return lambda cls, v, values, field, config: validator(cls, v, field=field)
|
|
elif args == {'config'}:
|
|
return lambda cls, v, values, field, config: validator(cls, v, config=config)
|
|
elif args == {'values', 'field'}:
|
|
return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field)
|
|
elif args == {'values', 'config'}:
|
|
return lambda cls, v, values, field, config: validator(cls, v, values=values, config=config)
|
|
elif args == {'field', 'config'}:
|
|
return lambda cls, v, values, field, config: validator(cls, v, field=field, config=config)
|
|
else:
|
|
# args == {'values', 'field', 'config'}
|
|
return lambda cls, v, values, field, config: validator(cls, v, values=values, field=field, config=config)
|
|
|
|
|
|
def _generic_validator_basic(validator: AnyCallable, sig: Signature, args: Set[str]) -> 'ValidatorCallable':
|
|
has_kwargs = False
|
|
if 'kwargs' in args:
|
|
has_kwargs = True
|
|
args -= {'kwargs'}
|
|
|
|
if not args.issubset(all_kwargs):
|
|
raise ConfigError(
|
|
f'Invalid signature for validator {validator}: {sig}, should be: '
|
|
f'(value, values, config, field), "values", "config" and "field" are all optional.'
|
|
)
|
|
|
|
if has_kwargs:
|
|
return lambda cls, v, values, field, config: validator(v, values=values, field=field, config=config)
|
|
elif args == set():
|
|
return lambda cls, v, values, field, config: validator(v)
|
|
elif args == {'values'}:
|
|
return lambda cls, v, values, field, config: validator(v, values=values)
|
|
elif args == {'field'}:
|
|
return lambda cls, v, values, field, config: validator(v, field=field)
|
|
elif args == {'config'}:
|
|
return lambda cls, v, values, field, config: validator(v, config=config)
|
|
elif args == {'values', 'field'}:
|
|
return lambda cls, v, values, field, config: validator(v, values=values, field=field)
|
|
elif args == {'values', 'config'}:
|
|
return lambda cls, v, values, field, config: validator(v, values=values, config=config)
|
|
elif args == {'field', 'config'}:
|
|
return lambda cls, v, values, field, config: validator(v, field=field, config=config)
|
|
else:
|
|
# args == {'values', 'field', 'config'}
|
|
return lambda cls, v, values, field, config: validator(v, values=values, field=field, config=config)
|
|
|
|
|
|
def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, classmethod]:
|
|
all_attributes = ChainMap(*[cls.__dict__ for cls in type_.__mro__])
|
|
return {
|
|
k: v
|
|
for k, v in all_attributes.items()
|
|
if hasattr(v, VALIDATOR_CONFIG_KEY) or hasattr(v, ROOT_VALIDATOR_CONFIG_KEY)
|
|
}
|