mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
f55515820a
* renaming .json -> .model_dump_json * renaming .dict -> .model_dump * renaming .__fields__ -> .model_fields * renaming .schema -> .model_json_schema * renaming .construct -> .model_construct * renaming .parse_obj -> .model_validate * make linters happy * add changes md-file Co-authored-by: Samuel Colvin <s@muelcolvin.com>
1418 lines
39 KiB
Python
1418 lines
39 KiB
Python
from collections import deque
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
from itertools import product
|
|
from typing import Dict, List, Optional, Tuple, Union
|
|
|
|
import pytest
|
|
from typing_extensions import Literal
|
|
|
|
from pydantic import BaseModel, Extra, Field, PydanticUserError, ValidationError, errors, validator
|
|
from pydantic.validator_functions import root_validator
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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'}]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
@pytest.mark.parametrize('value', [2.2250738585072011e308, float('nan'), float('inf')])
|
|
def test_int_overflow_validation(value):
|
|
class Model(BaseModel):
|
|
a: int
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=value)
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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})
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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])
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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]']
|
|
|
|
|
|
@pytest.fixture(scope='session', name='ValidateAssignmentModel')
|
|
def validate_assignment_model_fixture():
|
|
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
|
|
|
|
return ValidateAssignmentModel
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_ok(ValidateAssignmentModel):
|
|
p = ValidateAssignmentModel(b='hello')
|
|
assert p.b == 'hello'
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_fail(ValidateAssignmentModel):
|
|
with pytest.raises(ValidationError):
|
|
ValidateAssignmentModel(a=10, b='hello')
|
|
|
|
p = ValidateAssignmentModel(b='hello')
|
|
with pytest.raises(ValidationError):
|
|
p.b = 'x'
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_value_change(ValidateAssignmentModel):
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_extra(ValidateAssignmentModel):
|
|
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'
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_dict(ValidateAssignmentModel):
|
|
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'}
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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').model_dump() == {'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'},
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_duplicates():
|
|
with pytest.raises(errors.PydanticUserError) 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.PydanticUserError) 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.PydanticUserError) 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)
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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').model_dump() == dict(a='abc', b=123)
|
|
assert calls == [('check_a', 'abc', 'a'), ('check_all', 'abc', 'a'), ('check_all', 123, 'b')]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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'},
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_invalid_field():
|
|
with pytest.raises(errors.PydanticUserError) 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)"
|
|
)
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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}
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validation_each_item_one_sublevel():
|
|
class Model(BaseModel):
|
|
foobar: List[Tuple[int, int]]
|
|
|
|
@validator('foobar', each_item=True)
|
|
def check_foobar(cls, v: Tuple[int, int]) -> Tuple[int, int]:
|
|
v1, v2 = v
|
|
assert v1 == v2
|
|
return v
|
|
|
|
assert Model(foobar=[(1, 1), (2, 2)]).foobar == [(1, 1), (2, 2)]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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}
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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'
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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_validator_bad_fields_throws_configerror():
|
|
"""
|
|
Attempts to create a validator with fields set as a list of strings,
|
|
rather than just multiple string args. Expects PydanticUserError to be raised.
|
|
"""
|
|
with pytest.raises(PydanticUserError, match='validator fields should be passed as separate string args.'):
|
|
|
|
class Model(BaseModel):
|
|
a: str
|
|
b: str
|
|
|
|
@validator(['a', 'b'])
|
|
def check_fields(cls, v):
|
|
return v
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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.xfail(reason='working on V2')
|
|
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'}
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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').model_dump() == {'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'},
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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').model_dump() == {'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'}]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_root_validator_repeat():
|
|
with pytest.raises(errors.PydanticUserError, 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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_root_validator_repeat2():
|
|
with pytest.raises(errors.PydanticUserError, 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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_root_validator_self():
|
|
with pytest.raises(
|
|
errors.PydanticUserError, 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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_root_validator_extra():
|
|
with pytest.raises(errors.PydanticUserError) 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).'
|
|
)
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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').model_dump() == {'a': 1, 'b': 'bar', 'c': 'wobble'}
|
|
|
|
assert root_val_values == (Model, {'a': 1, 'b': 'bar', 'c': 'wobble'})
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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).model_dump() == {'extra2': 2, 'extra1': 1, 'a': 123}
|
|
assert calls == ["parent validator: {'a': 123}", "child validator: {'extra1': 1, 'a': 123}"]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_root_validator_returns_none_exception():
|
|
class Model(BaseModel):
|
|
a: int = 1
|
|
|
|
@root_validator
|
|
def root_validator_repeated(cls, values):
|
|
return None
|
|
|
|
with pytest.raises(TypeError, match='Model values must be a dict'):
|
|
Model()
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def reusable_validator(num):
|
|
return num * 2
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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}
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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.validator_functions import _FUNCS
|
|
|
|
original_tracked_validators = set(_FUNCS)
|
|
yield
|
|
_FUNCS.clear()
|
|
_FUNCS.update(original_tracked_validators)
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
@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(PydanticUserError) 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.xfail(reason='working on V2')
|
|
@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').model_dump() == {'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'}]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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.xfail(reason='working on V2')
|
|
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.xfail(reason='working on V2')
|
|
def test_literal_validator_str_enum():
|
|
class Bar(str, Enum):
|
|
FIZ = 'fiz'
|
|
FUZ = 'fuz'
|
|
|
|
class Foo(BaseModel):
|
|
bar: Bar
|
|
barfiz: Literal[Bar.FIZ]
|
|
fizfuz: Literal[Bar.FIZ, Bar.FUZ]
|
|
|
|
my_foo = Foo.model_validate({'bar': 'fiz', 'barfiz': 'fiz', 'fizfuz': 'fiz'})
|
|
assert my_foo.bar is Bar.FIZ
|
|
assert my_foo.barfiz is Bar.FIZ
|
|
assert my_foo.fizfuz is Bar.FIZ
|
|
|
|
my_foo = Foo.model_validate({'bar': 'fiz', 'barfiz': 'fiz', 'fizfuz': 'fuz'})
|
|
assert my_foo.bar is Bar.FIZ
|
|
assert my_foo.barfiz is Bar.FIZ
|
|
assert my_foo.fizfuz is Bar.FUZ
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_union_literal_with_constraints():
|
|
class Model(BaseModel, validate_assignment=True):
|
|
x: Union[Literal[42], Literal['pika']] = Field(allow_mutation=False)
|
|
|
|
m = Model(x=42)
|
|
with pytest.raises(TypeError):
|
|
m.x += 1
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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')]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
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'
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_overridden_root_validators(mocker):
|
|
validate_stub = mocker.stub(name='validate')
|
|
|
|
class A(BaseModel):
|
|
x: str
|
|
|
|
@root_validator(pre=True)
|
|
def pre_root(cls, values):
|
|
validate_stub('A', 'pre')
|
|
return values
|
|
|
|
@root_validator(pre=False)
|
|
def post_root(cls, values):
|
|
validate_stub('A', 'post')
|
|
return values
|
|
|
|
class B(A):
|
|
@root_validator(pre=True)
|
|
def pre_root(cls, values):
|
|
validate_stub('B', 'pre')
|
|
return values
|
|
|
|
@root_validator(pre=False)
|
|
def post_root(cls, values):
|
|
validate_stub('B', 'post')
|
|
return values
|
|
|
|
A(x='pika')
|
|
assert validate_stub.call_args_list == [mocker.call('A', 'pre'), mocker.call('A', 'post')]
|
|
|
|
validate_stub.reset_mock()
|
|
|
|
B(x='pika')
|
|
assert validate_stub.call_args_list == [mocker.call('B', 'pre'), mocker.call('B', 'post')]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_pre_root_validator_fail():
|
|
class Model(BaseModel):
|
|
current_value: float = Field(..., alias='current')
|
|
max_value: float
|
|
|
|
class Config:
|
|
validate_assignment = True
|
|
|
|
@root_validator(pre=True)
|
|
def values_are_not_string(cls, values):
|
|
if any(isinstance(x, str) for x in values.values()):
|
|
raise ValueError('values cannot be a string')
|
|
return values
|
|
|
|
m = Model(current=100, max_value=200)
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
m.current_value = '100'
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'loc': ('__root__',),
|
|
'msg': 'values cannot be a string',
|
|
'type': 'value_error',
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_validating_assignment_post_root_validator_fail():
|
|
class Model(BaseModel):
|
|
current_value: float = Field(..., alias='current')
|
|
max_value: float
|
|
|
|
class Config:
|
|
validate_assignment = True
|
|
|
|
@root_validator
|
|
def current_lessequal_max(cls, values):
|
|
current_value = values.get('current_value')
|
|
max_value = values.get('max_value')
|
|
if current_value > max_value:
|
|
raise ValueError('current_value cannot be greater than max_value')
|
|
return values
|
|
|
|
@root_validator(skip_on_failure=True)
|
|
def current_lessequal_300(cls, values):
|
|
current_value = values.get('current_value')
|
|
if current_value > 300:
|
|
raise ValueError('current_value cannot be greater than 300')
|
|
return values
|
|
|
|
@root_validator
|
|
def current_lessequal_500(cls, values):
|
|
current_value = values.get('current_value')
|
|
if current_value > 500:
|
|
raise ValueError('current_value cannot be greater than 500')
|
|
return values
|
|
|
|
m = Model(current=100, max_value=200)
|
|
m.current_value = '100'
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
m.current_value = 1000
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('__root__',), 'msg': 'current_value cannot be greater than max_value', 'type': 'value_error'},
|
|
{
|
|
'loc': ('__root__',),
|
|
'msg': 'current_value cannot be greater than 500',
|
|
'type': 'value_error',
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.mark.xfail(reason='working on V2')
|
|
def test_root_validator_many_values_change():
|
|
"""It should run root_validator on assignment and update ALL concerned fields"""
|
|
|
|
class Rectangle(BaseModel):
|
|
width: float
|
|
height: float
|
|
area: float = None
|
|
|
|
class Config:
|
|
validate_assignment = True
|
|
|
|
@root_validator
|
|
def set_area(cls, values):
|
|
values['area'] = values['width'] * values['height']
|
|
return values
|
|
|
|
r = Rectangle(width=1, height=1)
|
|
assert r.area == 1
|
|
r.height = 5
|
|
assert r.area == 5
|