mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
6b5adcc977
* Rename skip-defaults, and add ability to exclude actual defaults * Add __defaults__ and deprecation warnings * Add note about `skip_defaults` to docs * Incorporate feedback * Add tests and changes * Fix reference to .json()
1215 lines
33 KiB
Python
1215 lines
33 KiB
Python
import re
|
|
import sys
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Optional, Set, Tuple, Type, Union
|
|
|
|
import pytest
|
|
|
|
from pydantic import (
|
|
BaseConfig,
|
|
BaseModel,
|
|
BaseSettings,
|
|
Extra,
|
|
NoneStrBytes,
|
|
StrBytes,
|
|
ValidationError,
|
|
constr,
|
|
errors,
|
|
validate_model,
|
|
validator,
|
|
)
|
|
from pydantic.fields import Field, Schema
|
|
|
|
|
|
def test_str_bytes():
|
|
class Model(BaseModel):
|
|
v: StrBytes = ...
|
|
|
|
m = Model(v='s')
|
|
assert m.v == 's'
|
|
assert repr(m.__fields__['v']) == "ModelField(name='v', type=Union[str, bytes], required=True)"
|
|
|
|
m = Model(v=b'b')
|
|
assert m.v == 'b'
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=None)
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v',), 'msg': 'none is not an allowed value', 'type': 'type_error.none.not_allowed'}
|
|
]
|
|
|
|
|
|
def test_str_bytes_none():
|
|
class Model(BaseModel):
|
|
v: NoneStrBytes = ...
|
|
|
|
m = Model(v='s')
|
|
assert m.v == 's'
|
|
|
|
m = Model(v=b'b')
|
|
assert m.v == 'b'
|
|
|
|
m = Model(v=None)
|
|
assert m.v is None
|
|
|
|
|
|
def test_union_int_str():
|
|
class Model(BaseModel):
|
|
v: Union[int, str] = ...
|
|
|
|
m = Model(v=123)
|
|
assert m.v == 123
|
|
|
|
m = Model(v='123')
|
|
assert m.v == 123
|
|
|
|
m = Model(v=b'foobar')
|
|
assert m.v == 'foobar'
|
|
|
|
# here both validators work and it's impossible to work out which value "closer"
|
|
m = Model(v=12.2)
|
|
assert m.v == 12
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=None)
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v',), 'msg': 'none is not an allowed value', 'type': 'type_error.none.not_allowed'}
|
|
]
|
|
|
|
|
|
def test_union_priority():
|
|
class ModelOne(BaseModel):
|
|
v: Union[int, str] = ...
|
|
|
|
class ModelTwo(BaseModel):
|
|
v: Union[str, int] = ...
|
|
|
|
assert ModelOne(v='123').v == 123
|
|
assert ModelTwo(v='123').v == '123'
|
|
|
|
|
|
def test_typed_list():
|
|
class Model(BaseModel):
|
|
v: List[int] = ...
|
|
|
|
m = Model(v=[1, 2, '3'])
|
|
assert m.v == [1, 2, 3]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[1, 'x', 'y'])
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v', 1), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('v', 2), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=1)
|
|
assert exc_info.value.errors() == [{'loc': ('v',), 'msg': 'value is not a valid list', 'type': 'type_error.list'}]
|
|
|
|
|
|
def test_typed_set():
|
|
class Model(BaseModel):
|
|
v: Set[int] = ...
|
|
|
|
assert Model(v={1, 2, '3'}).v == {1, 2, 3}
|
|
assert Model(v=[1, 2, '3']).v == {1, 2, 3}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[1, 'x'])
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v', 1), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
|
|
|
|
def test_dict_dict():
|
|
class Model(BaseModel):
|
|
v: Dict[str, int] = ...
|
|
|
|
assert Model(v={'foo': 1}).dict() == {'v': {'foo': 1}}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
[
|
|
({'a': 2, 'b': 4}, {'a': 2, 'b': 4}),
|
|
({1: '2', 'b': 4}, {'1': 2, 'b': 4}),
|
|
([('a', 2), ('b', 4)], {'a': 2, 'b': 4}),
|
|
],
|
|
)
|
|
def test_typed_dict(value, result):
|
|
class Model(BaseModel):
|
|
v: Dict[str, int] = ...
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,errors',
|
|
[
|
|
(1, [{'loc': ('v',), 'msg': 'value is not a valid dict', 'type': 'type_error.dict'}]),
|
|
({'a': 'b'}, [{'loc': ('v', 'a'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}]),
|
|
([1, 2, 3], [{'loc': ('v',), 'msg': 'value is not a valid dict', 'type': 'type_error.dict'}]),
|
|
],
|
|
)
|
|
def test_typed_dict_error(value, errors):
|
|
class Model(BaseModel):
|
|
v: Dict[str, int] = ...
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
assert exc_info.value.errors() == errors
|
|
|
|
|
|
def test_dict_key_error():
|
|
class Model(BaseModel):
|
|
v: Dict[int, int] = ...
|
|
|
|
assert Model(v={1: 2, '3': '4'}).v == {1: 2, 3: 4}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v={'foo': 2, '3': '4'})
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v', '__key__'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
|
|
|
|
def test_tuple():
|
|
class Model(BaseModel):
|
|
v: Tuple[int, float, bool]
|
|
|
|
m = Model(v=[1.2, '2.2', 'true'])
|
|
assert m.v == (1, 2.2, True)
|
|
|
|
|
|
def test_tuple_more():
|
|
class Model(BaseModel):
|
|
simple_tuple: tuple = None
|
|
tuple_of_different_types: Tuple[int, float, str, bool] = None
|
|
|
|
m = Model(simple_tuple=[1, 2, 3, 4], tuple_of_different_types=[4, 3, 2, 1])
|
|
assert m.dict() == {'simple_tuple': (1, 2, 3, 4), 'tuple_of_different_types': (4, 3.0, '2', True)}
|
|
|
|
|
|
def test_tuple_length_error():
|
|
class Model(BaseModel):
|
|
v: Tuple[int, float, bool]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[1, 2])
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'loc': ('v',),
|
|
'msg': 'wrong tuple length 2, expected 3',
|
|
'type': 'value_error.tuple.length',
|
|
'ctx': {'actual_length': 2, 'expected_length': 3},
|
|
}
|
|
]
|
|
|
|
|
|
def test_tuple_invalid():
|
|
class Model(BaseModel):
|
|
v: Tuple[int, float, bool]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v='xxx')
|
|
assert exc_info.value.errors() == [{'loc': ('v',), 'msg': 'value is not a valid tuple', 'type': 'type_error.tuple'}]
|
|
|
|
|
|
def test_tuple_value_error():
|
|
class Model(BaseModel):
|
|
v: Tuple[int, float, Decimal]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=['x', 'y', 'x'])
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v', 0), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('v', 1), 'msg': 'value is not a valid float', 'type': 'type_error.float'},
|
|
{'loc': ('v', 2), 'msg': 'value is not a valid decimal', 'type': 'type_error.decimal'},
|
|
]
|
|
|
|
|
|
def test_recursive_list():
|
|
class SubModel(BaseModel):
|
|
name: str = ...
|
|
count: int = None
|
|
|
|
class Model(BaseModel):
|
|
v: List[SubModel] = []
|
|
|
|
m = Model(v=[])
|
|
assert m.v == []
|
|
|
|
m = Model(v=[{'name': 'testing', 'count': 4}])
|
|
assert repr(m) == "Model(v=[SubModel(name='testing', count=4)])"
|
|
assert m.v[0].name == 'testing'
|
|
assert m.v[0].count == 4
|
|
assert m.dict() == {'v': [{'count': 4, 'name': 'testing'}]}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=['x'])
|
|
assert exc_info.value.errors() == [{'loc': ('v', 0), 'msg': 'value is not a valid dict', 'type': 'type_error.dict'}]
|
|
|
|
|
|
def test_recursive_list_error():
|
|
class SubModel(BaseModel):
|
|
name: str = ...
|
|
count: int = None
|
|
|
|
class Model(BaseModel):
|
|
v: List[SubModel] = []
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[{}])
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v', 0, 'name'), 'msg': 'field required', 'type': 'value_error.missing'}
|
|
]
|
|
|
|
|
|
def test_list_unions():
|
|
class Model(BaseModel):
|
|
v: List[Union[int, str]] = ...
|
|
|
|
assert Model(v=[123, '456', 'foobar']).v == [123, 456, 'foobar']
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[1, 2, None])
|
|
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('v', 2), 'msg': 'none is not an allowed value', 'type': 'type_error.none.not_allowed'}
|
|
]
|
|
|
|
|
|
def test_recursive_lists():
|
|
class Model(BaseModel):
|
|
v: List[List[Union[int, float]]] = ...
|
|
|
|
assert Model(v=[[1, 2], [3, '4', '4.1']]).v == [[1, 2], [3, 4, 4.1]]
|
|
assert Model.__fields__['v'].sub_fields[0].name == '_v'
|
|
assert len(Model.__fields__['v'].sub_fields) == 1
|
|
assert Model.__fields__['v'].sub_fields[0].sub_fields[0].name == '__v'
|
|
assert len(Model.__fields__['v'].sub_fields[0].sub_fields) == 1
|
|
assert Model.__fields__['v'].sub_fields[0].sub_fields[0].sub_fields[1].name == '__v_float'
|
|
assert len(Model.__fields__['v'].sub_fields[0].sub_fields[0].sub_fields) == 2
|
|
|
|
|
|
class StrEnum(str, Enum):
|
|
a = 'a10'
|
|
b = 'b10'
|
|
|
|
|
|
def test_str_enum():
|
|
class Model(BaseModel):
|
|
v: StrEnum = ...
|
|
|
|
assert Model(v='a10').v is StrEnum.a
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v='different')
|
|
|
|
|
|
def test_any_dict():
|
|
class Model(BaseModel):
|
|
v: Dict[int, Any] = ...
|
|
|
|
assert Model(v={1: 'foobar'}).dict() == {'v': {1: 'foobar'}}
|
|
assert Model(v={123: 456}).dict() == {'v': {123: 456}}
|
|
assert Model(v={2: [1, 2, 3]}).dict() == {'v': {2: [1, 2, 3]}}
|
|
|
|
|
|
def test_infer_alias():
|
|
class Model(BaseModel):
|
|
a = 'foobar'
|
|
|
|
class Config:
|
|
fields = {'a': '_a'}
|
|
|
|
assert Model(_a='different').a == 'different'
|
|
assert repr(Model.__fields__['a']) == (
|
|
"ModelField(name='a', type=str, required=False, default='foobar', alias='_a')"
|
|
)
|
|
|
|
|
|
def test_alias_error():
|
|
class Model(BaseModel):
|
|
a = 123
|
|
|
|
class Config:
|
|
fields = {'a': '_a'}
|
|
|
|
assert Model(_a='123').a == 123
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(_a='foo')
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('_a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
|
|
|
|
def test_annotation_config():
|
|
class Model(BaseModel):
|
|
b: float
|
|
a: int = 10
|
|
_c: str
|
|
|
|
class Config:
|
|
fields = {'b': 'foobar'}
|
|
|
|
assert list(Model.__fields__.keys()) == ['b', 'a']
|
|
assert [f.alias for f in Model.__fields__.values()] == ['foobar', 'a']
|
|
assert Model(foobar='123').b == 123.0
|
|
|
|
|
|
def test_success_values_include():
|
|
class Model(BaseModel):
|
|
a: int = 1
|
|
b: int = 2
|
|
c: int = 3
|
|
|
|
m = Model()
|
|
assert m.dict() == {'a': 1, 'b': 2, 'c': 3}
|
|
assert m.dict(include={'a'}) == {'a': 1}
|
|
assert m.dict(exclude={'a'}) == {'b': 2, 'c': 3}
|
|
assert m.dict(include={'a', 'b'}, exclude={'a'}) == {'b': 2}
|
|
|
|
|
|
def test_include_exclude_unset():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: int
|
|
c: int = 3
|
|
d: int = 4
|
|
e: int = 5
|
|
f: int = 6
|
|
|
|
m = Model(a=1, b=2, e=5, f=7)
|
|
assert m.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 7}
|
|
assert m.__fields_set__ == {'a', 'b', 'e', 'f'}
|
|
assert m.dict(exclude_unset=True) == {'a': 1, 'b': 2, 'e': 5, 'f': 7}
|
|
|
|
assert m.dict(include={'a'}, exclude_unset=True) == {'a': 1}
|
|
assert m.dict(include={'c'}, exclude_unset=True) == {}
|
|
|
|
assert m.dict(exclude={'a'}, exclude_unset=True) == {'b': 2, 'e': 5, 'f': 7}
|
|
assert m.dict(exclude={'c'}, exclude_unset=True) == {'a': 1, 'b': 2, 'e': 5, 'f': 7}
|
|
|
|
assert m.dict(include={'a', 'b', 'c'}, exclude={'b'}, exclude_unset=True) == {'a': 1}
|
|
assert m.dict(include={'a', 'b', 'c'}, exclude={'a', 'c'}, exclude_unset=True) == {'b': 2}
|
|
|
|
|
|
def test_include_exclude_defaults():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: int
|
|
c: int = 3
|
|
d: int = 4
|
|
e: int = 5
|
|
f: int = 6
|
|
|
|
m = Model(a=1, b=2, e=5, f=7)
|
|
assert m.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 7}
|
|
assert m.__fields_set__ == {'a', 'b', 'e', 'f'}
|
|
assert m.dict(exclude_defaults=True) == {'a': 1, 'b': 2, 'f': 7}
|
|
|
|
assert m.dict(include={'a'}, exclude_defaults=True) == {'a': 1}
|
|
assert m.dict(include={'c'}, exclude_defaults=True) == {}
|
|
|
|
assert m.dict(exclude={'a'}, exclude_defaults=True) == {'b': 2, 'f': 7}
|
|
assert m.dict(exclude={'c'}, exclude_defaults=True) == {'a': 1, 'b': 2, 'f': 7}
|
|
|
|
assert m.dict(include={'a', 'b', 'c'}, exclude={'b'}, exclude_defaults=True) == {'a': 1}
|
|
assert m.dict(include={'a', 'b', 'c'}, exclude={'a', 'c'}, exclude_defaults=True) == {'b': 2}
|
|
|
|
|
|
def test_skip_defaults_deprecated():
|
|
class Model(BaseModel):
|
|
x: int
|
|
|
|
m = Model(x=1)
|
|
match = r'Model.dict\(\): "skip_defaults" is deprecated and replaced by "exclude_unset"'
|
|
with pytest.warns(DeprecationWarning, match=match):
|
|
assert m.dict(skip_defaults=True)
|
|
with pytest.warns(DeprecationWarning, match=match):
|
|
assert m.dict(skip_defaults=False)
|
|
|
|
match = r'Model.json\(\): "skip_defaults" is deprecated and replaced by "exclude_unset"'
|
|
with pytest.warns(DeprecationWarning, match=match):
|
|
assert m.json(skip_defaults=True)
|
|
with pytest.warns(DeprecationWarning, match=match):
|
|
assert m.json(skip_defaults=False)
|
|
|
|
|
|
def test_advanced_exclude():
|
|
class SubSubModel(BaseModel):
|
|
a: str
|
|
b: str
|
|
|
|
class SubModel(BaseModel):
|
|
c: str
|
|
d: List[SubSubModel]
|
|
|
|
class Model(BaseModel):
|
|
e: str
|
|
f: SubModel
|
|
|
|
m = Model(e='e', f=SubModel(c='foo', d=[SubSubModel(a='a', b='b'), SubSubModel(a='c', b='e')]))
|
|
|
|
assert m.dict(exclude={'f': {'c': ..., 'd': {-1: {'a'}}}}) == {
|
|
'e': 'e',
|
|
'f': {'d': [{'a': 'a', 'b': 'b'}, {'b': 'e'}]},
|
|
}
|
|
assert m.dict(exclude={'e': ..., 'f': {'d'}}) == {'f': {'c': 'foo'}}
|
|
|
|
|
|
def test_advanced_value_inclide():
|
|
class SubSubModel(BaseModel):
|
|
a: str
|
|
b: str
|
|
|
|
class SubModel(BaseModel):
|
|
c: str
|
|
d: List[SubSubModel]
|
|
|
|
class Model(BaseModel):
|
|
e: str
|
|
f: SubModel
|
|
|
|
m = Model(e='e', f=SubModel(c='foo', d=[SubSubModel(a='a', b='b'), SubSubModel(a='c', b='e')]))
|
|
|
|
assert m.dict(include={'f'}) == {'f': {'c': 'foo', 'd': [{'a': 'a', 'b': 'b'}, {'a': 'c', 'b': 'e'}]}}
|
|
assert m.dict(include={'e'}) == {'e': 'e'}
|
|
assert m.dict(include={'f': {'d': {0: ..., -1: {'b'}}}}) == {'f': {'d': [{'a': 'a', 'b': 'b'}, {'b': 'e'}]}}
|
|
|
|
|
|
def test_advanced_value_exclude_include():
|
|
class SubSubModel(BaseModel):
|
|
a: str
|
|
b: str
|
|
|
|
class SubModel(BaseModel):
|
|
c: str
|
|
d: List[SubSubModel]
|
|
|
|
class Model(BaseModel):
|
|
e: str
|
|
f: SubModel
|
|
|
|
m = Model(e='e', f=SubModel(c='foo', d=[SubSubModel(a='a', b='b'), SubSubModel(a='c', b='e')]))
|
|
|
|
assert m.dict(exclude={'f': {'c': ..., 'd': {-1: {'a'}}}}, include={'f'}) == {
|
|
'f': {'d': [{'a': 'a', 'b': 'b'}, {'b': 'e'}]}
|
|
}
|
|
assert m.dict(exclude={'e': ..., 'f': {'d'}}, include={'e', 'f'}) == {'f': {'c': 'foo'}}
|
|
|
|
assert m.dict(exclude={'f': {'d': {-1: {'a'}}}}, include={'f': {'d'}}) == {
|
|
'f': {'d': [{'a': 'a', 'b': 'b'}, {'b': 'e'}]}
|
|
}
|
|
|
|
|
|
def test_field_set_ignore_extra():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: int
|
|
c: int = 3
|
|
|
|
class Config:
|
|
extra = Extra.ignore
|
|
|
|
m = Model(a=1, b=2)
|
|
assert m.dict() == {'a': 1, 'b': 2, 'c': 3}
|
|
assert m.__fields_set__ == {'a', 'b'}
|
|
assert m.dict(exclude_unset=True) == {'a': 1, 'b': 2}
|
|
|
|
m2 = Model(a=1, b=2, d=4)
|
|
assert m2.dict() == {'a': 1, 'b': 2, 'c': 3}
|
|
assert m2.__fields_set__ == {'a', 'b'}
|
|
assert m2.dict(exclude_unset=True) == {'a': 1, 'b': 2}
|
|
|
|
|
|
def test_field_set_allow_extra():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: int
|
|
c: int = 3
|
|
|
|
class Config:
|
|
extra = Extra.allow
|
|
|
|
m = Model(a=1, b=2)
|
|
assert m.dict() == {'a': 1, 'b': 2, 'c': 3}
|
|
assert m.__fields_set__ == {'a', 'b'}
|
|
assert m.dict(exclude_unset=True) == {'a': 1, 'b': 2}
|
|
|
|
m2 = Model(a=1, b=2, d=4)
|
|
assert m2.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4}
|
|
assert m2.__fields_set__ == {'a', 'b', 'd'}
|
|
assert m2.dict(exclude_unset=True) == {'a': 1, 'b': 2, 'd': 4}
|
|
|
|
|
|
def test_field_set_field_name():
|
|
class Model(BaseModel):
|
|
a: int
|
|
field_set: int
|
|
b: int = 3
|
|
|
|
assert Model(a=1, field_set=2).dict() == {'a': 1, 'field_set': 2, 'b': 3}
|
|
assert Model(a=1, field_set=2).dict(exclude_unset=True) == {'a': 1, 'field_set': 2}
|
|
assert Model.construct(dict(a=1, field_set=3), {'a', 'field_set'}).dict() == {'a': 1, 'field_set': 3}
|
|
|
|
|
|
def test_values_order():
|
|
class Model(BaseModel):
|
|
a: int = 1
|
|
b: int = 2
|
|
c: int = 3
|
|
|
|
m = Model(c=30, b=20, a=10)
|
|
assert list(m) == [('a', 10), ('b', 20), ('c', 30)]
|
|
|
|
|
|
def test_inheritance():
|
|
class Foo(BaseModel):
|
|
a: float = ...
|
|
|
|
class Bar(Foo):
|
|
x: float = 12.3
|
|
a = 123.0
|
|
|
|
assert Bar().dict() == {'x': 12.3, 'a': 123.0}
|
|
|
|
|
|
def test_invalid_type():
|
|
with pytest.raises(RuntimeError) as exc_info:
|
|
|
|
class Model(BaseModel):
|
|
x: 43 = 123
|
|
|
|
assert 'error checking inheritance of 43 (type: int)' in exc_info.value.args[0]
|
|
|
|
|
|
class CustomStr(str):
|
|
def foobar(self):
|
|
return 7
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,expected',
|
|
[
|
|
('a string', 'a string'),
|
|
(b'some bytes', 'some bytes'),
|
|
(bytearray('foobar', encoding='utf8'), 'foobar'),
|
|
(123, '123'),
|
|
(123.45, '123.45'),
|
|
(Decimal('12.45'), '12.45'),
|
|
(True, 'True'),
|
|
(False, 'False'),
|
|
(StrEnum.a, 'a10'),
|
|
(CustomStr('whatever'), 'whatever'),
|
|
],
|
|
)
|
|
def test_valid_string_types(value, expected):
|
|
class Model(BaseModel):
|
|
v: str
|
|
|
|
assert Model(v=value).v == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,errors',
|
|
[
|
|
({'foo': 'bar'}, [{'loc': ('v',), 'msg': 'str type expected', 'type': 'type_error.str'}]),
|
|
([1, 2, 3], [{'loc': ('v',), 'msg': 'str type expected', 'type': 'type_error.str'}]),
|
|
],
|
|
)
|
|
def test_invalid_string_types(value, errors):
|
|
class Model(BaseModel):
|
|
v: str
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
assert exc_info.value.errors() == errors
|
|
|
|
|
|
def test_inheritance_config():
|
|
class Parent(BaseModel):
|
|
a: int
|
|
|
|
class Child(Parent):
|
|
b: str
|
|
|
|
class Config:
|
|
fields = {'a': 'aaa', 'b': 'bbb'}
|
|
|
|
m = Child(aaa=1, bbb='s')
|
|
assert repr(m) == "Child(a=1, b='s')"
|
|
|
|
|
|
def test_partial_inheritance_config():
|
|
class Parent(BaseModel):
|
|
a: int
|
|
|
|
class Config:
|
|
fields = {'a': 'aaa'}
|
|
|
|
class Child(Parent):
|
|
b: str
|
|
|
|
class Config:
|
|
fields = {'b': 'bbb'}
|
|
|
|
m = Child(aaa=1, bbb='s')
|
|
assert repr(m) == "Child(a=1, b='s')"
|
|
|
|
|
|
def test_annotation_inheritance():
|
|
class A(BaseModel):
|
|
integer: int = 1
|
|
|
|
class B(A):
|
|
integer = 2
|
|
|
|
assert B.__annotations__['integer'] == int
|
|
assert B.__fields__['integer'].type_ == int
|
|
|
|
class C(A):
|
|
integer: str = 'G'
|
|
|
|
assert C.__annotations__['integer'] == str
|
|
assert C.__fields__['integer'].type_ == str
|
|
|
|
with pytest.raises(TypeError) as exc_info:
|
|
|
|
class D(A):
|
|
integer = 'G'
|
|
|
|
assert str(exc_info.value) == (
|
|
'The type of D.integer differs from the new default value; '
|
|
'if you wish to change the type of this field, please use a type annotation'
|
|
)
|
|
|
|
|
|
def test_string_none():
|
|
class Model(BaseModel):
|
|
a: constr(min_length=20, max_length=1000) = ...
|
|
|
|
class Config:
|
|
extra = Extra.ignore
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=None)
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('a',), 'msg': 'none is not an allowed value', 'type': 'type_error.none.not_allowed'}
|
|
]
|
|
|
|
|
|
def test_alias_camel_case():
|
|
class Model(BaseModel):
|
|
one_thing: int
|
|
another_thing: int
|
|
|
|
class Config(BaseConfig):
|
|
@classmethod
|
|
def get_field_info(cls, name):
|
|
field_config = super().get_field_info(name) or {}
|
|
if 'alias' not in field_config:
|
|
field_config['alias'] = re.sub(r'(?:^|_)([a-z])', lambda m: m.group(1).upper(), name)
|
|
return field_config
|
|
|
|
v = Model(**{'OneThing': 123, 'AnotherThing': '321'})
|
|
assert v.one_thing == 123
|
|
assert v.another_thing == 321
|
|
assert v == {'one_thing': 123, 'another_thing': 321}
|
|
|
|
|
|
def test_get_field_info_inherit():
|
|
class ModelOne(BaseModel):
|
|
class Config(BaseConfig):
|
|
@classmethod
|
|
def get_field_info(cls, name):
|
|
field_config = super().get_field_info(name) or {}
|
|
if 'alias' not in field_config:
|
|
field_config['alias'] = re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
|
return field_config
|
|
|
|
class ModelTwo(ModelOne):
|
|
one_thing: int
|
|
another_thing: int
|
|
third_thing: int
|
|
|
|
class Config:
|
|
fields = {'third_thing': 'Banana'}
|
|
|
|
v = ModelTwo(**{'oneThing': 123, 'anotherThing': '321', 'Banana': 1})
|
|
assert v == {'one_thing': 123, 'another_thing': 321, 'third_thing': 1}
|
|
|
|
|
|
def test_return_errors_ok():
|
|
class Model(BaseModel):
|
|
foo: int
|
|
bar: List[int]
|
|
|
|
assert validate_model(Model, {'foo': '123', 'bar': (1, 2, 3)}) == (
|
|
{'foo': 123, 'bar': [1, 2, 3]},
|
|
{'foo', 'bar'},
|
|
None,
|
|
)
|
|
d, f, e = validate_model(Model, {'foo': '123', 'bar': (1, 2, 3)}, False)
|
|
assert d == {'foo': 123, 'bar': [1, 2, 3]}
|
|
assert f == {'foo', 'bar'}
|
|
assert e is None
|
|
|
|
|
|
def test_return_errors_error():
|
|
class Model(BaseModel):
|
|
foo: int
|
|
bar: List[int]
|
|
|
|
d, f, e = validate_model(Model, {'foo': '123', 'bar': (1, 2, 'x')}, False)
|
|
assert d == {'foo': 123}
|
|
assert f == {'foo', 'bar'}
|
|
assert e.errors() == [{'loc': ('bar', 2), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}]
|
|
|
|
d, f, e = validate_model(Model, {'bar': (1, 2, 3)}, False)
|
|
assert d == {'bar': [1, 2, 3]}
|
|
assert f == {'bar'}
|
|
assert e.errors() == [{'loc': ('foo',), 'msg': 'field required', 'type': 'value_error.missing'}]
|
|
|
|
|
|
def test_optional_required():
|
|
class Model(BaseModel):
|
|
bar: Optional[int]
|
|
|
|
assert Model(bar=123).dict() == {'bar': 123}
|
|
assert Model().dict() == {'bar': None}
|
|
assert Model(bar=None).dict() == {'bar': None}
|
|
|
|
|
|
def test_invalid_validator():
|
|
class InvalidValidator:
|
|
@classmethod
|
|
def __get_validators__(cls):
|
|
yield cls.has_wrong_arguments
|
|
|
|
@classmethod
|
|
def has_wrong_arguments(cls, value, bar):
|
|
pass
|
|
|
|
with pytest.raises(errors.ConfigError) as exc_info:
|
|
|
|
class InvalidValidatorModel(BaseModel):
|
|
x: InvalidValidator = ...
|
|
|
|
assert exc_info.value.args[0].startswith('Invalid signature for validator')
|
|
|
|
|
|
def test_unable_to_infer():
|
|
with pytest.raises(errors.ConfigError) as exc_info:
|
|
|
|
class InvalidDefinitionModel(BaseModel):
|
|
x = None
|
|
|
|
assert exc_info.value.args[0] == 'unable to infer type for attribute "x"'
|
|
|
|
|
|
def test_multiple_errors():
|
|
class Model(BaseModel):
|
|
a: Union[None, int, float, Decimal]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a='foobar')
|
|
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('a',), 'msg': 'value is not a valid float', 'type': 'type_error.float'},
|
|
{'loc': ('a',), 'msg': 'value is not a valid decimal', 'type': 'type_error.decimal'},
|
|
]
|
|
assert Model().a is None
|
|
assert Model(a=None).a is None
|
|
|
|
|
|
def test_pop_by_alias():
|
|
class Model(BaseModel):
|
|
last_updated_by: Optional[str] = None
|
|
|
|
class Config:
|
|
extra = Extra.forbid
|
|
allow_population_by_field_name = True
|
|
fields = {'last_updated_by': 'lastUpdatedBy'}
|
|
|
|
assert Model(lastUpdatedBy='foo').dict() == {'last_updated_by': 'foo'}
|
|
assert Model(last_updated_by='foo').dict() == {'last_updated_by': 'foo'}
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(lastUpdatedBy='foo', last_updated_by='bar')
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('last_updated_by',), 'msg': 'extra fields not permitted', 'type': 'value_error.extra'}
|
|
]
|
|
|
|
|
|
def test_validate_all():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: int
|
|
|
|
class Config:
|
|
validate_all = True
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model()
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'},
|
|
{'loc': ('b',), 'msg': 'field required', 'type': 'value_error.missing'},
|
|
]
|
|
|
|
|
|
def test_force_extra():
|
|
class Model(BaseModel):
|
|
foo: int
|
|
|
|
class Config:
|
|
extra = 'ignore'
|
|
|
|
assert Model.__config__.extra is Extra.ignore
|
|
|
|
|
|
def test_illegal_extra_value():
|
|
with pytest.raises(ValueError, match='is not a valid value for "extra"'):
|
|
|
|
class Model(BaseModel):
|
|
foo: int
|
|
|
|
class Config:
|
|
extra = 'foo'
|
|
|
|
|
|
def test_multiple_inheritance_config():
|
|
class Parent(BaseModel):
|
|
class Config:
|
|
allow_mutation = False
|
|
extra = Extra.forbid
|
|
|
|
class Mixin(BaseModel):
|
|
class Config:
|
|
use_enum_values = True
|
|
|
|
class Child(Mixin, Parent):
|
|
class Config:
|
|
allow_population_by_field_name = True
|
|
|
|
assert BaseModel.__config__.allow_mutation is True
|
|
assert BaseModel.__config__.allow_population_by_field_name is False
|
|
assert BaseModel.__config__.extra is Extra.ignore
|
|
assert BaseModel.__config__.use_enum_values is False
|
|
|
|
assert Parent.__config__.allow_mutation is False
|
|
assert Parent.__config__.allow_population_by_field_name is False
|
|
assert Parent.__config__.extra is Extra.forbid
|
|
assert Parent.__config__.use_enum_values is False
|
|
|
|
assert Mixin.__config__.allow_mutation is True
|
|
assert Mixin.__config__.allow_population_by_field_name is False
|
|
assert Mixin.__config__.extra is Extra.ignore
|
|
assert Mixin.__config__.use_enum_values is True
|
|
|
|
assert Child.__config__.allow_mutation is False
|
|
assert Child.__config__.allow_population_by_field_name is True
|
|
assert Child.__config__.extra is Extra.forbid
|
|
assert Child.__config__.use_enum_values is True
|
|
|
|
|
|
def test_submodel_different_type():
|
|
class Foo(BaseModel):
|
|
a: int
|
|
|
|
class Bar(BaseModel):
|
|
b: int
|
|
|
|
class Spam(BaseModel):
|
|
c: Foo
|
|
|
|
assert Spam(c={'a': '123'}).dict() == {'c': {'a': 123}}
|
|
with pytest.raises(ValidationError):
|
|
Spam(c={'b': '123'})
|
|
|
|
assert Spam(c=Foo(a='123')).dict() == {'c': {'a': 123}}
|
|
with pytest.raises(ValidationError):
|
|
Spam(c=Bar(b='123'))
|
|
|
|
|
|
def test_self():
|
|
class Model(BaseModel):
|
|
self: str
|
|
|
|
m = Model.parse_obj(dict(self='some value'))
|
|
assert m.dict() == {'self': 'some value'}
|
|
assert m.self == 'some value'
|
|
assert m.schema() == {
|
|
'title': 'Model',
|
|
'type': 'object',
|
|
'properties': {'self': {'title': 'Self', 'type': 'string'}},
|
|
'required': ['self'],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize('model', [BaseModel, BaseSettings])
|
|
def test_self_recursive(model):
|
|
class SubModel(model):
|
|
self: int
|
|
|
|
class Model(model):
|
|
sm: SubModel
|
|
|
|
m = Model.parse_obj({'sm': {'self': '123'}})
|
|
assert m.dict() == {'sm': {'self': 123}}
|
|
|
|
|
|
@pytest.mark.parametrize('model', [BaseModel, BaseSettings])
|
|
def test_nested_init(model):
|
|
class NestedModel(model):
|
|
self: str
|
|
modified_number: int = 1
|
|
|
|
def __init__(someinit, **kwargs):
|
|
super().__init__(**kwargs)
|
|
someinit.modified_number += 1
|
|
|
|
class TopModel(model):
|
|
self: str
|
|
nest: NestedModel
|
|
|
|
m = TopModel.parse_obj(dict(self='Top Model', nest=dict(self='Nested Model', modified_number=0)))
|
|
assert m.self == 'Top Model'
|
|
assert m.nest.self == 'Nested Model'
|
|
assert m.nest.modified_number == 1
|
|
|
|
|
|
def test_values_attr_deprecation():
|
|
class Model(BaseModel):
|
|
foo: int
|
|
bar: str
|
|
|
|
m = Model(foo=4, bar='baz')
|
|
with pytest.warns(DeprecationWarning, match='`__values__` attribute is deprecated, use `__dict__` instead'):
|
|
assert m.__values__ == m.__dict__
|
|
|
|
|
|
def test_init_inspection():
|
|
class Foobar(BaseModel):
|
|
x: int
|
|
|
|
def __init__(self, **data) -> None:
|
|
with pytest.raises(AttributeError):
|
|
assert self.x
|
|
super().__init__(**data)
|
|
|
|
Foobar(x=1)
|
|
|
|
|
|
def test_type_on_annotation():
|
|
class FooBar:
|
|
pass
|
|
|
|
class Model(BaseModel):
|
|
a: int = int
|
|
b: Type[int]
|
|
c: Type[int] = int
|
|
d: FooBar = FooBar
|
|
e: Type[FooBar]
|
|
f: Type[FooBar] = FooBar
|
|
|
|
assert Model.__fields__.keys() == {'b', 'c', 'e', 'f'}
|
|
|
|
|
|
def test_assign_type():
|
|
class Parent:
|
|
def echo(self):
|
|
return 'parent'
|
|
|
|
class Child(Parent):
|
|
def echo(self):
|
|
return 'child'
|
|
|
|
class Different:
|
|
def echo(self):
|
|
return 'different'
|
|
|
|
class Model(BaseModel):
|
|
v: Type[Parent] = Parent
|
|
|
|
assert Model(v=Parent).v().echo() == 'parent'
|
|
assert Model().v().echo() == 'parent'
|
|
assert Model(v=Child).v().echo() == 'child'
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=Different)
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'loc': ('v',),
|
|
'msg': 'subclass of Parent expected',
|
|
'type': 'type_error.subclass',
|
|
'ctx': {'expected_class': 'Parent'},
|
|
}
|
|
]
|
|
|
|
|
|
def test_optional_subfields():
|
|
class Model(BaseModel):
|
|
a: Optional[int]
|
|
|
|
assert Model.__fields__['a'].sub_fields is None
|
|
assert Model.__fields__['a'].allow_none is True
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a='foobar')
|
|
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
assert Model().a is None
|
|
assert Model(a=None).a is None
|
|
assert Model(a=12).a == 12
|
|
|
|
|
|
def test_not_optional_subfields():
|
|
class Model(BaseModel):
|
|
a: Optional[int]
|
|
|
|
@validator('a')
|
|
def check_a(cls, v):
|
|
return v
|
|
|
|
assert Model.__fields__['a'].sub_fields is None
|
|
# assert Model.__fields__['a'].required is True
|
|
assert Model.__fields__['a'].allow_none is True
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a='foobar')
|
|
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
assert Model().a is None
|
|
assert Model(a=None).a is None
|
|
assert Model(a=12).a == 12
|
|
|
|
|
|
def test_scheme_deprecated():
|
|
|
|
with pytest.warns(DeprecationWarning, match='`Schema` is deprecated, use `Field` instead'):
|
|
|
|
class Model(BaseModel):
|
|
foo: int = Schema(4)
|
|
|
|
|
|
def test_population_by_alias():
|
|
with pytest.warns(DeprecationWarning, match='"allow_population_by_alias" is deprecated and replaced by'):
|
|
|
|
class Model(BaseModel):
|
|
a: str
|
|
|
|
class Config:
|
|
allow_population_by_alias = True
|
|
fields = {'a': {'alias': '_a'}}
|
|
|
|
assert Model.__config__.allow_population_by_field_name is True
|
|
assert Model(a='different').a == 'different'
|
|
assert Model(a='different').dict() == {'a': 'different'}
|
|
assert Model(a='different').dict(by_alias=True) == {'_a': 'different'}
|
|
|
|
|
|
def test_fields_deprecated():
|
|
class Model(BaseModel):
|
|
v: str = 'x'
|
|
|
|
with pytest.warns(DeprecationWarning, match='`fields` attribute is deprecated, use `__fields__` instead'):
|
|
assert Model().fields.keys() == {'v'}
|
|
|
|
assert Model().__fields__.keys() == {'v'}
|
|
assert Model.__fields__.keys() == {'v'}
|
|
|
|
|
|
def test_alias_child_precedence():
|
|
class Parent(BaseModel):
|
|
x: int
|
|
|
|
class Config:
|
|
fields = {'x': 'x1'}
|
|
|
|
class Child(Parent):
|
|
y: int
|
|
|
|
class Config:
|
|
fields = {'y': 'y2', 'x': 'x2'}
|
|
|
|
assert Child.__fields__['y'].alias == 'y2'
|
|
assert Child.__fields__['x'].alias == 'x2'
|
|
|
|
|
|
def test_alias_generator_parent():
|
|
class Parent(BaseModel):
|
|
x: int
|
|
|
|
class Config:
|
|
allow_population_by_field_name = True
|
|
|
|
@classmethod
|
|
def alias_generator(cls, f_name):
|
|
return f_name + '1'
|
|
|
|
class Child(Parent):
|
|
y: int
|
|
|
|
class Config:
|
|
@classmethod
|
|
def alias_generator(cls, f_name):
|
|
return f_name + '2'
|
|
|
|
assert Child.__fields__['y'].alias == 'y2'
|
|
assert Child.__fields__['x'].alias == 'x2'
|
|
|
|
|
|
def test_optional_field_constraints():
|
|
class MyModel(BaseModel):
|
|
my_int: Optional[int] = Field(..., ge=3)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MyModel(my_int=2)
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'loc': ('my_int',),
|
|
'msg': 'ensure this value is greater than or equal to 3',
|
|
'type': 'value_error.number.not_ge',
|
|
'ctx': {'limit_value': 3},
|
|
}
|
|
]
|
|
|
|
|
|
def test_field_str_shape():
|
|
class Model(BaseModel):
|
|
a: List[int]
|
|
|
|
assert repr(Model.__fields__['a']) == "ModelField(name='a', type=List[int], required=True)"
|
|
assert str(Model.__fields__['a']) == "name='a' type=List[int] required=True"
|
|
|
|
|
|
@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6')
|
|
@pytest.mark.parametrize(
|
|
'type_,expected',
|
|
[
|
|
(int, 'int'),
|
|
(Optional[int], 'Optional[int]'),
|
|
(Union[None, int, str], 'Union[NoneType, int, str]'),
|
|
(Union[int, str, bytes], 'Union[int, str, bytes]'),
|
|
(List[int], 'List[int]'),
|
|
(Tuple[int, str, bytes], 'Tuple[int, str, bytes]'),
|
|
(Union[List[int], Set[bytes]], 'Union[List[int], Set[bytes]]'),
|
|
(List[Tuple[int, int]], 'List[Tuple[int, int]]'),
|
|
(Dict[int, str], 'Mapping[int, str]'),
|
|
(Tuple[int, ...], 'Tuple[int, ...]'),
|
|
(Optional[List[int]], 'Optional[List[int]]'),
|
|
],
|
|
)
|
|
def test_field_type_display(type_, expected):
|
|
class Model(BaseModel):
|
|
a: type_
|
|
|
|
assert Model.__fields__['a']._type_display() == expected
|