Skip to content

Commit

Permalink
rebuild validator arguments (pydantic#388)
Browse files Browse the repository at this point in the history
* rebuild validator arguments

* cleanup and tests

* update docs
  • Loading branch information
samuelcolvin authored Feb 13, 2019
1 parent 8fa1382 commit baade9a
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 67 deletions.
6 changes: 6 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
History
-------

v0.20.0 (unreleased)
....................
* **breaking change** (maybe): more sophisticated argument parsing for validators, any subset of
``values``, ``config`` and ``field`` is now permitted, eg. ``(cls, value, field)``,
however the variadic key word argument ("``**kwargs``") **must** be called ``kwargs``, #388 by @samuelcolvin

v0.19.0 (2019-02-04)
....................
* Support ``Callable`` type hint, fix #279 by @proofit404
Expand Down
4 changes: 3 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,9 @@ A few things to note on validators:

* validators are "class methods", the first value they receive here will be the ``UserModel`` not an instance
of ``UserModel``
* their signature can be ``(cls, value)`` or ``(cls, value, *, values, config, field)``
* their signature can be ``(cls, value)`` or ``(cls, value, values, config, field)``. As of **v0.20**, any subset of
``values``, ``config`` and ``field`` is also permitted, eg. ``(cls, value, field)``, however due to the way
validators are inspected, the variadic key word argument ("``**kwargs``") **must** be called ``kwargs``.
* validator should either return the new value or raise a ``ValueError`` or ``TypeError``
* where validators rely on other values, you should be aware that:

Expand Down
134 changes: 100 additions & 34 deletions pydantic/class_validators.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import inspect
from dataclasses import dataclass
from enum import IntEnum
from functools import wraps
from inspect import Signature, signature
from itertools import chain
from types import FunctionType
from typing import Any, Callable, Dict, List, Optional, Set
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Type

from .errors import ConfigError
from .utils import AnyCallable, in_ipython

if TYPE_CHECKING: # pragma: no cover
from .main import BaseConfig, BaseModel
from .fields import Field

class ValidatorSignature(IntEnum):
JUST_VALUE = 1
VALUE_KWARGS = 2
CLS_JUST_VALUE = 3
CLS_VALUE_KWARGS = 4
ValidatorCallable = Callable[[Optional[Type[BaseModel]], Any, Dict[str, Any], Field, Type[BaseConfig]], Any]


@dataclass
Expand Down Expand Up @@ -118,30 +117,97 @@ def inherit_validators(base_validators: ValidatorListDict, validators: Validator
return validators


def get_validator_signature(validator: Any) -> ValidatorSignature:
signature = inspect.signature(validator)

# bind here will raise a TypeError so:
# 1. we can deal with it before validation begins
# 2. (more importantly) it doesn't get confused with a TypeError when executing the validator
try:
if 'cls' in signature._parameters: # type: ignore
if len(signature.parameters) == 2:
signature.bind(object(), 1)
return ValidatorSignature.CLS_JUST_VALUE
else:
signature.bind(object(), 1, values=2, config=3, field=4)
return ValidatorSignature.CLS_VALUE_KWARGS
else:
if len(signature.parameters) == 1:
signature.bind(1)
return ValidatorSignature.JUST_VALUE
else:
signature.bind(1, values=2, config=3, field=4)
return ValidatorSignature.VALUE_KWARGS
except TypeError as e:
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)))


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}: {signature}, should be: '
f'(value) or (value, *, values, config, field) or for class validators '
f'(cls, value) or (cls, value, *, values, config, field)'
) from e
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)
37 changes: 15 additions & 22 deletions pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, Optional, Pattern, Set, Tuple, Type, Union, cast

from . import errors as errors_
from .class_validators import Validator, ValidatorSignature, get_validator_signature
from .class_validators import Validator, make_generic_validator
from .error_wrappers import ErrorWrapper
from .types import Json, JsonWrapper
from .utils import AnyCallable, AnyType, Callable, ForwardRef, display_as_type, lenient_issubclass, list_like
Expand All @@ -12,11 +12,12 @@
Required: Any = Ellipsis

if TYPE_CHECKING: # pragma: no cover
from .schema import Schema # noqa: F401
from .main import BaseConfig, BaseModel # noqa: F401
from .class_validators import ValidatorCallable # noqa: F401
from .error_wrappers import ErrorList
from .main import BaseConfig, BaseModel # noqa: F401
from .schema import Schema # noqa: F401

ValidatorTuple = Tuple[Tuple[ValidatorSignature, AnyCallable], ...]
ValidatorsList = List[ValidatorCallable]
ValidateReturn = Tuple[Optional[Any], Optional[ErrorList]]
LocType = Union[Tuple[str, ...], str]

Expand Down Expand Up @@ -78,9 +79,9 @@ def __init__(
self.validate_always: bool = False
self.sub_fields: Optional[List[Field]] = None
self.key_field: Optional[Field] = None
self.validators: 'ValidatorTuple' = ()
self.whole_pre_validators: Optional['ValidatorTuple'] = None
self.whole_post_validators: Optional['ValidatorTuple'] = None
self.validators: 'ValidatorsList' = []
self.whole_pre_validators: Optional['ValidatorsList'] = None
self.whole_post_validators: Optional['ValidatorsList'] = None
self.parse_json: bool = False
self.shape: Shape = Shape.SINGLETON
self.prepare()
Expand Down Expand Up @@ -220,13 +221,13 @@ def _populate_validators(self) -> None:
f'get_validators has been replaced by __get_validators__ (on {self.name})', DeprecationWarning
)
v_funcs = (
*tuple(v.func for v in class_validators_ if not v.whole and v.pre),
*[v.func for v in class_validators_ if not v.whole and v.pre],
*(
get_validators()
if get_validators
else find_validators(self.type_, self.model_config.arbitrary_types_allowed)
),
*tuple(v.func for v in class_validators_ if not v.whole and not v.pre),
*[v.func for v in class_validators_ if not v.whole and not v.pre],
)
self.validators = self._prep_vals(v_funcs)

Expand All @@ -235,8 +236,8 @@ def _populate_validators(self) -> None:
self.whole_post_validators = self._prep_vals(v.func for v in class_validators_ if v.whole and not v.pre)

@staticmethod
def _prep_vals(v_funcs: Iterable[AnyCallable]) -> 'ValidatorTuple':
return tuple((get_validator_signature(f), f) for f in v_funcs if f)
def _prep_vals(v_funcs: Iterable[AnyCallable]) -> 'ValidatorsList':
return [make_generic_validator(f) for f in v_funcs if f]

def validate(
self, v: Any, values: Dict[str, Any], *, loc: 'LocType', cls: Optional[Type['BaseModel']] = None
Expand Down Expand Up @@ -379,19 +380,11 @@ def _apply_validators(
values: Dict[str, Any],
loc: 'LocType',
cls: Optional[Type['BaseModel']],
validators: 'ValidatorTuple',
validators: 'ValidatorsList',
) -> 'ValidateReturn':
for signature, validator in validators:
for validator in validators:
try:
if signature is ValidatorSignature.JUST_VALUE:
v = validator(v)
elif signature is ValidatorSignature.VALUE_KWARGS:
v = validator(v, values=values, config=self.model_config, field=self)
elif signature is ValidatorSignature.CLS_JUST_VALUE:
v = validator(cls, v)
else:
# ValidatorSignature.CLS_VALUE_KWARGS
v = validator(cls, v, values=values, config=self.model_config, field=self)
v = validator(cls, v, values, self, self.model_config)
except (ValueError, TypeError) as exc:
return v, ErrorWrapper(exc, loc=loc, config=self.model_config)
return v, None
Expand Down
2 changes: 1 addition & 1 deletion pydantic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate

@classmethod
def validate(cls, value: str, values: Dict[str, Any], **kwarg: Any) -> str:
def validate(cls, value: str, values: Dict[str, Any]) -> str:
if value:
return value

Expand Down
12 changes: 6 additions & 6 deletions pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ def float_validator(v: Any) -> float:
return float(v)


def number_multiple_validator(v: 'Number', field: 'Field', config: 'BaseConfig', **kwargs: Any) -> 'Number':
def number_multiple_validator(v: 'Number', field: 'Field') -> 'Number':
field_type = cast('ConstrainedNumber', field.type_)
if field_type.multiple_of is not None and v % field_type.multiple_of != 0: # type: ignore
raise errors.NumberNotMultipleError(multiple_of=field_type.multiple_of)

return v


def number_size_validator(v: 'Number', field: 'Field', config: 'BaseConfig', **kwargs: Any) -> 'Number':
def number_size_validator(v: 'Number', field: 'Field') -> 'Number':
field_type = cast('ConstrainedNumber', field.type_)
if field_type.gt is not None and not v > field_type.gt:
raise errors.NumberNotGtError(limit_value=field_type.gt)
Expand All @@ -112,7 +112,7 @@ def number_size_validator(v: 'Number', field: 'Field', config: 'BaseConfig', **k
return v


def anystr_length_validator(v: 'StrBytes', field: 'Field', config: 'BaseConfig', **kwargs: Any) -> 'StrBytes':
def anystr_length_validator(v: 'StrBytes', field: 'Field', config: 'BaseConfig') -> 'StrBytes':
v_len = len(v)

min_length = getattr(field.type_, 'min_length', config.min_anystr_length)
Expand All @@ -126,7 +126,7 @@ def anystr_length_validator(v: 'StrBytes', field: 'Field', config: 'BaseConfig',
return v


def anystr_strip_whitespace(v: 'StrBytes', field: 'Field', config: 'BaseConfig', **kwargs: Any) -> 'StrBytes':
def anystr_strip_whitespace(v: 'StrBytes', field: 'Field', config: 'BaseConfig') -> 'StrBytes':
strip_whitespace = getattr(field.type_, 'strip_whitespace', config.anystr_strip_whitespace)
if strip_whitespace:
v = v.strip()
Expand Down Expand Up @@ -177,14 +177,14 @@ def set_validator(v: Any) -> Set[Any]:
raise errors.SetError()


def enum_validator(v: Any, field: 'Field', config: 'BaseConfig', **kwargs: Any) -> Enum:
def enum_validator(v: Any, field: 'Field', config: 'BaseConfig') -> Enum:
with change_exception(errors.EnumError, ValueError):
enum_v = field.type_(v)

return enum_v.value if config.use_enum_values else enum_v


def uuid_validator(v: Any, field: 'Field', config: 'BaseConfig', **kwargs: Any) -> UUID:
def uuid_validator(v: Any, field: 'Field') -> UUID:
with change_exception(errors.UUIDError, ValueError):
if isinstance(v, str):
v = UUID(v)
Expand Down
2 changes: 1 addition & 1 deletion pydantic/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

__all__ = ['VERSION']

VERSION = StrictVersion('0.19.0')
VERSION = StrictVersion('0.20.0a1')
2 changes: 1 addition & 1 deletion tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Model(BaseModel):
m = Model(v='s')
assert m.v == 's'
assert '<Field(v type=typing.Union[str, bytes] required)>' == repr(m.fields['v'])
assert 'not_none_validator' in [v[1].__qualname__ for v in m.fields['v'].sub_fields[0].validators]
assert 'not_none_validator' in [v.__qualname__ for v in m.fields['v'].sub_fields[0].validators]

m = Model(v=b'b')
assert m.v == 'b'
Expand Down
Loading

0 comments on commit baade9a

Please sign in to comment.