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, Field, 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_validating_assignment_values_dict(): class ModelOne(BaseModel): a: int class ModelTwo(BaseModel): m: ModelOne b: int @validator('b') def validate_b(cls, b, values): if 'm' in values: return b + values['m'].a # this fails if values['m'] is a dict else: return b class Config: validate_assignment = True model = ModelTwo(m=ModelOne(a=1), b=2) assert model.b == 3 model.b = 3 assert model.b == 4 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..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')}, } ] def test_field_that_is_being_validated_is_excluded_from_validator_values(mocker): check_values = mocker.MagicMock() class Model(BaseModel): foo: str bar: str = Field(alias='pika') baz: str class Config: validate_assignment = True @validator('foo') def validate_foo(cls, v, values): check_values({**values}) return v @validator('bar') def validate_bar(cls, v, values): check_values({**values}) return v model = Model(foo='foo_value', pika='bar_value', baz='baz_value') check_values.reset_mock() assert list(dict(model).items()) == [('foo', 'foo_value'), ('bar', 'bar_value'), ('baz', 'baz_value')] model.foo = 'new_foo_value' check_values.assert_called_once_with({'bar': 'bar_value', 'baz': 'baz_value'}) check_values.reset_mock() model.bar = 'new_bar_value' check_values.assert_called_once_with({'foo': 'new_foo_value', 'baz': 'baz_value'}) # ensure field order is the same assert list(dict(model).items()) == [('foo', 'new_foo_value'), ('bar', 'new_bar_value'), ('baz', 'baz_value')] def test_exceptions_in_field_validators_restore_original_field_value(): class Model(BaseModel): foo: str class Config: validate_assignment = True @validator('foo') def validate_foo(cls, v): if v == 'raise_exception': raise RuntimeError('test error') return v model = Model(foo='foo') with pytest.raises(RuntimeError, match='test error'): model.foo = 'raise_exception' assert model.foo == 'foo'