Files
pydantic/tests/test_validators.py
T
Tyler Wozniak 3e4c1b5714 Adding deque to valid field types (#1935)
* Adding deque to valid field types

* Added missing type example

* Fix bad copy-paste

Co-authored-by: PrettyWood <em.jolibois@gmail.com>

* correct enum validator name

* correct enum validator name, take2

Co-authored-by: PrettyWood <em.jolibois@gmail.com>
Co-authored-by: Samuel Colvin <s@muelcolvin.com>
2020-10-25 17:52:34 +00:00

1134 lines
31 KiB
Python

from collections import deque
from datetime import datetime
from itertools import product
from typing import Dict, List, Optional, Tuple
import pytest
from pydantic import BaseModel, ConfigError, Extra, ValidationError, errors, validator
from pydantic.class_validators import make_generic_validator, root_validator
from pydantic.typing import Literal
def test_simple():
class Model(BaseModel):
a: str
@validator('a')
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
assert Model(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError) as exc_info:
Model(a='snap')
assert exc_info.value.errors() == [{'loc': ('a',), 'msg': '"foobar" not found in a', 'type': 'value_error'}]
def test_int_validation():
class Model(BaseModel):
a: int
with pytest.raises(ValidationError) as exc_info:
Model(a='snap')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
assert Model(a=3).a == 3
assert Model(a=True).a == 1
assert Model(a=False).a == 0
assert Model(a=4.5).a == 4
def test_frozenset_validation():
class Model(BaseModel):
a: frozenset
with pytest.raises(ValidationError) as exc_info:
Model(a='snap')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid frozenset', 'type': 'type_error.frozenset'}
]
assert Model(a={1, 2, 3}).a == frozenset({1, 2, 3})
assert Model(a=frozenset({1, 2, 3})).a == frozenset({1, 2, 3})
assert Model(a=[4, 5]).a == frozenset({4, 5})
assert Model(a=(6,)).a == frozenset({6})
def test_deque_validation():
class Model(BaseModel):
a: deque
with pytest.raises(ValidationError) as exc_info:
Model(a='snap')
assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'value is not a valid deque', 'type': 'type_error.deque'}]
assert Model(a={1, 2, 3}).a == deque([1, 2, 3])
assert Model(a=deque({1, 2, 3})).a == deque([1, 2, 3])
assert Model(a=[4, 5]).a == deque([4, 5])
assert Model(a=(6,)).a == deque([6])
def test_validate_whole():
class Model(BaseModel):
a: List[int]
@validator('a', pre=True)
def check_a1(cls, v):
v.append('123')
return v
@validator('a')
def check_a2(cls, v):
v.append(456)
return v
assert Model(a=[1, 2]).a == [1, 2, 123, 456]
def test_validate_kwargs():
class Model(BaseModel):
b: int
a: List[int]
@validator('a', each_item=True)
def check_a1(cls, v, values, **kwargs):
return v + values['b']
assert Model(a=[1, 2], b=6).a == [7, 8]
def test_validate_pre_error():
calls = []
class Model(BaseModel):
a: List[int]
@validator('a', pre=True)
def check_a1(cls, v):
calls.append(f'check_a1 {v}')
if 1 in v:
raise ValueError('a1 broken')
v[0] += 1
return v
@validator('a')
def check_a2(cls, v):
calls.append(f'check_a2 {v}')
if 10 in v:
raise ValueError('a2 broken')
return v
assert Model(a=[3, 8]).a == [4, 8]
assert calls == ['check_a1 [3, 8]', 'check_a2 [4, 8]']
calls = []
with pytest.raises(ValidationError) as exc_info:
Model(a=[1, 3])
assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'a1 broken', 'type': 'value_error'}]
assert calls == ['check_a1 [1, 3]']
calls = []
with pytest.raises(ValidationError) as exc_info:
Model(a=[5, 10])
assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'a2 broken', 'type': 'value_error'}]
assert calls == ['check_a1 [5, 10]', 'check_a2 [6, 10]']
class ValidateAssignmentModel(BaseModel):
a: int = 4
b: str = ...
c: int = 0
@validator('b')
def b_length(cls, v, values, **kwargs):
if 'a' in values and len(v) < values['a']:
raise ValueError('b too short')
return v
@validator('c')
def double_c(cls, v):
return v * 2
class Config:
validate_assignment = True
extra = Extra.allow
def test_validating_assignment_ok():
p = ValidateAssignmentModel(b='hello')
assert p.b == 'hello'
def test_validating_assignment_fail():
with pytest.raises(ValidationError):
ValidateAssignmentModel(a=10, b='hello')
p = ValidateAssignmentModel(b='hello')
with pytest.raises(ValidationError):
p.b = 'x'
def test_validating_assignment_value_change():
p = ValidateAssignmentModel(b='hello', c=2)
assert p.c == 4
p = ValidateAssignmentModel(b='hello')
assert p.c == 0
p.c = 3
assert p.c == 6
def test_validating_assignment_extra():
p = ValidateAssignmentModel(b='hello', extra_field=1.23)
assert p.extra_field == 1.23
p = ValidateAssignmentModel(b='hello')
p.extra_field = 1.23
assert p.extra_field == 1.23
p.extra_field = 'bye'
assert p.extra_field == 'bye'
def test_validating_assignment_dict():
with pytest.raises(ValidationError) as exc_info:
ValidateAssignmentModel(a='x', b='xx')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
def test_validate_multiple():
# also test TypeError
class Model(BaseModel):
a: str
b: str
@validator('a', 'b')
def check_a_and_b(cls, v, field, **kwargs):
if len(v) < 4:
raise TypeError(f'{field.alias} is too short')
return v + 'x'
assert Model(a='1234', b='5678').dict() == {'a': '1234x', 'b': '5678x'}
with pytest.raises(ValidationError) as exc_info:
Model(a='x', b='x')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'a is too short', 'type': 'type_error'},
{'loc': ('b',), 'msg': 'b is too short', 'type': 'type_error'},
]
def test_classmethod():
class Model(BaseModel):
a: str
@validator('a')
def check_a(cls, v):
assert cls is Model
return v
m = Model(a='this is foobar good')
assert m.a == 'this is foobar good'
m.check_a('x')
def test_duplicates():
with pytest.raises(errors.ConfigError) as exc_info:
class Model(BaseModel):
a: str
b: str
@validator('a')
def duplicate_name(cls, v):
return v
@validator('b') # noqa
def duplicate_name(cls, v): # noqa
return v
assert str(exc_info.value) == (
'duplicate validator function '
'"tests.test_validators.test_duplicates.<locals>.Model.duplicate_name"; '
'if this is intended, set `allow_reuse=True`'
)
def test_use_bare():
with pytest.raises(errors.ConfigError) as exc_info:
class Model(BaseModel):
a: str
@validator
def checker(cls, v):
return v
assert 'validators should be used with fields' in str(exc_info.value)
def test_use_no_fields():
with pytest.raises(errors.ConfigError) as exc_info:
class Model(BaseModel):
a: str
@validator()
def checker(cls, v):
return v
assert 'validator with no fields specified' in str(exc_info.value)
def test_validate_always():
check_calls = 0
class Model(BaseModel):
a: str = None
@validator('a', pre=True, always=True)
def check_a(cls, v):
nonlocal check_calls
check_calls += 1
return v or 'xxx'
assert Model().a == 'xxx'
assert check_calls == 1
assert Model(a='y').a == 'y'
assert check_calls == 2
def test_validate_always_on_inheritance():
check_calls = 0
class ParentModel(BaseModel):
a: str = None
class Model(ParentModel):
@validator('a', pre=True, always=True)
def check_a(cls, v):
nonlocal check_calls
check_calls += 1
return v or 'xxx'
assert Model().a == 'xxx'
assert check_calls == 1
assert Model(a='y').a == 'y'
assert check_calls == 2
def test_validate_not_always():
check_calls = 0
class Model(BaseModel):
a: str = None
@validator('a', pre=True)
def check_a(cls, v):
nonlocal check_calls
check_calls += 1
return v or 'xxx'
assert Model().a is None
assert check_calls == 0
assert Model(a='y').a == 'y'
assert check_calls == 1
def test_wildcard_validators():
calls = []
class Model(BaseModel):
a: str
b: int
@validator('a')
def check_a(cls, v, field, **kwargs):
calls.append(('check_a', v, field.name))
return v
@validator('*')
def check_all(cls, v, field, **kwargs):
calls.append(('check_all', v, field.name))
return v
assert Model(a='abc', b='123').dict() == dict(a='abc', b=123)
assert calls == [('check_a', 'abc', 'a'), ('check_all', 'abc', 'a'), ('check_all', 123, 'b')]
def test_wildcard_validator_error():
class Model(BaseModel):
a: str
b: str
@validator('*')
def check_all(cls, v, field, **kwargs):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
assert Model(a='foobar a', b='foobar b').b == 'foobar b'
with pytest.raises(ValidationError) as exc_info:
Model(a='snap')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': '"foobar" not found in a', 'type': 'value_error'},
{'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'},
]
def test_invalid_field():
with pytest.raises(errors.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 " # noqa: Q000
"(use check_fields=False if you're inheriting from the model and intended this)"
)
def test_validate_child():
class Parent(BaseModel):
a: str
class Child(Parent):
@validator('a')
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
assert Parent(a='this is not a child').a == 'this is not a child'
assert Child(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError):
Child(a='snap')
def test_validate_child_extra():
class Parent(BaseModel):
a: str
@validator('a')
def check_a_one(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
class Child(Parent):
@validator('a')
def check_a_two(cls, v):
return v.upper()
assert Parent(a='this is foobar good').a == 'this is foobar good'
assert Child(a='this is foobar good').a == 'THIS IS FOOBAR GOOD'
with pytest.raises(ValidationError):
Child(a='snap')
def test_validate_child_all():
class Parent(BaseModel):
a: str
class Child(Parent):
@validator('*')
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
assert Parent(a='this is not a child').a == 'this is not a child'
assert Child(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError):
Child(a='snap')
def test_validate_parent():
class Parent(BaseModel):
a: str
@validator('a')
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
class Child(Parent):
pass
assert Parent(a='this is foobar good').a == 'this is foobar good'
assert Child(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError):
Parent(a='snap')
with pytest.raises(ValidationError):
Child(a='snap')
def test_validate_parent_all():
class Parent(BaseModel):
a: str
@validator('*')
def check_a(cls, v):
if 'foobar' not in v:
raise ValueError('"foobar" not found in a')
return v
class Child(Parent):
pass
assert Parent(a='this is foobar good').a == 'this is foobar good'
assert Child(a='this is foobar good').a == 'this is foobar good'
with pytest.raises(ValidationError):
Parent(a='snap')
with pytest.raises(ValidationError):
Child(a='snap')
def test_inheritance_keep():
class Parent(BaseModel):
a: int
@validator('a')
def add_to_a(cls, v):
return v + 1
class Child(Parent):
pass
assert Child(a=0).a == 1
def test_inheritance_replace():
class Parent(BaseModel):
a: int
@validator('a')
def add_to_a(cls, v):
return v + 1
class Child(Parent):
@validator('a')
def add_to_a(cls, v):
return v + 5
assert Child(a=0).a == 5
def test_inheritance_new():
class Parent(BaseModel):
a: int
@validator('a')
def add_one_to_a(cls, v):
return v + 1
class Child(Parent):
@validator('a')
def add_five_to_a(cls, v):
return v + 5
assert Child(a=0).a == 6
def test_validation_each_item():
class Model(BaseModel):
foobar: Dict[int, int]
@validator('foobar', each_item=True)
def check_foobar(cls, v):
return v + 1
assert Model(foobar={1: 1}).foobar == {1: 2}
def test_key_validation():
class Model(BaseModel):
foobar: Dict[int, int]
@validator('foobar')
def check_foobar(cls, value):
return {k + 1: v + 1 for k, v in value.items()}
assert Model(foobar={1: 1}).foobar == {2: 2}
def test_validator_always_optional():
check_calls = 0
class Model(BaseModel):
a: Optional[str] = None
@validator('a', pre=True, always=True)
def check_a(cls, v):
nonlocal check_calls
check_calls += 1
return v or 'default value'
assert Model(a='y').a == 'y'
assert check_calls == 1
assert Model().a == 'default value'
assert check_calls == 2
def test_validator_always_pre():
check_calls = 0
class Model(BaseModel):
a: str = None
@validator('a', always=True, pre=True)
def check_a(cls, v):
nonlocal check_calls
check_calls += 1
return v or 'default value'
assert Model(a='y').a == 'y'
assert Model().a == 'default value'
assert check_calls == 2
def test_validator_always_post():
class Model(BaseModel):
a: str = None
@validator('a', always=True)
def check_a(cls, v):
return v or 'default value'
assert Model(a='y').a == 'y'
assert Model().a == 'default value'
def test_validator_always_post_optional():
class Model(BaseModel):
a: Optional[str] = None
@validator('a', always=True, pre=True)
def check_a(cls, v):
return v or 'default value'
assert Model(a='y').a == 'y'
assert Model().a == 'default value'
def test_datetime_validator():
check_calls = 0
class Model(BaseModel):
d: datetime = None
@validator('d', pre=True, always=True)
def check_d(cls, v):
nonlocal check_calls
check_calls += 1
return v or datetime(2032, 1, 1)
assert Model(d='2023-01-01T00:00:00').d == datetime(2023, 1, 1)
assert check_calls == 1
assert Model().d == datetime(2032, 1, 1)
assert check_calls == 2
assert Model(d=datetime(2023, 1, 1)).d == datetime(2023, 1, 1)
assert check_calls == 3
def test_pre_called_once():
check_calls = 0
class Model(BaseModel):
a: Tuple[int, int, int]
@validator('a', pre=True)
def check_a(cls, v):
nonlocal check_calls
check_calls += 1
return v
assert Model(a=['1', '2', '3']).a == (1, 2, 3)
assert check_calls == 1
@pytest.mark.parametrize(
'fields,result',
[
(['val'], '_v_'),
(['foobar'], '_v_'),
(['val', 'field'], '_v_,_field_'),
(['val', 'config'], '_v_,_config_'),
(['val', 'values'], '_v_,_values_'),
(['val', 'field', 'config'], '_v_,_field_,_config_'),
(['val', 'field', 'values'], '_v_,_field_,_values_'),
(['val', 'config', 'values'], '_v_,_config_,_values_'),
(['val', 'field', 'values', 'config'], '_v_,_field_,_values_,_config_'),
(['cls', 'val'], '_cls_,_v_'),
(['cls', 'foobar'], '_cls_,_v_'),
(['cls', 'val', 'field'], '_cls_,_v_,_field_'),
(['cls', 'val', 'config'], '_cls_,_v_,_config_'),
(['cls', 'val', 'values'], '_cls_,_v_,_values_'),
(['cls', 'val', 'field', 'config'], '_cls_,_v_,_field_,_config_'),
(['cls', 'val', 'field', 'values'], '_cls_,_v_,_field_,_values_'),
(['cls', 'val', 'config', 'values'], '_cls_,_v_,_config_,_values_'),
(['cls', 'val', 'field', 'values', 'config'], '_cls_,_v_,_field_,_values_,_config_'),
],
)
def test_make_generic_validator(fields, result):
exec(f"""def testing_function({', '.join(fields)}): return {' + "," + '.join(fields)}""")
func = locals()['testing_function']
validator = make_generic_validator(func)
assert validator.__qualname__ == 'testing_function'
assert validator.__name__ == 'testing_function'
# args: cls, v, values, field, config
assert validator('_cls_', '_v_', '_values_', '_field_', '_config_') == result
def test_make_generic_validator_kwargs():
def test_validator(v, **kwargs):
return ', '.join(f'{k}: {v}' for k, v in kwargs.items())
validator = make_generic_validator(test_validator)
assert validator.__name__ == 'test_validator'
assert validator('_cls_', '_v_', '_vs_', '_f_', '_c_') == 'values: _vs_, field: _f_, config: _c_'
def test_make_generic_validator_invalid():
def test_validator(v, foobar):
return foobar
with pytest.raises(ConfigError) as exc_info:
make_generic_validator(test_validator)
assert ': (v, foobar), should be: (value, values, config, field)' in str(exc_info.value)
def test_make_generic_validator_cls_kwargs():
def test_validator(cls, v, **kwargs):
return ', '.join(f'{k}: {v}' for k, v in kwargs.items())
validator = make_generic_validator(test_validator)
assert validator.__name__ == 'test_validator'
assert validator('_cls_', '_v_', '_vs_', '_f_', '_c_') == 'values: _vs_, field: _f_, config: _c_'
def test_make_generic_validator_cls_invalid():
def test_validator(cls, v, foobar):
return foobar
with pytest.raises(ConfigError) as exc_info:
make_generic_validator(test_validator)
assert ': (cls, v, foobar), should be: (cls, value, values, config, field)' in str(exc_info.value)
def test_make_generic_validator_self():
def test_validator(self, v):
return v
with pytest.raises(ConfigError) as exc_info:
make_generic_validator(test_validator)
assert ': (self, v), "self" not permitted as first argument, should be: (cls, value' in str(exc_info.value)
def test_assert_raises_validation_error():
class Model(BaseModel):
a: str
@validator('a')
def check_a(cls, v):
assert v == 'a', 'invalid a'
return v
Model(a='a')
with pytest.raises(ValidationError) as exc_info:
Model(a='snap')
injected_by_pytest = "\nassert 'snap' == 'a'\n - a\n + snap"
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': f'invalid a{injected_by_pytest}', 'type': 'assertion_error'}
]
def test_whole():
with pytest.warns(DeprecationWarning, match='The "whole" keyword argument is deprecated'):
class Model(BaseModel):
x: List[int]
@validator('x', whole=True)
def check_something(cls, v):
return v
def test_root_validator():
root_val_values = []
class Model(BaseModel):
a: int = 1
b: str
c: str
@validator('b')
def repeat_b(cls, v):
return v * 2
@root_validator
def example_root_validator(cls, values):
root_val_values.append(values)
if 'snap' in values.get('b', ''):
raise ValueError('foobar')
return dict(values, b='changed')
@root_validator
def example_root_validator2(cls, values):
root_val_values.append(values)
if 'snap' in values.get('c', ''):
raise ValueError('foobar2')
return dict(values, c='changed')
assert Model(a='123', b='bar', c='baz').dict() == {'a': 123, 'b': 'changed', 'c': 'changed'}
with pytest.raises(ValidationError) as exc_info:
Model(b='snap dragon', c='snap dragon2')
assert exc_info.value.errors() == [
{'loc': ('__root__',), 'msg': 'foobar', 'type': 'value_error'},
{'loc': ('__root__',), 'msg': 'foobar2', 'type': 'value_error'},
]
with pytest.raises(ValidationError) as exc_info:
Model(a='broken', b='bar', c='baz')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
assert root_val_values == [
{'a': 123, 'b': 'barbar', 'c': 'baz'},
{'a': 123, 'b': 'changed', 'c': 'baz'},
{'a': 1, 'b': 'snap dragonsnap dragon', 'c': 'snap dragon2'},
{'a': 1, 'b': 'snap dragonsnap dragon', 'c': 'snap dragon2'},
{'b': 'barbar', 'c': 'baz'},
{'b': 'changed', 'c': 'baz'},
]
def test_root_validator_pre():
root_val_values = []
class Model(BaseModel):
a: int = 1
b: str
@validator('b')
def repeat_b(cls, v):
return v * 2
@root_validator(pre=True)
def root_validator(cls, values):
root_val_values.append(values)
if 'snap' in values.get('b', ''):
raise ValueError('foobar')
return {'a': 42, 'b': 'changed'}
assert Model(a='123', b='bar').dict() == {'a': 42, 'b': 'changedchanged'}
with pytest.raises(ValidationError) as exc_info:
Model(b='snap dragon')
assert root_val_values == [{'a': '123', 'b': 'bar'}, {'b': 'snap dragon'}]
assert exc_info.value.errors() == [{'loc': ('__root__',), 'msg': 'foobar', 'type': 'value_error'}]
def test_root_validator_repeat():
with pytest.raises(errors.ConfigError, match='duplicate validator function'):
class Model(BaseModel):
a: int = 1
@root_validator
def root_validator_repeated(cls, values):
return values
@root_validator # noqa: F811
def root_validator_repeated(cls, values): # noqa: F811
return values
def test_root_validator_repeat2():
with pytest.raises(errors.ConfigError, match='duplicate validator function'):
class Model(BaseModel):
a: int = 1
@validator('a')
def repeat_validator(cls, v):
return v
@root_validator(pre=True) # noqa: F811
def repeat_validator(cls, values): # noqa: F811
return values
def test_root_validator_self():
with pytest.raises(
errors.ConfigError, match=r'Invalid signature for root validator root_validator: \(self, values\)'
):
class Model(BaseModel):
a: int = 1
@root_validator
def root_validator(self, values):
return values
def test_root_validator_extra():
with pytest.raises(errors.ConfigError) as exc_info:
class Model(BaseModel):
a: int = 1
@root_validator
def root_validator(cls, values, another):
return values
assert str(exc_info.value) == (
'Invalid signature for root validator root_validator: (cls, values, another), should be: (cls, values).'
)
def test_root_validator_types():
root_val_values = None
class Model(BaseModel):
a: int = 1
b: str
@root_validator
def root_validator(cls, values):
nonlocal root_val_values
root_val_values = cls, values
return values
class Config:
extra = Extra.allow
assert Model(b='bar', c='wobble').dict() == {'a': 1, 'b': 'bar', 'c': 'wobble'}
assert root_val_values == (Model, {'a': 1, 'b': 'bar', 'c': 'wobble'})
def test_root_validator_inheritance():
calls = []
class Parent(BaseModel):
pass
@root_validator
def root_validator_parent(cls, values):
calls.append(f'parent validator: {values}')
return {'extra1': 1, **values}
class Child(Parent):
a: int
@root_validator
def root_validator_child(cls, values):
calls.append(f'child validator: {values}')
return {'extra2': 2, **values}
assert len(Child.__post_root_validators__) == 2
assert len(Child.__pre_root_validators__) == 0
assert Child(a=123).dict() == {'extra2': 2, 'extra1': 1, 'a': 123}
assert calls == ["parent validator: {'a': 123}", "child validator: {'extra1': 1, 'a': 123}"]
def reusable_validator(num):
return num * 2
def test_reuse_global_validators():
class Model(BaseModel):
x: int
y: int
double_x = validator('x', allow_reuse=True)(reusable_validator)
double_y = validator('y', allow_reuse=True)(reusable_validator)
assert dict(Model(x=1, y=1)) == {'x': 2, 'y': 2}
def declare_with_reused_validators(include_root, allow_1, allow_2, allow_3):
class Model(BaseModel):
a: str
b: str
@validator('a', allow_reuse=allow_1)
def duplicate_name(cls, v):
return v
@validator('b', allow_reuse=allow_2) # noqa F811
def duplicate_name(cls, v): # noqa F811
return v
if include_root:
@root_validator(allow_reuse=allow_3) # noqa F811
def duplicate_name(cls, values): # noqa F811
return values
@pytest.fixture
def reset_tracked_validators():
from pydantic.class_validators import _FUNCS
original_tracked_validators = set(_FUNCS)
yield
_FUNCS.clear()
_FUNCS.update(original_tracked_validators)
@pytest.mark.parametrize('include_root,allow_1,allow_2,allow_3', product(*[[True, False]] * 4))
def test_allow_reuse(include_root, allow_1, allow_2, allow_3, reset_tracked_validators):
duplication_count = int(not allow_1) + int(not allow_2) + int(include_root and not allow_3)
if duplication_count > 1:
with pytest.raises(ConfigError) as exc_info:
declare_with_reused_validators(include_root, allow_1, allow_2, allow_3)
assert str(exc_info.value).startswith('duplicate validator function')
else:
declare_with_reused_validators(include_root, allow_1, allow_2, allow_3)
@pytest.mark.parametrize('validator_classmethod,root_validator_classmethod', product(*[[True, False]] * 2))
def test_root_validator_classmethod(validator_classmethod, root_validator_classmethod, reset_tracked_validators):
root_val_values = []
class Model(BaseModel):
a: int = 1
b: str
def repeat_b(cls, v):
return v * 2
if validator_classmethod:
repeat_b = classmethod(repeat_b)
repeat_b = validator('b')(repeat_b)
def example_root_validator(cls, values):
root_val_values.append(values)
if 'snap' in values.get('b', ''):
raise ValueError('foobar')
return dict(values, b='changed')
if root_validator_classmethod:
example_root_validator = classmethod(example_root_validator)
example_root_validator = root_validator(example_root_validator)
assert Model(a='123', b='bar').dict() == {'a': 123, 'b': 'changed'}
with pytest.raises(ValidationError) as exc_info:
Model(b='snap dragon')
assert exc_info.value.errors() == [{'loc': ('__root__',), 'msg': 'foobar', 'type': 'value_error'}]
with pytest.raises(ValidationError) as exc_info:
Model(a='broken', b='bar')
assert exc_info.value.errors() == [
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
]
assert root_val_values == [{'a': 123, 'b': 'barbar'}, {'a': 1, 'b': 'snap dragonsnap dragon'}, {'b': 'barbar'}]
def test_root_validator_skip_on_failure():
a_called = False
class ModelA(BaseModel):
a: int
@root_validator
def example_root_validator(cls, values):
nonlocal a_called
a_called = True
with pytest.raises(ValidationError):
ModelA(a='a')
assert a_called
b_called = False
class ModelB(BaseModel):
a: int
@root_validator(skip_on_failure=True)
def example_root_validator(cls, values):
nonlocal b_called
b_called = True
with pytest.raises(ValidationError):
ModelB(a='a')
assert not b_called
def test_assignment_validator_cls():
validator_calls = 0
class Model(BaseModel):
name: str
class Config:
validate_assignment = True
@validator('name')
def check_foo(cls, value):
nonlocal validator_calls
validator_calls += 1
assert cls == Model
return value
m = Model(name='hello')
m.name = 'goodbye'
assert validator_calls == 2
@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
def test_literal_validator():
class Model(BaseModel):
a: Literal['foo']
Model(a='foo')
with pytest.raises(ValidationError) as exc_info:
Model(a='nope')
assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': "unexpected value; permitted: 'foo'",
'type': 'value_error.const',
'ctx': {'given': 'nope', 'permitted': ('foo',)},
}
]
@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
def test_nested_literal_validator():
L1 = Literal['foo']
L2 = Literal['bar']
class Model(BaseModel):
a: Literal[L1, L2]
Model(a='foo')
with pytest.raises(ValidationError) as exc_info:
Model(a='nope')
assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': "unexpected value; permitted: 'foo', 'bar'",
'type': 'value_error.const',
'ctx': {'given': 'nope', 'permitted': ('foo', 'bar')},
}
]