mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
381 lines
10 KiB
Python
381 lines
10 KiB
Python
import os
|
|
from collections import OrderedDict
|
|
from datetime import date, datetime, time, timedelta
|
|
from enum import Enum, IntEnum
|
|
|
|
import pytest
|
|
|
|
from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeInt, PositiveInt, PyObject, ValidationError, conint,
|
|
constr)
|
|
|
|
|
|
class ConStringModel(BaseModel):
|
|
v: constr(max_length=10) = 'foobar'
|
|
|
|
|
|
def test_constrained_str_good():
|
|
m = ConStringModel(v='short')
|
|
assert m.v == 'short'
|
|
|
|
|
|
def test_constrained_str_default():
|
|
m = ConStringModel()
|
|
assert m.v == 'foobar'
|
|
|
|
|
|
def test_constrained_str_too_long():
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConStringModel(v='this is too long')
|
|
assert """\
|
|
{
|
|
"v": {
|
|
"error_msg": "length greater than maximum allowed: 10",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "ConstrainedStrValue"
|
|
}
|
|
}""" == exc_info.value.json(2)
|
|
|
|
|
|
class DsnModel(BaseModel):
|
|
db_name = 'foobar'
|
|
db_user = 'postgres'
|
|
db_password: str = None
|
|
db_host = 'localhost'
|
|
db_port = '5432'
|
|
db_driver = 'postgres'
|
|
db_query: dict = None
|
|
dsn: DSN = None
|
|
|
|
|
|
def test_dsn_compute():
|
|
m = DsnModel()
|
|
assert m.dsn == 'postgres://postgres@localhost:5432/foobar'
|
|
|
|
|
|
def test_dsn_define():
|
|
m = DsnModel(dsn='postgres://postgres@localhost:5432/different')
|
|
assert m.dsn == 'postgres://postgres@localhost:5432/different'
|
|
|
|
|
|
def test_dsn_pw_host():
|
|
m = DsnModel(db_password='pword', db_host='before:after', db_query={'v': 1})
|
|
assert m.dsn == 'postgres://postgres:pword@[before:after]:5432/foobar?v=1'
|
|
|
|
|
|
def test_dsn_no_driver():
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
DsnModel(db_driver=None)
|
|
assert '"db_driver" field may not be missing or None' in str(exc_info.value)
|
|
|
|
|
|
class PyObjectModel(BaseModel):
|
|
module: PyObject = 'os.path'
|
|
|
|
|
|
def test_module_import():
|
|
m = PyObjectModel()
|
|
assert m.module == os.path
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PyObjectModel(module='foobar')
|
|
assert '"foobar" doesn\'t look like a module path' in str(exc_info.value)
|
|
|
|
|
|
class CheckModel(BaseModel):
|
|
bool_check = True
|
|
str_check = 's'
|
|
bytes_check = b's'
|
|
int_check = 1
|
|
float_check = 1.0
|
|
|
|
class Config:
|
|
max_anystr_length = 10
|
|
max_number_size = 100
|
|
|
|
|
|
@pytest.mark.parametrize('field,value,result', [
|
|
('bool_check', True, True),
|
|
('bool_check', False, False),
|
|
('bool_check', None, False),
|
|
('bool_check', '', False),
|
|
('bool_check', 1, True),
|
|
('bool_check', 'TRUE', True),
|
|
('bool_check', b'TRUE', True),
|
|
('bool_check', 'true', True),
|
|
('bool_check', '1', True),
|
|
('bool_check', '2', False),
|
|
('bool_check', 2, True),
|
|
('bool_check', 'on', True),
|
|
('bool_check', 'yes', True),
|
|
|
|
('str_check', 's', 's'),
|
|
('str_check', b's', 's'),
|
|
('str_check', 1, '1'),
|
|
('str_check', 'x' * 11, ValidationError),
|
|
('str_check', b'x' * 11, ValidationError),
|
|
|
|
('bytes_check', 's', b's'),
|
|
('bytes_check', b's', b's'),
|
|
('bytes_check', 1, b'1'),
|
|
('bytes_check', 'x' * 11, ValidationError),
|
|
('bytes_check', b'x' * 11, ValidationError),
|
|
|
|
('int_check', 1, 1),
|
|
('int_check', 1.9, 1),
|
|
('int_check', '1', 1),
|
|
('int_check', '1.9', ValidationError),
|
|
('int_check', b'1', 1),
|
|
('int_check', 12, 12),
|
|
('int_check', '12', 12),
|
|
('int_check', b'12', 12),
|
|
('int_check', 123, ValidationError),
|
|
('int_check', '123', ValidationError),
|
|
('int_check', b'123', ValidationError),
|
|
|
|
('float_check', 1, 1.0),
|
|
('float_check', 1.0, 1.0),
|
|
('float_check', '1.0', 1.0),
|
|
('float_check', '1', 1.0),
|
|
('float_check', b'1.0', 1.0),
|
|
('float_check', b'1', 1.0),
|
|
('float_check', 123, ValidationError),
|
|
('float_check', '123', ValidationError),
|
|
('float_check', b'123', ValidationError),
|
|
])
|
|
def test_default_validators(field, value, result):
|
|
kwargs = {field: value}
|
|
if result == ValidationError:
|
|
with pytest.raises(ValidationError):
|
|
CheckModel(**kwargs)
|
|
else:
|
|
assert CheckModel(**kwargs).values()[field] == result
|
|
|
|
|
|
class DatetimeModel(BaseModel):
|
|
dt: datetime = ...
|
|
date_: date = ...
|
|
time_: time = ...
|
|
duration: timedelta = ...
|
|
|
|
|
|
def test_datetime_successful():
|
|
m = DatetimeModel(
|
|
dt='2017-10-5T19:47:07',
|
|
date_=1494012000,
|
|
time_='10:20:30.400',
|
|
duration='15:30.0001',
|
|
)
|
|
assert m.dt == datetime(2017, 10, 5, 19, 47, 7)
|
|
assert m.date_ == date(2017, 5, 5)
|
|
assert m.time_ == time(10, 20, 30, 400000)
|
|
assert m.duration == timedelta(minutes=15, seconds=30, microseconds=100)
|
|
|
|
|
|
def test_datetime_errors():
|
|
with pytest.raises(ValueError) as exc_info:
|
|
DatetimeModel(
|
|
dt='2017-13-5T19:47:07',
|
|
date_='XX1494012000',
|
|
time_='25:20:30.400',
|
|
duration='15:30.0001 broken',
|
|
)
|
|
assert exc_info.value.message == '4 errors validating input'
|
|
assert """\
|
|
{
|
|
"date_": {
|
|
"error_msg": "Invalid date format",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "date"
|
|
},
|
|
"dt": {
|
|
"error_msg": "month must be in 1..12",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "datetime"
|
|
},
|
|
"duration": {
|
|
"error_msg": "Invalid duration format",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "timedelta"
|
|
},
|
|
"time_": {
|
|
"error_msg": "hour must be in 0..23",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "time"
|
|
}
|
|
}""" == exc_info.value.json(2)
|
|
|
|
|
|
class FruitEnum(str, Enum):
|
|
pear = 'pear'
|
|
banana = 'banana'
|
|
|
|
|
|
class ToolEnum(IntEnum):
|
|
spanner = 1
|
|
wrench = 2
|
|
|
|
|
|
class CookingModel(BaseModel):
|
|
fruit: FruitEnum = FruitEnum.pear
|
|
tool: ToolEnum = ToolEnum.spanner
|
|
|
|
|
|
def test_enum_successful():
|
|
m = CookingModel(tool=2)
|
|
assert m.fruit == FruitEnum.pear
|
|
assert m.tool == ToolEnum.wrench
|
|
assert repr(m.tool) == '<ToolEnum.wrench: 2>'
|
|
|
|
|
|
def test_enum_fails():
|
|
with pytest.raises(ValueError) as exc_info:
|
|
CookingModel(tool=3)
|
|
assert exc_info.value.message == '1 error validating input'
|
|
assert """\
|
|
{
|
|
"tool": {
|
|
"error_msg": "3 is not a valid ToolEnum",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "ToolEnum"
|
|
}
|
|
}""" == exc_info.value.json(2)
|
|
|
|
|
|
class MoreStringsModel(BaseModel):
|
|
str_regex: constr(regex=r'^xxx\d{3}$') = ...
|
|
str_min_length: constr(min_length=5) = ...
|
|
str_curtailed: constr(curtail_length=5) = ...
|
|
str_email: EmailStr = ...
|
|
name_email: NameEmail = ...
|
|
|
|
|
|
def test_string_success():
|
|
m = MoreStringsModel(
|
|
str_regex='xxx123',
|
|
str_min_length='12345',
|
|
str_curtailed='123456',
|
|
str_email='foobar@example.com ',
|
|
name_email='foo bar <foobaR@example.com>',
|
|
)
|
|
assert m.str_regex == 'xxx123'
|
|
assert m.str_curtailed == '12345'
|
|
assert m.str_email == 'foobar@example.com'
|
|
assert repr(m.name_email) == '<NameEmail("foo bar <foobar@example.com>")>'
|
|
assert m.name_email.name == 'foo bar'
|
|
assert m.name_email.email == 'foobar@example.com'
|
|
|
|
|
|
def test_string_fails():
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MoreStringsModel(
|
|
str_regex='xxx123 ',
|
|
str_min_length='1234',
|
|
str_curtailed='123', # doesn't fail
|
|
str_email='foobar\n@example.com',
|
|
name_email='foobar @example.com',
|
|
)
|
|
assert exc_info.value.message == '4 errors validating input'
|
|
assert """\
|
|
{
|
|
"name_email": {
|
|
"error_msg": "Email address is not valid",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "NameEmail"
|
|
},
|
|
"str_email": {
|
|
"error_msg": "Email address is not valid",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "EmailStr"
|
|
},
|
|
"str_min_length": {
|
|
"error_msg": "length less than minimum allowed: 5",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "ConstrainedStrValue"
|
|
},
|
|
"str_regex": {
|
|
"error_msg": "string does not match regex \\"^xxx\\\\d{3}$\\"",
|
|
"error_type": "ValueError",
|
|
"index": null,
|
|
"track": "ConstrainedStrValue"
|
|
}
|
|
}""" == exc_info.value.json(2)
|
|
|
|
|
|
class ListDictTupleModel(BaseModel):
|
|
a: dict = None
|
|
b: list = None
|
|
c: OrderedDict = None
|
|
d: tuple = None
|
|
|
|
|
|
def test_dict():
|
|
assert ListDictTupleModel(a={1: 10, 2: 20}).a == {1: 10, 2: 20}
|
|
assert ListDictTupleModel(a=[(1, 2), (3, 4)]).a == {1: 2, 3: 4}
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ListDictTupleModel(a=[1, 2, 3])
|
|
assert 'cannot convert dictionary update sequence element #0 to a sequence' in str(exc_info.value)
|
|
|
|
|
|
def test_list():
|
|
m = ListDictTupleModel(b=[1, 2, '3'])
|
|
assert m.a is None
|
|
assert m.b == [1, 2, '3']
|
|
assert ListDictTupleModel(b='xyz').b == ['x', 'y', 'z']
|
|
assert ListDictTupleModel(b=(i**2 for i in range(5))).b == [0, 1, 4, 9, 16]
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ListDictTupleModel(b=1)
|
|
assert "'int' object is not iterable" in str(exc_info.value)
|
|
|
|
|
|
def test_ordered_dict():
|
|
assert ListDictTupleModel(c=OrderedDict([(1, 10), (2, 20)])).c == OrderedDict([(1, 10), (2, 20)])
|
|
assert ListDictTupleModel(c={1: 10, 2: 20}).c in (OrderedDict([(1, 10), (2, 20)]), OrderedDict([(2, 20), (1, 10)]))
|
|
assert ListDictTupleModel(c=[(1, 2), (3, 4)]).c == OrderedDict([(1, 2), (3, 4)])
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ListDictTupleModel(c=[1, 2, 3])
|
|
assert "'int' object is not iterable" in str(exc_info.value)
|
|
|
|
|
|
def test_tuple():
|
|
m = ListDictTupleModel(d=(1, 2, '3'))
|
|
assert m.a is None
|
|
assert m.d == (1, 2, '3')
|
|
assert m.values() == {'a': None, 'b': None, 'c': None, 'd': (1, 2, '3')}
|
|
assert ListDictTupleModel(d='xyz').d == ('x', 'y', 'z')
|
|
assert ListDictTupleModel(d=(i**2 for i in range(5))).d == (0, 1, 4, 9, 16)
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ListDictTupleModel(d=1)
|
|
assert "'int' object is not iterable" in str(exc_info.value)
|
|
|
|
|
|
class IntModel(BaseModel):
|
|
a: PositiveInt = None
|
|
b: NegativeInt = None
|
|
c: conint(gt=4, lt=10) = None
|
|
|
|
|
|
def test_int_validation():
|
|
m = IntModel(a=5, b=-5, c=5)
|
|
assert m == {'a': 5, 'b': -5, 'c': 5}
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
IntModel(a=-5, b=5, c=-5)
|
|
assert exc_info.value.message == '3 errors validating input'
|
|
|
|
|
|
def test_set():
|
|
class SetModel(BaseModel):
|
|
v: set = ...
|
|
|
|
m = SetModel(v=[1, 2, 3])
|
|
assert m.v == {1, 2, 3}
|
|
assert m.values() == {'v': {1, 2, 3}}
|
|
assert SetModel(v={'a', 'b', 'c'}).v == {'a', 'b', 'c'}
|