mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
check for invalid validators (#140)
* check for invalid validators * documentation and history
This commit is contained in:
@@ -12,3 +12,4 @@ htmlcov/
|
||||
benchmarks/*.json
|
||||
docs/_build/
|
||||
docs/.TMP_HISTORY.rst
|
||||
.pytest_cache/
|
||||
|
||||
+2
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -27,6 +27,7 @@ class Validator(NamedTuple):
|
||||
pre: bool
|
||||
whole: bool
|
||||
always: bool
|
||||
check_fields: bool
|
||||
|
||||
|
||||
class Field:
|
||||
|
||||
+33
-15
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)")
|
||||
|
||||
Reference in New Issue
Block a user