check for invalid validators (#140)

* check for invalid validators

* documentation and history
This commit is contained in:
Samuel Colvin
2018-03-25 16:41:48 +01:00
committed by GitHub
parent 76db984cc1
commit a8096959e2
7 changed files with 64 additions and 18 deletions
+1
View File
@@ -12,3 +12,4 @@ htmlcov/
benchmarks/*.json
docs/_build/
docs/.TMP_HISTORY.rst
.pytest_cache/
+2 -1
View File
@@ -3,9 +3,10 @@
History
-------
v0.8.0 (2018-03-XX)
v0.8.0 (2018-03-25)
...................
* fix type annotation for ``inherit_config`` #139
* **breaking change**: vheck for invalid field names in validators #140
v0.7.1 (2018-02-07)
...................
+14 -1
View File
@@ -160,7 +160,20 @@ to set a dynamic default value.
(This script is complete, it should run "as is")
You'll often want to use this together with ``pre`` since otherwise the with ``always=True``
_pydantic_ would try to validate the default ``None`` which would cause an error.
*pydantic* would try to validate the default ``None`` which would cause an error.
Field Checks
~~~~~~~~~~~~
.. note::
New in version ``v0.8.0``.
On class creation validators are checked to confirm that the fields they specify actually exist on the model.
Occasionally however this is not wanted: when you define a validator to validate fields on inheriting models.
In this case you should set ``check_fields=False`` on the validator.
Recursive Models
+1
View File
@@ -27,6 +27,7 @@ class Validator(NamedTuple):
pre: bool
whole: bool
always: bool
check_fields: bool
class Field:
+33 -15
View File
@@ -1,6 +1,7 @@
import warnings
from abc import ABCMeta
from copy import deepcopy
from itertools import chain
from pathlib import Path
from types import FunctionType
from typing import Any, Dict, Set, Type, Union
@@ -39,6 +40,27 @@ def inherit_config(self_config, parent_config) -> Type[BaseConfig]:
TYPE_BLACKLIST = FunctionType, property, type, classmethod, staticmethod
class ValidatorGroup:
def __init__(self, validators):
self.validators = validators
self.used_validators = {'*'}
def get_validators(self, name):
self.used_validators.add(name)
specific_validators = self.validators.get(name)
wildcard_validators = self.validators.get('*')
if specific_validators or wildcard_validators:
return (specific_validators or []) + (wildcard_validators or [])
def check_for_unused(self):
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} "
f"(use check_fields=True if you're inheriting from the model and intended this)")
def _extract_validators(namespace):
validators = {}
for var_name, value in namespace.items():
@@ -53,15 +75,7 @@ def _extract_validators(namespace):
return validators
def _get_validators(validators, name):
specific_validators = validators.get(name)
wildcard_validators = validators.get('*')
if specific_validators or wildcard_validators:
return (specific_validators or []) + (wildcard_validators or [])
class MetaModel(ABCMeta):
def __new__(mcs, name, bases, namespace):
fields = {}
config = BaseConfig
@@ -71,7 +85,7 @@ class MetaModel(ABCMeta):
config = inherit_config(base.__config__, config)
config = inherit_config(namespace.get('Config'), config)
validators = _extract_validators(namespace)
vg = ValidatorGroup(_extract_validators(namespace))
for f in fields.values():
f.set_config(config)
@@ -84,7 +98,7 @@ class MetaModel(ABCMeta):
name=ann_name,
value=...,
annotation=ann_type,
class_validators=_get_validators(validators, ann_name),
class_validators=vg.get_validators(ann_name),
config=config,
)
@@ -94,14 +108,15 @@ class MetaModel(ABCMeta):
name=var_name,
value=value,
annotation=annotations.get(var_name),
class_validators=_get_validators(validators, var_name),
class_validators=vg.get_validators(var_name),
config=config,
)
vg.check_for_unused()
new_namespace = {
'__config__': config,
'__fields__': fields,
'__validators__': validators,
'__validators__': vg.validators,
**{n: v for n, v in namespace.items() if n not in fields}
}
return super().__new__(mcs, name, bases, new_namespace)
@@ -335,6 +350,7 @@ def create_model(
validators = {}
config = __config__ or BaseConfig
vg = ValidatorGroup(validators)
for f_name, f_def in field_definitions.items():
if isinstance(f_def, tuple):
@@ -353,9 +369,10 @@ def create_model(
name=f_name,
value=f_value,
annotation=f_annotation,
class_validators=_get_validators(validators, f_name),
class_validators=vg.get_validators(f_name),
config=config,
)
namespace = {
'config': config,
'__fields__': fields,
@@ -366,13 +383,14 @@ def create_model(
_FUNCS = set()
def validator(*fields, pre: bool=False, whole: bool=False, always: bool=False):
def validator(*fields, pre: bool=False, whole: bool=False, always: bool=False, check_fields: bool=True):
"""
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 whole: for complex objects (sets, lists etc.) whether to validate individual elements or 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')
@@ -386,6 +404,6 @@ def validator(*fields, pre: bool=False, whole: bool=False, always: bool=False):
raise ConfigError(f'duplicate validator function "{ref}"')
_FUNCS.add(ref)
f_cls = classmethod(f)
f_cls.__validator_config = fields, Validator(f, pre, whole, always)
f_cls.__validator_config = fields, Validator(f, pre, whole, always, check_fields)
return f_cls
return dec
+1 -1
View File
@@ -63,7 +63,7 @@ def test_custom_config():
def test_inheritance_validators():
class BarModel(BaseModel):
@validator('a')
@validator('a', check_fields=False)
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
+12
View File
@@ -277,3 +277,15 @@ def test_wildcard_validator_error():
Model(a='snap')
assert '"foobar" not found in a' in str(exc_info.value)
assert len(exc_info.value.errors_dict) == 2
def test_invalid_field():
with pytest.raises(ConfigError) as exc_info:
class Model(BaseModel):
a: str
@validator('b')
def check_b(cls, v):
return v
assert str(exc_info.value) == ("Validators defined with incorrect fields: check_b "
"(use check_fields=True if you're inheriting from the model and intended this)")