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>
3592 lines
104 KiB
Python
3592 lines
104 KiB
Python
import itertools
|
|
import math
|
|
import os
|
|
import re
|
|
import uuid
|
|
from collections import OrderedDict, deque
|
|
from datetime import date, datetime, time, timedelta
|
|
from decimal import Decimal
|
|
from enum import Enum, IntEnum
|
|
from pathlib import Path
|
|
from typing import (
|
|
Any,
|
|
Callable,
|
|
Deque,
|
|
Dict,
|
|
FrozenSet,
|
|
Iterable,
|
|
List,
|
|
MutableSet,
|
|
NewType,
|
|
Optional,
|
|
Pattern,
|
|
Sequence,
|
|
Set,
|
|
Tuple,
|
|
Union,
|
|
)
|
|
from uuid import UUID
|
|
|
|
import annotated_types
|
|
import pytest
|
|
from dirty_equals import AnyThing, HasRepr
|
|
from pydantic_core._pydantic_core import PydanticCustomError, SchemaError
|
|
from typing_extensions import Annotated, Literal, TypedDict
|
|
|
|
from pydantic import (
|
|
UUID1,
|
|
UUID3,
|
|
UUID4,
|
|
UUID5,
|
|
BaseModel,
|
|
ByteSize,
|
|
DirectoryPath,
|
|
EmailStr,
|
|
Field,
|
|
FilePath,
|
|
FiniteFloat,
|
|
Json,
|
|
NameEmail,
|
|
NegativeFloat,
|
|
NegativeInt,
|
|
NonNegativeFloat,
|
|
NonNegativeInt,
|
|
NonPositiveFloat,
|
|
NonPositiveInt,
|
|
PositiveFloat,
|
|
PositiveInt,
|
|
SecretBytes,
|
|
SecretStr,
|
|
StrictBool,
|
|
StrictBytes,
|
|
StrictFloat,
|
|
StrictInt,
|
|
StrictStr,
|
|
ValidationError,
|
|
conbytes,
|
|
condecimal,
|
|
confloat,
|
|
confrozenset,
|
|
conint,
|
|
conlist,
|
|
conset,
|
|
constr,
|
|
validator,
|
|
)
|
|
from pydantic.types import ImportString, SecretField, Strict
|
|
|
|
try:
|
|
import email_validator
|
|
except ImportError:
|
|
email_validator = None
|
|
|
|
# TODO add back tests for Iterator
|
|
|
|
|
|
@pytest.fixture(scope='session', name='ConBytesModel')
|
|
def con_bytes_model_fixture():
|
|
class ConBytesModel(BaseModel):
|
|
v: conbytes(max_length=10) = b'foobar'
|
|
|
|
return ConBytesModel
|
|
|
|
|
|
def test_constrained_bytes_good(ConBytesModel):
|
|
m = ConBytesModel(v=b'short')
|
|
assert m.v == b'short'
|
|
|
|
|
|
def test_constrained_bytes_default(ConBytesModel):
|
|
m = ConBytesModel()
|
|
assert m.v == b'foobar'
|
|
|
|
|
|
def test_strict_raw_type():
|
|
class Model(BaseModel):
|
|
v: Annotated[str, Strict]
|
|
|
|
assert Model(v='foo').v == 'foo'
|
|
with pytest.raises(ValidationError, match=r'Input should be a valid string \[type=string_type,'):
|
|
Model(v=b'fo')
|
|
|
|
|
|
def test_constrained_bytes_too_long(ConBytesModel):
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConBytesModel(v=b'this is too long')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'bytes_too_long',
|
|
'loc': ('v',),
|
|
'msg': 'Data should have at most 10 bytes',
|
|
'input': b'this is too long',
|
|
'ctx': {'max_length': 10},
|
|
}
|
|
]
|
|
|
|
|
|
def test_constrained_bytes_strict_true():
|
|
class Model(BaseModel):
|
|
v: conbytes(strict=True)
|
|
|
|
assert Model(v=b'foobar').v == b'foobar'
|
|
with pytest.raises(ValidationError):
|
|
Model(v=bytearray('foobar', 'utf-8'))
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v='foostring')
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=42)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=0.42)
|
|
|
|
|
|
def test_constrained_bytes_strict_false():
|
|
class Model(BaseModel):
|
|
v: conbytes(strict=False)
|
|
|
|
assert Model(v=b'foobar').v == b'foobar'
|
|
assert Model(v=bytearray('foobar', 'utf-8')).v == b'foobar'
|
|
assert Model(v='foostring').v == b'foostring'
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=42)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=0.42)
|
|
|
|
|
|
def test_constrained_bytes_strict_default():
|
|
class Model(BaseModel):
|
|
v: conbytes()
|
|
|
|
assert Model(v=b'foobar').v == b'foobar'
|
|
assert Model(v=bytearray('foobar', 'utf-8')).v == b'foobar'
|
|
assert Model(v='foostring').v == b'foostring'
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=42)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=0.42)
|
|
|
|
|
|
def test_constrained_list_good():
|
|
class ConListModelMax(BaseModel):
|
|
v: conlist(int) = []
|
|
|
|
m = ConListModelMax(v=[1, 2, 3])
|
|
assert m.v == [1, 2, 3]
|
|
|
|
|
|
def test_constrained_list_default():
|
|
class ConListModelMax(BaseModel):
|
|
v: conlist(int) = []
|
|
|
|
m = ConListModelMax()
|
|
assert m.v == []
|
|
|
|
|
|
def test_constrained_list_too_long():
|
|
class ConListModelMax(BaseModel):
|
|
v: conlist(int, max_length=10) = []
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConListModelMax(v=list(str(i) for i in range(11)))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_long',
|
|
'loc': ('v',),
|
|
'msg': 'List should have at most 10 items after validation, not 11',
|
|
'input': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10'],
|
|
'ctx': {'field_type': 'List', 'max_length': 10, 'actual_length': 11},
|
|
}
|
|
]
|
|
|
|
|
|
def test_constrained_list_too_short():
|
|
class ConListModelMin(BaseModel):
|
|
v: conlist(int, min_length=1)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConListModelMin(v=[])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('v',),
|
|
'msg': 'List should have at least 1 item after validation, not 0',
|
|
'input': [],
|
|
'ctx': {'field_type': 'List', 'min_length': 1, 'actual_length': 0},
|
|
}
|
|
]
|
|
|
|
|
|
def test_constrained_list_optional():
|
|
class Model(BaseModel):
|
|
req: Optional[conlist(str, min_length=1)]
|
|
opt: Optional[conlist(str, min_length=1)] = None
|
|
|
|
assert Model(req=None).model_dump() == {'req': None, 'opt': None}
|
|
assert Model(req=None, opt=None).model_dump() == {'req': None, 'opt': None}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(req=[], opt=[])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('req',),
|
|
'msg': 'List should have at least 1 item after validation, not 0',
|
|
'input': [],
|
|
'ctx': {'field_type': 'List', 'min_length': 1, 'actual_length': 0},
|
|
},
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('opt',),
|
|
'msg': 'List should have at least 1 item after validation, not 0',
|
|
'input': [],
|
|
'ctx': {'field_type': 'List', 'min_length': 1, 'actual_length': 0},
|
|
},
|
|
]
|
|
|
|
assert Model(req=['a'], opt=['a']).model_dump() == {'req': ['a'], 'opt': ['a']}
|
|
|
|
|
|
def test_constrained_list_constraints():
|
|
class ConListModelBoth(BaseModel):
|
|
v: conlist(int, min_length=7, max_length=11)
|
|
|
|
m = ConListModelBoth(v=list(range(7)))
|
|
assert m.v == list(range(7))
|
|
|
|
m = ConListModelBoth(v=list(range(11)))
|
|
assert m.v == list(range(11))
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConListModelBoth(v=list(range(6)))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('v',),
|
|
'msg': 'List should have at least 7 items after validation, not 6',
|
|
'input': [0, 1, 2, 3, 4, 5],
|
|
'ctx': {'field_type': 'List', 'min_length': 7, 'actual_length': 6},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConListModelBoth(v=list(range(12)))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_long',
|
|
'loc': ('v',),
|
|
'msg': 'List should have at most 11 items after validation, not 12',
|
|
'input': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
'ctx': {'field_type': 'List', 'max_length': 11, 'actual_length': 12},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConListModelBoth(v=1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'list_type', 'loc': ('v',), 'msg': 'Input should be a valid list/array', 'input': 1}
|
|
]
|
|
|
|
|
|
def test_constrained_list_item_type_fails():
|
|
class ConListModel(BaseModel):
|
|
v: conlist(int) = []
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConListModel(v=['a', 'b', 'c'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'b',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'c',
|
|
},
|
|
]
|
|
|
|
|
|
def test_conlist():
|
|
class Model(BaseModel):
|
|
foo: List[int] = Field(..., min_length=2, max_length=4)
|
|
bar: conlist(str, min_length=1, max_length=4) = None
|
|
|
|
assert Model(foo=[1, 2], bar=['spoon']).model_dump() == {'foo': [1, 2], 'bar': ['spoon']}
|
|
|
|
msg = r'List should have at least 2 items after validation, not 1 \[type=too_short,'
|
|
with pytest.raises(ValidationError, match=msg):
|
|
Model(foo=[1])
|
|
|
|
msg = r'List should have at most 4 items after validation, not 5 \[type=too_long,'
|
|
with pytest.raises(ValidationError, match=msg):
|
|
Model(foo=list(range(5)))
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=[1, 'x', 'y'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('foo', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'x',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('foo', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'y',
|
|
},
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'list_type', 'loc': ('foo',), 'msg': 'Input should be a valid list/array', 'input': 1}
|
|
]
|
|
|
|
|
|
def test_conlist_wrong_type_default():
|
|
"""It should not validate default value by default"""
|
|
|
|
class Model(BaseModel):
|
|
v: conlist(int) = 'a'
|
|
|
|
m = Model()
|
|
assert m.v == 'a'
|
|
|
|
|
|
def test_constrained_set_good():
|
|
class Model(BaseModel):
|
|
v: conset(int) = []
|
|
|
|
m = Model(v=[1, 2, 3])
|
|
assert m.v == {1, 2, 3}
|
|
|
|
|
|
def test_constrained_set_default():
|
|
class Model(BaseModel):
|
|
v: conset(int) = set()
|
|
|
|
m = Model()
|
|
assert m.v == set()
|
|
|
|
|
|
def test_constrained_set_default_invalid():
|
|
class Model(BaseModel):
|
|
v: conset(int) = 'not valid, not validated'
|
|
|
|
m = Model()
|
|
assert m.v == 'not valid, not validated'
|
|
|
|
|
|
def test_constrained_set_too_long():
|
|
class ConSetModelMax(BaseModel):
|
|
v: conset(int, max_length=10) = []
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConSetModelMax(v={str(i) for i in range(11)})
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_long',
|
|
'loc': ('v',),
|
|
'msg': 'Set should have at most 10 items after validation, not 11',
|
|
'input': {'4', '3', '10', '9', '5', '6', '1', '8', '0', '7', '2'},
|
|
'ctx': {'field_type': 'Set', 'max_length': 10, 'actual_length': 11},
|
|
}
|
|
]
|
|
|
|
|
|
def test_constrained_set_too_short():
|
|
class ConSetModelMin(BaseModel):
|
|
v: conset(int, min_length=1)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConSetModelMin(v=[])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('v',),
|
|
'msg': 'Set should have at least 1 item after validation, not 0',
|
|
'input': [],
|
|
'ctx': {'field_type': 'Set', 'min_length': 1, 'actual_length': 0},
|
|
}
|
|
]
|
|
|
|
|
|
def test_constrained_set_optional():
|
|
class Model(BaseModel):
|
|
req: Optional[conset(str, min_length=1)]
|
|
opt: Optional[conset(str, min_length=1)] = None
|
|
|
|
assert Model(req=None).model_dump() == {'req': None, 'opt': None}
|
|
assert Model(req=None, opt=None).model_dump() == {'req': None, 'opt': None}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(req=set(), opt=set())
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('req',),
|
|
'msg': 'Set should have at least 1 item after validation, not 0',
|
|
'input': set(),
|
|
'ctx': {'field_type': 'Set', 'min_length': 1, 'actual_length': 0},
|
|
},
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('opt',),
|
|
'msg': 'Set should have at least 1 item after validation, not 0',
|
|
'input': set(),
|
|
'ctx': {'field_type': 'Set', 'min_length': 1, 'actual_length': 0},
|
|
},
|
|
]
|
|
|
|
assert Model(req={'a'}, opt={'a'}).model_dump() == {'req': {'a'}, 'opt': {'a'}}
|
|
|
|
|
|
def test_constrained_set_constraints():
|
|
class ConSetModelBoth(BaseModel):
|
|
v: conset(int, min_length=7, max_length=11)
|
|
|
|
m = ConSetModelBoth(v=set(range(7)))
|
|
assert m.v == set(range(7))
|
|
|
|
m = ConSetModelBoth(v=set(range(11)))
|
|
assert m.v == set(range(11))
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConSetModelBoth(v=set(range(6)))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('v',),
|
|
'msg': 'Set should have at least 7 items after validation, not 6',
|
|
'input': {0, 1, 2, 3, 4, 5},
|
|
'ctx': {'field_type': 'Set', 'min_length': 7, 'actual_length': 6},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConSetModelBoth(v=set(range(12)))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_long',
|
|
'loc': ('v',),
|
|
'msg': 'Set should have at most 11 items after validation, not 12',
|
|
'input': {0, 8, 1, 9, 2, 10, 3, 7, 11, 4, 6, 5},
|
|
'ctx': {'field_type': 'Set', 'max_length': 11, 'actual_length': 12},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConSetModelBoth(v=1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'set_type', 'loc': ('v',), 'msg': 'Input should be a valid set', 'input': 1}
|
|
]
|
|
|
|
|
|
def test_constrained_set_item_type_fails():
|
|
class ConSetModel(BaseModel):
|
|
v: conset(int) = []
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConSetModel(v=['a', 'b', 'c'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'b',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'c',
|
|
},
|
|
]
|
|
|
|
|
|
def test_conset():
|
|
class Model(BaseModel):
|
|
foo: Set[int] = Field(..., min_length=2, max_length=4)
|
|
bar: conset(str, min_length=1, max_length=4) = None
|
|
|
|
assert Model(foo=[1, 2], bar=['spoon']).model_dump() == {'foo': {1, 2}, 'bar': {'spoon'}}
|
|
|
|
assert Model(foo=[1, 1, 1, 2, 2], bar=['spoon']).model_dump() == {'foo': {1, 2}, 'bar': {'spoon'}}
|
|
|
|
with pytest.raises(ValidationError, match='Set should have at least 2 items after validation, not 1'):
|
|
Model(foo=[1])
|
|
|
|
with pytest.raises(ValidationError, match='Set should have at most 4 items after validation, not 5'):
|
|
Model(foo=list(range(5)))
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=[1, 'x', 'y'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('foo', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'x',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('foo', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'y',
|
|
},
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'set_type', 'loc': ('foo',), 'msg': 'Input should be a valid set', 'input': 1}
|
|
]
|
|
|
|
|
|
def test_conset_not_required():
|
|
class Model(BaseModel):
|
|
foo: Optional[Set[int]] = None
|
|
|
|
assert Model(foo=None).foo is None
|
|
assert Model().foo is None
|
|
|
|
|
|
def test_confrozenset():
|
|
class Model(BaseModel):
|
|
foo: FrozenSet[int] = Field(..., min_length=2, max_length=4)
|
|
bar: confrozenset(str, min_length=1, max_length=4) = None
|
|
|
|
m = Model(foo=[1, 2], bar=['spoon'])
|
|
assert m.model_dump() == {'foo': {1, 2}, 'bar': {'spoon'}}
|
|
assert isinstance(m.foo, frozenset)
|
|
assert isinstance(m.bar, frozenset)
|
|
|
|
assert Model(foo=[1, 1, 1, 2, 2], bar=['spoon']).model_dump() == {'foo': {1, 2}, 'bar': {'spoon'}}
|
|
|
|
with pytest.raises(ValidationError, match='Frozenset should have at least 2 items after validation, not 1'):
|
|
Model(foo=[1])
|
|
|
|
with pytest.raises(ValidationError, match='Frozenset should have at most 4 items after validation, not 5'):
|
|
Model(foo=list(range(5)))
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=[1, 'x', 'y'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('foo', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'x',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('foo', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'y',
|
|
},
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'frozen_set_type', 'loc': ('foo',), 'msg': 'Input should be a valid frozenset', 'input': 1}
|
|
]
|
|
|
|
|
|
def test_confrozenset_not_required():
|
|
class Model(BaseModel):
|
|
foo: Optional[FrozenSet[int]] = None
|
|
|
|
assert Model(foo=None).foo is None
|
|
assert Model().foo is None
|
|
|
|
|
|
def test_constrained_frozenset_optional():
|
|
class Model(BaseModel):
|
|
req: Optional[confrozenset(str, min_length=1)]
|
|
opt: Optional[confrozenset(str, min_length=1)] = None
|
|
|
|
assert Model(req=None).model_dump() == {'req': None, 'opt': None}
|
|
assert Model(req=None, opt=None).model_dump() == {'req': None, 'opt': None}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(req=frozenset(), opt=frozenset())
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('req',),
|
|
'msg': 'Frozenset should have at least 1 item after validation, not 0',
|
|
'input': frozenset(),
|
|
'ctx': {'field_type': 'Frozenset', 'min_length': 1, 'actual_length': 0},
|
|
},
|
|
{
|
|
'type': 'too_short',
|
|
'loc': ('opt',),
|
|
'msg': 'Frozenset should have at least 1 item after validation, not 0',
|
|
'input': frozenset(),
|
|
'ctx': {'field_type': 'Frozenset', 'min_length': 1, 'actual_length': 0},
|
|
},
|
|
]
|
|
|
|
assert Model(req={'a'}, opt={'a'}).model_dump() == {'req': {'a'}, 'opt': {'a'}}
|
|
|
|
|
|
@pytest.fixture(scope='session', name='ConStringModel')
|
|
def constring_model_fixture():
|
|
class ConStringModel(BaseModel):
|
|
v: constr(max_length=10) = 'foobar'
|
|
|
|
return ConStringModel
|
|
|
|
|
|
def test_constrained_str_good(ConStringModel):
|
|
m = ConStringModel(v='short')
|
|
assert m.v == 'short'
|
|
|
|
|
|
def test_constrained_str_default(ConStringModel):
|
|
m = ConStringModel()
|
|
assert m.v == 'foobar'
|
|
|
|
|
|
def test_constrained_str_too_long(ConStringModel):
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ConStringModel(v='this is too long')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_too_long',
|
|
'loc': ('v',),
|
|
'msg': 'String should have at most 10 characters',
|
|
'input': 'this is too long',
|
|
'ctx': {'max_length': 10},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'to_upper, value, result',
|
|
[
|
|
(True, 'abcd', 'ABCD'),
|
|
(False, 'aBcD', 'aBcD'),
|
|
],
|
|
)
|
|
def test_constrained_str_upper(to_upper, value, result):
|
|
class Model(BaseModel):
|
|
v: constr(to_upper=to_upper)
|
|
|
|
m = Model(v=value)
|
|
assert m.v == result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'to_lower, value, result',
|
|
[
|
|
(True, 'ABCD', 'abcd'),
|
|
(False, 'ABCD', 'ABCD'),
|
|
],
|
|
)
|
|
def test_constrained_str_lower(to_lower, value, result):
|
|
class Model(BaseModel):
|
|
v: constr(to_lower=to_lower)
|
|
|
|
m = Model(v=value)
|
|
assert m.v == result
|
|
|
|
|
|
def test_constrained_str_max_length_0():
|
|
class Model(BaseModel):
|
|
v: constr(max_length=0)
|
|
|
|
m = Model(v='')
|
|
assert m.v == ''
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v='qwe')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_too_long',
|
|
'loc': ('v',),
|
|
'msg': 'String should have at most 0 characters',
|
|
'input': 'qwe',
|
|
'ctx': {'max_length': 0},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'annotation',
|
|
[
|
|
ImportString[Callable[[Any], Any]],
|
|
Annotated[Callable[[Any], Any], ImportString],
|
|
],
|
|
)
|
|
def test_string_import_callable(annotation):
|
|
class PyObjectModel(BaseModel):
|
|
callable: annotation
|
|
|
|
m = PyObjectModel(callable='math.cos')
|
|
assert m.callable == math.cos
|
|
|
|
m = PyObjectModel(callable=math.cos)
|
|
assert m.callable == math.cos
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PyObjectModel(callable='foobar')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'import_error',
|
|
'loc': ('callable',),
|
|
'msg': 'Invalid python path: "foobar" doesn\'t look like a module path',
|
|
'input': 'foobar',
|
|
'ctx': {'error': '"foobar" doesn\'t look like a module path'},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PyObjectModel(callable='os.missing')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'import_error',
|
|
'loc': ('callable',),
|
|
'msg': 'Invalid python path: Module "os" does not define a "missing" attribute',
|
|
'input': 'os.missing',
|
|
'ctx': {'error': 'Module "os" does not define a "missing" attribute'},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PyObjectModel(callable='os.path')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'callable_type', 'loc': ('callable',), 'msg': 'Input should be callable', 'input': os.path}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PyObjectModel(callable=[1, 2, 3])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'callable_type', 'loc': ('callable',), 'msg': 'Input should be callable', 'input': [1, 2, 3]}
|
|
]
|
|
|
|
|
|
def test_string_import_any():
|
|
class PyObjectModel(BaseModel):
|
|
thing: ImportString
|
|
|
|
assert PyObjectModel(thing='math.cos').model_dump() == {'thing': math.cos}
|
|
assert PyObjectModel(thing='os.path').model_dump() == {'thing': os.path}
|
|
assert PyObjectModel(thing=[1, 2, 3]).model_dump() == {'thing': [1, 2, 3]}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'annotation',
|
|
[
|
|
ImportString[Annotated[float, annotated_types.Ge(3), annotated_types.Le(4)]],
|
|
Annotated[float, annotated_types.Ge(3), annotated_types.Le(4), ImportString],
|
|
],
|
|
)
|
|
def test_string_import_constraints(annotation):
|
|
class PyObjectModel(BaseModel):
|
|
thing: annotation
|
|
|
|
assert PyObjectModel(thing='math.pi').model_dump() == {'thing': pytest.approx(3.141592654)}
|
|
with pytest.raises(ValidationError, match='type=greater_than_equal'):
|
|
PyObjectModel(thing='math.e')
|
|
|
|
|
|
def test_decimal():
|
|
class Model(BaseModel):
|
|
v: Decimal
|
|
|
|
m = Model(v='1.234')
|
|
assert m.v == Decimal('1.234')
|
|
assert isinstance(m.v, Decimal)
|
|
assert m.model_dump() == {'v': Decimal('1.234')}
|
|
|
|
|
|
@pytest.fixture(scope='session', name='CheckModel')
|
|
def check_model_fixture():
|
|
class CheckModel(BaseModel):
|
|
bool_check: bool = True
|
|
str_check: constr(strip_whitespace=True, max_length=10) = 's'
|
|
bytes_check: bytes = b's'
|
|
int_check: int = 1
|
|
float_check: float = 1.0
|
|
uuid_check: UUID = UUID('7bd00d58-6485-4ca6-b889-3da6d8df3ee4')
|
|
decimal_check: condecimal(allow_inf_nan=False) = Decimal('42.24')
|
|
|
|
return CheckModel
|
|
|
|
|
|
class BoolCastable:
|
|
def __bool__(self) -> bool:
|
|
return True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'field,value,result',
|
|
[
|
|
('bool_check', True, True),
|
|
('bool_check', 1, True),
|
|
('bool_check', 'y', True),
|
|
('bool_check', 'Y', True),
|
|
('bool_check', 'yes', True),
|
|
('bool_check', 'Yes', True),
|
|
('bool_check', 'YES', True),
|
|
('bool_check', 'true', True),
|
|
('bool_check', 'True', True),
|
|
('bool_check', 'TRUE', True),
|
|
('bool_check', 'on', True),
|
|
('bool_check', 'On', True),
|
|
('bool_check', 'ON', True),
|
|
('bool_check', '1', True),
|
|
('bool_check', 't', True),
|
|
('bool_check', 'T', True),
|
|
('bool_check', b'TRUE', True),
|
|
('bool_check', False, False),
|
|
('bool_check', 0, False),
|
|
('bool_check', 'n', False),
|
|
('bool_check', 'N', False),
|
|
('bool_check', 'no', False),
|
|
('bool_check', 'No', False),
|
|
('bool_check', 'NO', False),
|
|
('bool_check', 'false', False),
|
|
('bool_check', 'False', False),
|
|
('bool_check', 'FALSE', False),
|
|
('bool_check', 'off', False),
|
|
('bool_check', 'Off', False),
|
|
('bool_check', 'OFF', False),
|
|
('bool_check', '0', False),
|
|
('bool_check', 'f', False),
|
|
('bool_check', 'F', False),
|
|
('bool_check', b'FALSE', False),
|
|
('bool_check', None, ValidationError),
|
|
('bool_check', '', ValidationError),
|
|
('bool_check', [], ValidationError),
|
|
('bool_check', {}, ValidationError),
|
|
('bool_check', [1, 2, 3, 4], ValidationError),
|
|
('bool_check', {1: 2, 3: 4}, ValidationError),
|
|
('bool_check', b'2', ValidationError),
|
|
('bool_check', '2', ValidationError),
|
|
('bool_check', 2, ValidationError),
|
|
('bool_check', b'\x81', ValidationError),
|
|
('bool_check', BoolCastable(), ValidationError),
|
|
('str_check', 's', 's'),
|
|
('str_check', ' s ', 's'),
|
|
('str_check', b's', 's'),
|
|
('str_check', b' s ', 's'),
|
|
('str_check', 1, ValidationError),
|
|
('str_check', 'x' * 11, ValidationError),
|
|
('str_check', b'x' * 11, ValidationError),
|
|
('bytes_check', 's', b's'),
|
|
('bytes_check', ' s ', b' s '),
|
|
('bytes_check', b's', b's'),
|
|
('bytes_check', 1, ValidationError),
|
|
('bytes_check', bytearray('xx', encoding='utf8'), b'xx'),
|
|
('bytes_check', True, ValidationError),
|
|
('bytes_check', False, ValidationError),
|
|
('bytes_check', {}, ValidationError),
|
|
('bytes_check', 'x' * 11, b'x' * 11),
|
|
('bytes_check', b'x' * 11, b'x' * 11),
|
|
('int_check', 1, 1),
|
|
('int_check', 1.0, 1),
|
|
('int_check', 1.9, ValidationError),
|
|
('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),
|
|
('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),
|
|
('uuid_check', 'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
|
|
('uuid_check', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
|
|
('uuid_check', b'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')),
|
|
('uuid_check', b'\x12\x34\x56\x78' * 4, UUID('12345678-1234-5678-1234-567812345678')),
|
|
('uuid_check', 'ebcdab58-6eb8-46fb-a190-', ValidationError),
|
|
('uuid_check', 123, ValidationError),
|
|
('decimal_check', 42.24, Decimal('42.24')),
|
|
('decimal_check', '42.24', Decimal('42.24')),
|
|
('decimal_check', b'42.24', ValidationError),
|
|
('decimal_check', ' 42.24 ', Decimal('42.24')),
|
|
('decimal_check', Decimal('42.24'), Decimal('42.24')),
|
|
('decimal_check', 'not a valid decimal', ValidationError),
|
|
('decimal_check', 'NaN', ValidationError),
|
|
],
|
|
)
|
|
def test_default_validators(field, value, result, CheckModel):
|
|
kwargs = {field: value}
|
|
if result == ValidationError:
|
|
with pytest.raises(ValidationError):
|
|
CheckModel(**kwargs)
|
|
else:
|
|
assert CheckModel(**kwargs).model_dump()[field] == result
|
|
|
|
|
|
@pytest.fixture(scope='session', name='StrModel')
|
|
def str_model_fixture():
|
|
class StrModel(BaseModel):
|
|
str_check: Annotated[str, annotated_types.Len(5, 10)]
|
|
|
|
return StrModel
|
|
|
|
|
|
def test_string_too_long(StrModel):
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
StrModel(str_check='x' * 150)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_too_long',
|
|
'loc': ('str_check',),
|
|
'msg': 'String should have at most 10 characters',
|
|
'input': 'x' * 150,
|
|
'ctx': {'max_length': 10},
|
|
}
|
|
]
|
|
|
|
|
|
def test_string_too_short(StrModel):
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
StrModel(str_check='x')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_too_short',
|
|
'loc': ('str_check',),
|
|
'msg': 'String should have at least 5 characters',
|
|
'input': 'x',
|
|
'ctx': {'min_length': 5},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.fixture(scope='session', name='DatetimeModel')
|
|
def datetime_model_fixture():
|
|
class DatetimeModel(BaseModel):
|
|
dt: datetime
|
|
date_: date
|
|
time_: time
|
|
duration: timedelta
|
|
|
|
return DatetimeModel
|
|
|
|
|
|
def test_datetime_successful(DatetimeModel):
|
|
m = DatetimeModel(dt='2017-10-05T19:47:07', date_=1493942400, time_='10:20:30.400', duration='00: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, 400_000)
|
|
assert m.duration == timedelta(minutes=15, seconds=30, microseconds=100)
|
|
|
|
|
|
def test_datetime_errors(DatetimeModel):
|
|
with pytest.raises(ValueError) as exc_info:
|
|
DatetimeModel(dt='2017-13-05T19:47:07', date_='XX1494012000', time_='25:20:30.400', duration='15:30.0001broken')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'datetime_parsing',
|
|
'loc': ('dt',),
|
|
'msg': 'Input should be a valid datetime, month value is outside expected range of 1-12',
|
|
'input': '2017-13-05T19:47:07',
|
|
'ctx': {'error': 'month value is outside expected range of 1-12'},
|
|
},
|
|
{
|
|
'type': 'date_from_datetime_parsing',
|
|
'loc': ('date_',),
|
|
'msg': 'Input should be a valid date or datetime, invalid character in year',
|
|
'input': 'XX1494012000',
|
|
'ctx': {'error': 'invalid character in year'},
|
|
},
|
|
{
|
|
'type': 'time_parsing',
|
|
'loc': ('time_',),
|
|
'msg': 'Input should be in a valid time format, hour value is outside expected range of 0-23',
|
|
'input': '25:20:30.400',
|
|
'ctx': {'error': 'hour value is outside expected range of 0-23'},
|
|
},
|
|
{
|
|
'type': 'time_delta_parsing',
|
|
'loc': ('duration',),
|
|
'msg': 'Input should be a valid timedelta, unexpected extra characters at the end of the input',
|
|
'input': '15:30.0001broken',
|
|
'ctx': {'error': 'unexpected extra characters at the end of the input'},
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture(scope='session')
|
|
def cooking_model():
|
|
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
|
|
|
|
return FruitEnum, ToolEnum, CookingModel
|
|
|
|
|
|
def test_enum_successful(cooking_model):
|
|
FruitEnum, ToolEnum, CookingModel = cooking_model
|
|
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(cooking_model):
|
|
FruitEnum, ToolEnum, CookingModel = cooking_model
|
|
with pytest.raises(ValueError) as exc_info:
|
|
CookingModel(tool=3)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'literal_error',
|
|
'loc': ('tool',),
|
|
'msg': 'Input should be 1 or 2',
|
|
'input': 3,
|
|
'ctx': {'expected': '1 or 2'},
|
|
}
|
|
]
|
|
|
|
|
|
def test_int_enum_successful_for_str_int(cooking_model):
|
|
FruitEnum, ToolEnum, CookingModel = cooking_model
|
|
m = CookingModel(tool='2')
|
|
assert m.tool == ToolEnum.wrench
|
|
assert repr(m.tool) == '<ToolEnum.wrench: 2>'
|
|
|
|
|
|
def test_enum_type():
|
|
with pytest.raises(SchemaError, match='"expected" should have length > 0'):
|
|
|
|
class Model(BaseModel):
|
|
my_int_enum: Enum
|
|
|
|
|
|
def test_int_enum_type():
|
|
with pytest.raises(SchemaError, match='"expected" should have length > 0'):
|
|
|
|
class Model(BaseModel):
|
|
my_int_enum: IntEnum
|
|
|
|
|
|
@pytest.mark.skipif(not email_validator, reason='email_validator not installed')
|
|
def test_string_success():
|
|
class MoreStringsModel(BaseModel):
|
|
str_strip_enabled: constr(strip_whitespace=True)
|
|
str_strip_disabled: constr(strip_whitespace=False)
|
|
str_regex: constr(pattern=r'^xxx\d{3}$') = ... # noqa: F722
|
|
str_min_length: constr(min_length=5) = ...
|
|
str_email: EmailStr = ...
|
|
name_email: NameEmail = ...
|
|
|
|
m = MoreStringsModel(
|
|
str_strip_enabled=' xxx123 ',
|
|
str_strip_disabled=' xxx123 ',
|
|
str_regex='xxx123',
|
|
str_min_length='12345',
|
|
str_email='foobar@example.com ',
|
|
name_email='foo bar <foobaR@example.com>',
|
|
)
|
|
assert m.str_strip_enabled == 'xxx123'
|
|
assert m.str_strip_disabled == ' xxx123 '
|
|
assert m.str_regex == 'xxx123'
|
|
assert m.str_email == 'foobar@example.com'
|
|
assert repr(m.name_email) == "NameEmail(name='foo bar', email='foobaR@example.com')"
|
|
assert str(m.name_email) == 'foo bar <foobaR@example.com>'
|
|
assert m.name_email.name == 'foo bar'
|
|
assert m.name_email.email == 'foobaR@example.com'
|
|
|
|
|
|
@pytest.mark.skipif(not email_validator, reason='email_validator not installed')
|
|
def test_string_fails():
|
|
class MoreStringsModel(BaseModel):
|
|
str_regex: constr(pattern=r'^xxx\d{3}$') = ... # noqa: F722
|
|
str_min_length: constr(min_length=5) = ...
|
|
str_email: EmailStr = ...
|
|
name_email: NameEmail = ...
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MoreStringsModel(
|
|
str_regex='xxx123xxx',
|
|
str_min_length='1234',
|
|
str_email='foobar<@example.com',
|
|
name_email='foobar @example.com',
|
|
)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_pattern_mismatch',
|
|
'loc': ('str_regex',),
|
|
'msg': "String should match pattern '^xxx\\d{3}$'",
|
|
'input': 'xxx123xxx',
|
|
'ctx': {'pattern': '^xxx\\d{3}$'},
|
|
},
|
|
{
|
|
'type': 'string_too_short',
|
|
'loc': ('str_min_length',),
|
|
'msg': 'String should have at least 5 characters',
|
|
'input': '1234',
|
|
'ctx': {'min_length': 5},
|
|
},
|
|
{
|
|
'type': 'value_error',
|
|
'loc': ('str_email',),
|
|
'msg': (
|
|
'value is not a valid email address: The email address contains invalid '
|
|
'characters before the @-sign: LESS-THAN SIGN.'
|
|
),
|
|
'input': 'foobar<@example.com',
|
|
'ctx': {'reason': 'The email address contains invalid characters before the @-sign: LESS-THAN SIGN.'},
|
|
},
|
|
{
|
|
'type': 'value_error',
|
|
'loc': ('name_email',),
|
|
'msg': (
|
|
'value is not a valid email address: The email address contains invalid characters '
|
|
'before the @-sign: SPACE.'
|
|
),
|
|
'input': 'foobar @example.com',
|
|
'ctx': {'reason': 'The email address contains invalid characters before the @-sign: SPACE.'},
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif(email_validator, reason='email_validator is installed')
|
|
def test_email_validator_not_installed_email_str():
|
|
with pytest.raises(ImportError):
|
|
|
|
class Model(BaseModel):
|
|
str_email: EmailStr = ...
|
|
|
|
|
|
@pytest.mark.skipif(email_validator, reason='email_validator is installed')
|
|
def test_email_validator_not_installed_name_email():
|
|
with pytest.raises(ImportError):
|
|
|
|
class Model(BaseModel):
|
|
str_email: NameEmail = ...
|
|
|
|
|
|
def test_dict():
|
|
class Model(BaseModel):
|
|
v: dict
|
|
|
|
assert Model(v={1: 10, 2: 20}).v == {1: 10, 2: 20}
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[(1, 2), (3, 4)])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'dict_type',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be a valid dictionary',
|
|
'input': [(1, 2), (3, 4)],
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[1, 2, 3])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'dict_type', 'loc': ('v',), 'msg': 'Input should be a valid dictionary', 'input': [1, 2, 3]}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
(
|
|
([1, 2, '3'], [1, 2, '3']),
|
|
((1, 2, '3'), [1, 2, '3']),
|
|
((i**2 for i in range(5)), [0, 1, 4, 9, 16]),
|
|
(deque([1, 2, 3]), [1, 2, 3]),
|
|
),
|
|
)
|
|
def test_list_success(value, result):
|
|
class Model(BaseModel):
|
|
v: list
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
@pytest.mark.parametrize('value', (123, '123', {1, 2, '3'}))
|
|
def test_list_fails(value):
|
|
class Model(BaseModel):
|
|
v: list
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': value,
|
|
}
|
|
]
|
|
|
|
|
|
def test_ordered_dict():
|
|
class Model(BaseModel):
|
|
v: OrderedDict
|
|
|
|
assert Model(v=OrderedDict([(1, 10), (2, 20)])).v == OrderedDict([(1, 10), (2, 20)])
|
|
assert Model(v={1: 10, 2: 20}).v == OrderedDict([(1, 10), (2, 20)])
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=[1, 2, 3])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'dict_type', 'loc': ('v',), 'msg': 'Input should be a valid dictionary', 'input': [1, 2, 3]}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
(
|
|
([1, 2, '3'], (1, 2, '3')),
|
|
((1, 2, '3'), (1, 2, '3')),
|
|
((i**2 for i in range(5)), (0, 1, 4, 9, 16)),
|
|
(deque([1, 2, 3]), (1, 2, 3)),
|
|
),
|
|
)
|
|
def test_tuple_success(value, result):
|
|
class Model(BaseModel):
|
|
v: tuple
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
@pytest.mark.parametrize('value', (123, '123', {1, 2, '3'}))
|
|
def test_tuple_fails(value):
|
|
class Model(BaseModel):
|
|
v: tuple
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'tuple_type', 'loc': ('v',), 'msg': 'Input should be a valid tuple', 'input': value}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,cls,result',
|
|
(
|
|
([1, 2, '3'], int, (1, 2, 3)),
|
|
((1, 2, '3'), int, (1, 2, 3)),
|
|
((i**2 for i in range(5)), int, (0, 1, 4, 9, 16)),
|
|
(('a', 'b', 'c'), str, ('a', 'b', 'c')),
|
|
),
|
|
)
|
|
def test_tuple_variable_len_success(value, cls, result):
|
|
class Model(BaseModel):
|
|
v: Tuple[cls, ...]
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value, cls, exc',
|
|
[
|
|
(
|
|
('a', 'b', [1, 2], 'c'),
|
|
str,
|
|
[
|
|
{
|
|
'type': 'string_type',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid string',
|
|
'input': [1, 2],
|
|
}
|
|
],
|
|
),
|
|
(
|
|
('a', 'b', [1, 2], 'c', [3, 4]),
|
|
str,
|
|
[
|
|
{
|
|
'type': 'string_type',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid string',
|
|
'input': [1, 2],
|
|
},
|
|
{
|
|
'type': 'string_type',
|
|
'loc': ('v', 4),
|
|
'msg': 'Input should be a valid string',
|
|
'input': [3, 4],
|
|
},
|
|
],
|
|
),
|
|
],
|
|
)
|
|
def test_tuple_variable_len_fails(value, cls, exc):
|
|
class Model(BaseModel):
|
|
v: Tuple[cls, ...]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
assert exc_info.value.errors() == exc
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
(
|
|
({1, 2, 2, '3'}, {1, 2, '3'}),
|
|
((1, 2, 2, '3'), {1, 2, '3'}),
|
|
([1, 2, 2, '3'], {1, 2, '3'}),
|
|
({i**2 for i in range(5)}, {0, 1, 4, 9, 16}),
|
|
),
|
|
)
|
|
def test_set_success(value, result):
|
|
class Model(BaseModel):
|
|
v: set
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
@pytest.mark.parametrize('value', (123, '123'))
|
|
def test_set_fails(value):
|
|
class Model(BaseModel):
|
|
v: set
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'set_type', 'loc': ('v',), 'msg': 'Input should be a valid set', 'input': value}
|
|
]
|
|
|
|
|
|
def test_list_type_fails():
|
|
class Model(BaseModel):
|
|
v: List[int]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v='123')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'list_type', 'loc': ('v',), 'msg': 'Input should be a valid list/array', 'input': '123'}
|
|
]
|
|
|
|
|
|
def test_set_type_fails():
|
|
class Model(BaseModel):
|
|
v: Set[int]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v='123')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'set_type', 'loc': ('v',), 'msg': 'Input should be a valid set', 'input': '123'}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'cls, value,result',
|
|
(
|
|
(int, [1, 2, 3], [1, 2, 3]),
|
|
(int, (1, 2, 3), (1, 2, 3)),
|
|
(int, range(5), [0, 1, 2, 3, 4]),
|
|
(int, deque((1, 2, 3)), deque((1, 2, 3))),
|
|
(Set[int], [{1, 2}, {3, 4}, {5, 6}], [{1, 2}, {3, 4}, {5, 6}]),
|
|
(Tuple[int, str], ((1, 'a'), (2, 'b'), (3, 'c')), ((1, 'a'), (2, 'b'), (3, 'c'))),
|
|
),
|
|
)
|
|
def test_sequence_success(cls, value, result):
|
|
class Model(BaseModel):
|
|
v: Sequence[cls]
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
def int_iterable():
|
|
i = 0
|
|
while True:
|
|
i += 1
|
|
yield str(i)
|
|
|
|
|
|
def str_iterable():
|
|
while True:
|
|
yield from 'foobarbaz'
|
|
|
|
|
|
def test_infinite_iterable_int():
|
|
class Model(BaseModel):
|
|
it: Iterable[int]
|
|
|
|
m = Model(it=int_iterable())
|
|
|
|
assert repr(m.it) == 'ValidatorIterator(index=0, schema=Some(Int(IntValidator { strict: false })))'
|
|
|
|
output = []
|
|
for i in m.it:
|
|
output.append(i)
|
|
if i == 10:
|
|
break
|
|
|
|
assert output == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
|
|
m = Model(it=[1, 2, 3])
|
|
assert list(m.it) == [1, 2, 3]
|
|
|
|
m = Model(it=str_iterable())
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
next(m.it)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': (0,),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'f',
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize('type_annotation', (Iterable[Any], Iterable))
|
|
def test_iterable_any(type_annotation):
|
|
class Model(BaseModel):
|
|
it: type_annotation
|
|
|
|
m = Model(it=int_iterable())
|
|
|
|
output = []
|
|
for i in m.it:
|
|
output.append(i)
|
|
if int(i) == 10:
|
|
break
|
|
|
|
assert output == ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
|
|
|
|
m = Model(it=[1, '2', b'three'])
|
|
assert list(m.it) == [1, '2', b'three']
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(it=3)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'iterable_type', 'loc': ('it',), 'msg': 'Input should be iterable', 'input': 3}
|
|
]
|
|
|
|
|
|
def test_invalid_iterable():
|
|
class Model(BaseModel):
|
|
it: Iterable[int]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(it=3)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'iterable_type', 'loc': ('it',), 'msg': 'Input should be iterable', 'input': 3}
|
|
]
|
|
|
|
|
|
def test_infinite_iterable_validate_first():
|
|
class Model(BaseModel):
|
|
it: Iterable[int]
|
|
b: int
|
|
|
|
@validator('it')
|
|
def infinite_first_int(cls, it, **kwargs):
|
|
return itertools.chain([next(it)], it)
|
|
|
|
m = Model(it=int_iterable(), b=3)
|
|
|
|
assert m.b == 3
|
|
assert m.it
|
|
|
|
for i in m.it:
|
|
assert i
|
|
if i == 10:
|
|
break
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(it=str_iterable(), b=3)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('it', 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'f',
|
|
}
|
|
]
|
|
|
|
|
|
def test_sequence_generator_fails():
|
|
class Model(BaseModel):
|
|
v: Sequence[int]
|
|
|
|
gen = (i for i in [1, 2, 3])
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=gen)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'is_instance_of',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be an instance of Sequence',
|
|
'input': gen,
|
|
'ctx': {'class': 'Sequence'},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'cls,value,errors',
|
|
(
|
|
(
|
|
int,
|
|
[1, 'a', 3],
|
|
[
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
],
|
|
),
|
|
(
|
|
int,
|
|
(1, 2, 'a'),
|
|
[
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
],
|
|
),
|
|
(
|
|
float,
|
|
('a', 2.2, 3.3),
|
|
[
|
|
{
|
|
'type': 'float_parsing',
|
|
'loc': ('v', 0),
|
|
'msg': 'Input should be a valid number, unable to parse string as an number',
|
|
'input': 'a',
|
|
},
|
|
],
|
|
),
|
|
(
|
|
float,
|
|
(1.1, 2.2, 'a'),
|
|
[
|
|
{
|
|
'type': 'float_parsing',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid number, unable to parse string as an number',
|
|
'input': 'a',
|
|
},
|
|
],
|
|
),
|
|
(
|
|
float,
|
|
{1.0, 2.0, 3.0},
|
|
[
|
|
{
|
|
'type': 'is_instance_of',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be an instance of Sequence',
|
|
'input': {
|
|
1.0,
|
|
2.0,
|
|
3.0,
|
|
},
|
|
'ctx': {
|
|
'class': 'Sequence',
|
|
},
|
|
},
|
|
],
|
|
),
|
|
(
|
|
Set[int],
|
|
[{1, 2}, {2, 3}, {'d'}],
|
|
[
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 2, 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'd',
|
|
}
|
|
],
|
|
),
|
|
(
|
|
Tuple[int, str],
|
|
((1, 'a'), ('a', 'a'), (3, 'c')),
|
|
[
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 1, 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
}
|
|
],
|
|
),
|
|
(
|
|
List[int],
|
|
[{'a': 1, 'b': 2}, [1, 2], [2, 3]],
|
|
[
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('v', 0),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': {'a': 1, 'b': 2},
|
|
}
|
|
],
|
|
),
|
|
),
|
|
ids=repr,
|
|
)
|
|
def test_sequence_fails(cls, value, errors):
|
|
class Model(BaseModel):
|
|
v: Sequence[cls]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
assert exc_info.value.errors() == errors
|
|
|
|
|
|
def test_int_validation():
|
|
class Model(BaseModel):
|
|
a: PositiveInt = None
|
|
b: NegativeInt = None
|
|
c: NonNegativeInt = None
|
|
d: NonPositiveInt = None
|
|
e: conint(gt=4, lt=10) = None
|
|
f: conint(ge=0, le=10) = None
|
|
g: conint(multiple_of=5) = None
|
|
|
|
m = Model(a=5, b=-5, c=0, d=0, e=5, f=0, g=25)
|
|
assert m == {'a': 5, 'b': -5, 'c': 0, 'd': 0, 'e': 5, 'f': 0, 'g': 25}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=-5, b=5, c=-5, d=5, e=-5, f=11, g=42)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'greater_than',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be greater than 0',
|
|
'input': -5,
|
|
'ctx': {'gt': 0},
|
|
},
|
|
{
|
|
'type': 'less_than',
|
|
'loc': ('b',),
|
|
'msg': 'Input should be less than 0',
|
|
'input': 5,
|
|
'ctx': {'lt': 0},
|
|
},
|
|
{
|
|
'type': 'greater_than_equal',
|
|
'loc': ('c',),
|
|
'msg': 'Input should be greater than or equal to 0',
|
|
'input': -5,
|
|
'ctx': {'ge': 0},
|
|
},
|
|
{
|
|
'type': 'less_than_equal',
|
|
'loc': ('d',),
|
|
'msg': 'Input should be less than or equal to 0',
|
|
'input': 5,
|
|
'ctx': {'le': 0},
|
|
},
|
|
{
|
|
'type': 'greater_than',
|
|
'loc': ('e',),
|
|
'msg': 'Input should be greater than 4',
|
|
'input': -5,
|
|
'ctx': {'gt': 4},
|
|
},
|
|
{
|
|
'type': 'less_than_equal',
|
|
'loc': ('f',),
|
|
'msg': 'Input should be less than or equal to 10',
|
|
'input': 11,
|
|
'ctx': {'le': 10},
|
|
},
|
|
{
|
|
'type': 'multiple_of',
|
|
'loc': ('g',),
|
|
'msg': 'Input should be a multiple of 5',
|
|
'input': 42,
|
|
'ctx': {'multiple_of': 5},
|
|
},
|
|
]
|
|
|
|
|
|
def test_float_validation():
|
|
class Model(BaseModel):
|
|
a: PositiveFloat = None
|
|
b: NegativeFloat = None
|
|
c: NonNegativeFloat = None
|
|
d: NonPositiveFloat = None
|
|
e: confloat(gt=4, lt=12.2) = None
|
|
f: confloat(ge=0, le=9.9) = None
|
|
g: confloat(multiple_of=0.5) = None
|
|
h: confloat(allow_inf_nan=False) = None
|
|
|
|
m = Model(a=5.1, b=-5.2, c=0, d=0, e=5.3, f=9.9, g=2.5, h=42)
|
|
assert m.model_dump() == {'a': 5.1, 'b': -5.2, 'c': 0, 'd': 0, 'e': 5.3, 'f': 9.9, 'g': 2.5, 'h': 42}
|
|
|
|
assert Model(a=float('inf')).a == float('inf')
|
|
assert Model(b=float('-inf')).b == float('-inf')
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=-5.1, b=5.2, c=-5.1, d=5.1, e=-5.3, f=9.91, g=4.2, h=float('nan'))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'greater_than',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be greater than 0',
|
|
'input': -5.1,
|
|
'ctx': {
|
|
'gt': 0.0,
|
|
},
|
|
},
|
|
{
|
|
'type': 'less_than',
|
|
'loc': ('b',),
|
|
'msg': 'Input should be less than 0',
|
|
'input': 5.2,
|
|
'ctx': {
|
|
'lt': 0.0,
|
|
},
|
|
},
|
|
{
|
|
'type': 'greater_than_equal',
|
|
'loc': ('c',),
|
|
'msg': 'Input should be greater than or equal to 0',
|
|
'input': -5.1,
|
|
'ctx': {
|
|
'ge': 0.0,
|
|
},
|
|
},
|
|
{
|
|
'type': 'less_than_equal',
|
|
'loc': ('d',),
|
|
'msg': 'Input should be less than or equal to 0',
|
|
'input': 5.1,
|
|
'ctx': {
|
|
'le': 0.0,
|
|
},
|
|
},
|
|
{
|
|
'type': 'greater_than',
|
|
'loc': ('e',),
|
|
'msg': 'Input should be greater than 4',
|
|
'input': -5.3,
|
|
'ctx': {
|
|
'gt': 4.0,
|
|
},
|
|
},
|
|
{
|
|
'type': 'less_than_equal',
|
|
'loc': ('f',),
|
|
'msg': 'Input should be less than or equal to 9.9',
|
|
'input': 9.91,
|
|
'ctx': {
|
|
'le': 9.9,
|
|
},
|
|
},
|
|
{
|
|
'type': 'multiple_of',
|
|
'loc': ('g',),
|
|
'msg': 'Input should be a multiple of 0.5',
|
|
'input': 4.2,
|
|
'ctx': {
|
|
'multiple_of': 0.5,
|
|
},
|
|
},
|
|
{
|
|
'type': 'finite_number',
|
|
'loc': ('h',),
|
|
'msg': 'Input should be a finite number',
|
|
'input': HasRepr('nan'),
|
|
},
|
|
]
|
|
|
|
|
|
def test_finite_float_validation():
|
|
class Model(BaseModel):
|
|
a: float = None
|
|
|
|
assert Model(a=float('inf')).a == float('inf')
|
|
assert Model(a=float('-inf')).a == float('-inf')
|
|
assert math.isnan(Model(a=float('nan')).a)
|
|
|
|
|
|
@pytest.mark.parametrize('value', [float('inf'), float('-inf'), float('nan')])
|
|
def test_finite_float_validation_error(value):
|
|
class Model(BaseModel):
|
|
a: FiniteFloat
|
|
|
|
assert Model(a=42).a == 42
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=value)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'finite_number',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be a finite number',
|
|
'input': HasRepr(repr(value)),
|
|
}
|
|
]
|
|
|
|
|
|
def test_finite_float_config():
|
|
class Model(BaseModel):
|
|
a: float
|
|
|
|
class Config:
|
|
allow_inf_nan = False
|
|
|
|
assert Model(a=42).a == 42
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=float('nan'))
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'finite_number',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be a finite number',
|
|
'input': HasRepr('nan'),
|
|
}
|
|
]
|
|
|
|
|
|
def test_strict_bytes():
|
|
class Model(BaseModel):
|
|
v: StrictBytes
|
|
|
|
assert Model(v=b'foobar').v == b'foobar'
|
|
with pytest.raises(ValidationError, match='Input should be a valid bytes'):
|
|
Model(v=bytearray('foobar', 'utf-8'))
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid bytes'):
|
|
Model(v='foostring')
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid bytes'):
|
|
Model(v=42)
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid bytes'):
|
|
Model(v=0.42)
|
|
|
|
|
|
def test_strict_bytes_max_length():
|
|
class Model(BaseModel):
|
|
u: StrictBytes = Field(..., max_length=5)
|
|
|
|
assert Model(u=b'foo').u == b'foo'
|
|
|
|
with pytest.raises(ValidationError, match=r'Input should be a valid bytes \[type=bytes_type'):
|
|
Model(u=123)
|
|
with pytest.raises(ValidationError, match=r'Data should have at most 5 bytes \[type=bytes_too_long,'):
|
|
Model(u=b'1234567')
|
|
|
|
|
|
def test_strict_str():
|
|
class FruitEnum(str, Enum):
|
|
pear = 'pear'
|
|
banana = 'banana'
|
|
|
|
class Model(BaseModel):
|
|
v: StrictStr
|
|
|
|
assert Model(v='foobar').v == 'foobar'
|
|
|
|
msg = r'Input should be a string, not an instance of a subclass of str \[type=string_sub_type,'
|
|
with pytest.raises(ValidationError, match=msg):
|
|
Model(v=FruitEnum.banana)
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid string'):
|
|
Model(v=123)
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid string'):
|
|
Model(v=b'foobar')
|
|
|
|
|
|
def test_strict_str_max_length():
|
|
class Model(BaseModel):
|
|
u: StrictStr = Field(..., max_length=5)
|
|
|
|
assert Model(u='foo').u == 'foo'
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid string'):
|
|
Model(u=123)
|
|
|
|
with pytest.raises(ValidationError, match=r'String should have at most 5 characters \[type=string_too_long,'):
|
|
Model(u='1234567')
|
|
|
|
|
|
def test_strict_bool():
|
|
class Model(BaseModel):
|
|
v: StrictBool
|
|
|
|
assert Model(v=True).v is True
|
|
assert Model(v=False).v is False
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=1)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v='1')
|
|
|
|
with pytest.raises(ValidationError):
|
|
Model(v=b'1')
|
|
|
|
|
|
def test_strict_int():
|
|
class Model(BaseModel):
|
|
v: StrictInt
|
|
|
|
assert Model(v=123456).v == 123456
|
|
|
|
with pytest.raises(ValidationError, match=r'Input should be a valid integer \[type=int_type,'):
|
|
Model(v='123456')
|
|
|
|
with pytest.raises(ValidationError, match=r'Input should be a valid integer \[type=int_type,'):
|
|
Model(v=3.14159)
|
|
|
|
|
|
def test_strict_float():
|
|
class Model(BaseModel):
|
|
v: StrictFloat
|
|
|
|
assert Model(v=3.14159).v == 3.14159
|
|
assert Model(v=123456).v == 123456
|
|
|
|
with pytest.raises(ValidationError, match=r'Input should be a valid number \[type=float_type,'):
|
|
Model(v='3.14159')
|
|
|
|
|
|
def test_bool_unhashable_fails():
|
|
class Model(BaseModel):
|
|
v: bool
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v={})
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'bool_type', 'loc': ('v',), 'msg': 'Input should be a valid boolean', 'input': {}}
|
|
]
|
|
|
|
|
|
def test_uuid_error():
|
|
class Model(BaseModel):
|
|
v: UUID
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v='ebcdab58-6eb8-46fb-a190-d07a3')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'uuid_type',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be a valid UUID, string, or bytes',
|
|
'input': 'ebcdab58-6eb8-46fb-a190-d07a3',
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError, match='Input should be a valid UUID, string, or bytes'):
|
|
Model(v=None)
|
|
|
|
|
|
def test_uuid_validation():
|
|
class UUIDModel(BaseModel):
|
|
a: UUID1
|
|
b: UUID3
|
|
c: UUID4
|
|
d: UUID5
|
|
|
|
a = uuid.uuid1()
|
|
b = uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org')
|
|
c = uuid.uuid4()
|
|
d = uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
|
|
|
|
m = UUIDModel(a=a, b=b, c=c, d=d)
|
|
assert m.model_dump() == {'a': a, 'b': b, 'c': c, 'd': d}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
UUIDModel(a=d, b=c, c=b, d=a)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'uuid_version',
|
|
'loc': ('a',),
|
|
'msg': 'uuid version 1 expected',
|
|
'input': d,
|
|
'ctx': {'required_version': 1},
|
|
},
|
|
{
|
|
'type': 'uuid_version',
|
|
'loc': ('b',),
|
|
'msg': 'uuid version 3 expected',
|
|
'input': c,
|
|
'ctx': {'required_version': 3},
|
|
},
|
|
{
|
|
'type': 'uuid_version',
|
|
'loc': ('c',),
|
|
'msg': 'uuid version 4 expected',
|
|
'input': b,
|
|
'ctx': {'required_version': 4},
|
|
},
|
|
{
|
|
'type': 'uuid_version',
|
|
'loc': ('d',),
|
|
'msg': 'uuid version 5 expected',
|
|
'input': a,
|
|
'ctx': {'required_version': 5},
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'enabled,str_check,result_str_check',
|
|
[
|
|
(True, ' 123 ', '123'),
|
|
(True, ' 123\t\n', '123'),
|
|
(False, ' 123 ', ' 123 '),
|
|
],
|
|
)
|
|
def test_anystr_strip_whitespace(enabled, str_check, result_str_check):
|
|
class Model(BaseModel):
|
|
str_check: str
|
|
|
|
class Config:
|
|
anystr_strip_whitespace = enabled
|
|
|
|
m = Model(str_check=str_check)
|
|
assert m.str_check == result_str_check
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'enabled,str_check,result_str_check',
|
|
[(True, 'ABCDefG', 'ABCDEFG'), (False, 'ABCDefG', 'ABCDefG')],
|
|
)
|
|
def test_anystr_upper(enabled, str_check, result_str_check):
|
|
class Model(BaseModel):
|
|
str_check: str
|
|
|
|
class Config:
|
|
anystr_upper = enabled
|
|
|
|
m = Model(str_check=str_check)
|
|
|
|
assert m.str_check == result_str_check
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'enabled,str_check,result_str_check',
|
|
[(True, 'ABCDefG', 'abcdefg'), (False, 'ABCDefG', 'ABCDefG')],
|
|
)
|
|
def test_anystr_lower(enabled, str_check, result_str_check):
|
|
class Model(BaseModel):
|
|
str_check: str
|
|
|
|
class Config:
|
|
anystr_lower = enabled
|
|
|
|
m = Model(str_check=str_check)
|
|
|
|
assert m.str_check == result_str_check
|
|
|
|
|
|
pos_int_values = 'Inf', '+Inf', 'Infinity', '+Infinity'
|
|
neg_int_values = '-Inf', '-Infinity'
|
|
nan_values = 'NaN', '-NaN', '+NaN', 'sNaN', '-sNaN', '+sNaN'
|
|
non_finite_values = nan_values + pos_int_values + neg_int_values
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'type_args,value,result',
|
|
[
|
|
(dict(gt=Decimal('42.24')), Decimal('43'), Decimal('43')),
|
|
(
|
|
dict(gt=Decimal('42.24')),
|
|
Decimal('42'),
|
|
[
|
|
{
|
|
'type': 'greater_than',
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be greater than 42.24',
|
|
'input': Decimal('42'),
|
|
'ctx': {'gt': 42.24},
|
|
}
|
|
],
|
|
),
|
|
(dict(lt=Decimal('42.24')), Decimal('42'), Decimal('42')),
|
|
(
|
|
dict(lt=Decimal('42.24')),
|
|
Decimal('43'),
|
|
[
|
|
{
|
|
'type': 'less_than',
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be less than 42.24',
|
|
'input': Decimal('43'),
|
|
'ctx': {
|
|
'lt': 42.24,
|
|
},
|
|
},
|
|
],
|
|
),
|
|
(dict(ge=Decimal('42.24')), Decimal('43'), Decimal('43')),
|
|
(dict(ge=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')),
|
|
(
|
|
dict(ge=Decimal('42.24')),
|
|
Decimal('42'),
|
|
[
|
|
{
|
|
'type': 'greater_than_equal',
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be greater than or equal to 42.24',
|
|
'input': Decimal('42'),
|
|
'ctx': {
|
|
'ge': 42.24,
|
|
},
|
|
}
|
|
],
|
|
),
|
|
(dict(le=Decimal('42.24')), Decimal('42'), Decimal('42')),
|
|
(dict(le=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')),
|
|
(
|
|
dict(le=Decimal('42.24')),
|
|
Decimal('43'),
|
|
[
|
|
{
|
|
'type': 'less_than_equal',
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be less than or equal to 42.24',
|
|
'input': Decimal('43'),
|
|
'ctx': {
|
|
'le': 42.24,
|
|
},
|
|
}
|
|
],
|
|
),
|
|
(dict(max_digits=2, decimal_places=2), Decimal('0.99'), Decimal('0.99')),
|
|
(
|
|
dict(max_digits=2, decimal_places=1),
|
|
Decimal('0.99'),
|
|
[
|
|
{
|
|
'type': 'decimal_max_places',
|
|
'loc': ('foo',),
|
|
'msg': 'ensure that there are no more than 1 decimal places',
|
|
'input': Decimal('0.99'),
|
|
'ctx': {
|
|
'decimal_places': 1,
|
|
},
|
|
}
|
|
],
|
|
),
|
|
(
|
|
dict(max_digits=3, decimal_places=1),
|
|
Decimal('999'),
|
|
[
|
|
{
|
|
'loc': ('foo',),
|
|
'msg': 'ensure that there are no more than 2 digits before the decimal point',
|
|
'type': 'decimal_whole_digits',
|
|
'input': Decimal('999'),
|
|
'ctx': {'whole_digits': 2},
|
|
}
|
|
],
|
|
),
|
|
(dict(max_digits=4, decimal_places=1), Decimal('999'), Decimal('999')),
|
|
(dict(max_digits=20, decimal_places=2), Decimal('742403889818000000'), Decimal('742403889818000000')),
|
|
(dict(max_digits=20, decimal_places=2), Decimal('7.42403889818E+17'), Decimal('7.42403889818E+17')),
|
|
(
|
|
dict(max_digits=20, decimal_places=2),
|
|
Decimal('7424742403889818000000'),
|
|
[
|
|
{
|
|
'type': 'decimal_max_digits',
|
|
'loc': ('foo',),
|
|
'msg': 'ensure that there are no more than 20 digits in total',
|
|
'input': Decimal('7424742403889818000000'),
|
|
'ctx': {
|
|
'max_digits': 20,
|
|
},
|
|
},
|
|
],
|
|
),
|
|
(dict(max_digits=5, decimal_places=2), Decimal('7304E-1'), Decimal('7304E-1')),
|
|
(
|
|
dict(max_digits=5, decimal_places=2),
|
|
Decimal('7304E-3'),
|
|
[
|
|
{
|
|
'type': 'decimal_max_places',
|
|
'loc': ('foo',),
|
|
'msg': 'ensure that there are no more than 2 decimal places',
|
|
'input': Decimal('7.304'),
|
|
'ctx': {'decimal_places': 2},
|
|
}
|
|
],
|
|
),
|
|
(dict(max_digits=5, decimal_places=5), Decimal('70E-5'), Decimal('70E-5')),
|
|
(
|
|
dict(max_digits=5, decimal_places=5),
|
|
Decimal('70E-6'),
|
|
[
|
|
{
|
|
'loc': ('foo',),
|
|
'msg': 'ensure that there are no more than 5 digits in total',
|
|
'type': 'decimal_max_digits',
|
|
'input': Decimal('0.000070'),
|
|
'ctx': {'max_digits': 5},
|
|
}
|
|
],
|
|
),
|
|
*[
|
|
(
|
|
dict(decimal_places=2, max_digits=10, allow_inf_nan=False),
|
|
value,
|
|
[
|
|
{
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be a finite number',
|
|
'type': 'finite_number',
|
|
'input': value,
|
|
}
|
|
],
|
|
)
|
|
for value in non_finite_values
|
|
],
|
|
*[
|
|
(
|
|
dict(decimal_places=2, max_digits=10, allow_inf_nan=False),
|
|
Decimal(value),
|
|
[
|
|
{
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be a finite number',
|
|
'type': 'finite_number',
|
|
'input': AnyThing(),
|
|
}
|
|
],
|
|
)
|
|
for value in non_finite_values
|
|
],
|
|
(
|
|
dict(multiple_of=Decimal('5')),
|
|
Decimal('42'),
|
|
[
|
|
{
|
|
'type': 'decimal_multiple_of',
|
|
'loc': ('foo',),
|
|
'msg': 'Input should be a multiple of 5',
|
|
'input': Decimal('42'),
|
|
'ctx': {
|
|
'multiple_of': Decimal('5'),
|
|
},
|
|
}
|
|
],
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize('mode', ['Field', 'condecimal'])
|
|
def test_decimal_validation(mode, type_args, value, result):
|
|
if mode == 'Field':
|
|
|
|
class Model(BaseModel):
|
|
foo: Decimal = Field(**type_args)
|
|
|
|
else:
|
|
|
|
class Model(BaseModel):
|
|
foo: condecimal(**type_args)
|
|
|
|
if not isinstance(result, Decimal):
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
m = Model(foo=value)
|
|
print(f'unexpected result: {m!r}')
|
|
# debug(exc_info.value.errors())
|
|
assert exc_info.value.errors() == result
|
|
# assert exc_info.value.json().startswith('[')
|
|
else:
|
|
assert Model(foo=value).foo == result
|
|
|
|
|
|
@pytest.fixture(scope='module', name='AllowInfModel')
|
|
def fix_allow_inf_model():
|
|
class Model(BaseModel):
|
|
v: condecimal(allow_inf_nan=True)
|
|
|
|
return Model
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
[
|
|
(Decimal('42'), 'unchanged'),
|
|
*[(v, 'is_nan') for v in nan_values],
|
|
*[(v, 'is_pos_inf') for v in pos_int_values],
|
|
*[(v, 'is_neg_inf') for v in neg_int_values],
|
|
],
|
|
)
|
|
def test_decimal_not_finite(value, result, AllowInfModel):
|
|
m = AllowInfModel(v=value)
|
|
if result == 'unchanged':
|
|
assert m.v == value
|
|
elif result == 'is_nan':
|
|
assert m.v.is_nan(), m.v
|
|
elif result == 'is_pos_inf':
|
|
assert m.v.is_infinite() and m.v > 0, m.v
|
|
else:
|
|
assert result == 'is_neg_inf'
|
|
assert m.v.is_infinite() and m.v < 0, m.v
|
|
|
|
|
|
def test_decimal_invalid():
|
|
with pytest.raises(ValueError, match='allow_inf_nan=True cannot be used with max_digits or decimal_places'):
|
|
|
|
class Model(BaseModel):
|
|
v: condecimal(allow_inf_nan=True, max_digits=4)
|
|
|
|
|
|
@pytest.mark.parametrize('value,result', (('/test/path', Path('/test/path')), (Path('/test/path'), Path('/test/path'))))
|
|
def test_path_validation_success(value, result):
|
|
class Model(BaseModel):
|
|
foo: Path
|
|
|
|
assert Model(foo=value).foo == result
|
|
|
|
|
|
def test_path_validation_fails():
|
|
class Model(BaseModel):
|
|
foo: Path
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=123)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'path_type', 'loc': ('foo',), 'msg': 'Input is not a valid path', 'input': 123}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
(('tests/test_types.py', Path('tests/test_types.py')), (Path('tests/test_types.py'), Path('tests/test_types.py'))),
|
|
)
|
|
def test_file_path_validation_success(value, result):
|
|
class Model(BaseModel):
|
|
foo: FilePath
|
|
|
|
assert Model(foo=value).foo == result
|
|
|
|
|
|
@pytest.mark.parametrize('value', ['nonexistentfile', Path('nonexistentfile'), 'tests', Path('tests')])
|
|
def test_file_path_validation_fails(value):
|
|
class Model(BaseModel):
|
|
foo: FilePath
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=value)
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'path_not_file',
|
|
'loc': ('foo',),
|
|
'msg': 'Path does not point to a file',
|
|
'input': value,
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize('value,result', (('tests', Path('tests')), (Path('tests'), Path('tests'))))
|
|
def test_directory_path_validation_success(value, result):
|
|
class Model(BaseModel):
|
|
foo: DirectoryPath
|
|
|
|
assert Model(foo=value).foo == result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value', ['nonexistentdirectory', Path('nonexistentdirectory'), 'tests/test_t.py', Path('tests/test_ypestypes.py')]
|
|
)
|
|
def test_directory_path_validation_fails(value):
|
|
class Model(BaseModel):
|
|
foo: DirectoryPath
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(foo=value)
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'path_not_directory',
|
|
'loc': ('foo',),
|
|
'msg': 'Path does not point to a directory',
|
|
'input': value,
|
|
}
|
|
]
|
|
|
|
|
|
def test_number_gt():
|
|
class Model(BaseModel):
|
|
a: conint(gt=-1) = 0
|
|
|
|
assert Model(a=0).model_dump() == {'a': 0}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=-1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'greater_than',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be greater than -1',
|
|
'input': -1,
|
|
'ctx': {'gt': -1},
|
|
}
|
|
]
|
|
|
|
|
|
def test_number_ge():
|
|
class Model(BaseModel):
|
|
a: conint(ge=0) = 0
|
|
|
|
assert Model(a=0).model_dump() == {'a': 0}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=-1)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'greater_than_equal',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be greater than or equal to 0',
|
|
'input': -1,
|
|
'ctx': {'ge': 0},
|
|
}
|
|
]
|
|
|
|
|
|
def test_number_lt():
|
|
class Model(BaseModel):
|
|
a: conint(lt=5) = 0
|
|
|
|
assert Model(a=4).model_dump() == {'a': 4}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=5)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'less_than',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be less than 5',
|
|
'input': 5,
|
|
'ctx': {'lt': 5},
|
|
}
|
|
]
|
|
|
|
|
|
def test_number_le():
|
|
class Model(BaseModel):
|
|
a: conint(le=5) = 0
|
|
|
|
assert Model(a=5).model_dump() == {'a': 5}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=6)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'less_than_equal',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be less than or equal to 5',
|
|
'input': 6,
|
|
'ctx': {'le': 5},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize('value', (10, 100, 20))
|
|
def test_number_multiple_of_int_valid(value):
|
|
class Model(BaseModel):
|
|
a: conint(multiple_of=5)
|
|
|
|
assert Model(a=value).model_dump() == {'a': value}
|
|
|
|
|
|
@pytest.mark.parametrize('value', [1337, 23, 6, 14])
|
|
def test_number_multiple_of_int_invalid(value):
|
|
class Model(BaseModel):
|
|
a: conint(multiple_of=5)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=value)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'multiple_of',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be a multiple of 5',
|
|
'input': value,
|
|
'ctx': {'multiple_of': 5},
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize('value', [0.2, 0.3, 0.4, 0.5, 1])
|
|
def test_number_multiple_of_float_valid(value):
|
|
class Model(BaseModel):
|
|
a: confloat(multiple_of=0.1)
|
|
|
|
assert Model(a=value).model_dump() == {'a': value}
|
|
|
|
|
|
@pytest.mark.parametrize('value', [0.07, 1.27, 1.003])
|
|
def test_number_multiple_of_float_invalid(value):
|
|
class Model(BaseModel):
|
|
a: confloat(multiple_of=0.1)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a=value)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'multiple_of',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be a multiple of 0.1',
|
|
'input': value,
|
|
'ctx': {'multiple_of': 0.1},
|
|
}
|
|
]
|
|
|
|
|
|
def test_new_type_success():
|
|
a_type = NewType('a_type', int)
|
|
b_type = NewType('b_type', a_type)
|
|
c_type = NewType('c_type', List[int])
|
|
|
|
class Model(BaseModel):
|
|
a: a_type
|
|
b: b_type
|
|
c: c_type
|
|
|
|
m = Model(a=42, b=24, c=[1, 2, 3])
|
|
assert m.model_dump() == {'a': 42, 'b': 24, 'c': [1, 2, 3]}
|
|
|
|
|
|
def test_new_type_fails():
|
|
a_type = NewType('a_type', int)
|
|
b_type = NewType('b_type', a_type)
|
|
c_type = NewType('c_type', List[int])
|
|
|
|
class Model(BaseModel):
|
|
a: a_type
|
|
b: b_type
|
|
c: c_type
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a='foo', b='bar', c=['foo'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('a',),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'foo',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('b',),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'bar',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('c', 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'foo',
|
|
},
|
|
]
|
|
|
|
|
|
def test_valid_simple_json():
|
|
class JsonModel(BaseModel):
|
|
json_obj: Json
|
|
|
|
obj = '{"a": 1, "b": [2, 3]}'
|
|
assert JsonModel(json_obj=obj).model_dump() == {'json_obj': {'a': 1, 'b': [2, 3]}}
|
|
|
|
|
|
def test_valid_simple_json_any():
|
|
class JsonModel(BaseModel):
|
|
json_obj: Json[Any]
|
|
|
|
obj = '{"a": 1, "b": [2, 3]}'
|
|
assert JsonModel(json_obj=obj).model_dump() == {'json_obj': {'a': 1, 'b': [2, 3]}}
|
|
|
|
|
|
@pytest.mark.parametrize('gen_type', [lambda: Json, lambda: Json[Any]])
|
|
def test_invalid_simple_json(gen_type):
|
|
t = gen_type()
|
|
|
|
class JsonModel(BaseModel):
|
|
json_obj: t
|
|
|
|
obj = '{a: 1, b: [2, 3]}'
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
JsonModel(json_obj=obj)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'json_invalid',
|
|
'loc': ('json_obj',),
|
|
'msg': 'Invalid JSON: key must be a string at line 1 column 2',
|
|
'input': '{a: 1, b: [2, 3]}',
|
|
'ctx': {'error': 'key must be a string at line 1 column 2'},
|
|
}
|
|
]
|
|
|
|
|
|
def test_valid_simple_json_bytes():
|
|
class JsonModel(BaseModel):
|
|
json_obj: Json
|
|
|
|
obj = b'{"a": 1, "b": [2, 3]}'
|
|
assert JsonModel(json_obj=obj).model_dump() == {'json_obj': {'a': 1, 'b': [2, 3]}}
|
|
|
|
|
|
def test_valid_detailed_json():
|
|
class JsonDetailedModel(BaseModel):
|
|
json_obj: Json[List[int]]
|
|
|
|
obj = '[1, 2, 3]'
|
|
assert JsonDetailedModel(json_obj=obj).model_dump() == {'json_obj': [1, 2, 3]}
|
|
|
|
obj = b'[1, 2, 3]'
|
|
assert JsonDetailedModel(json_obj=obj).model_dump() == {'json_obj': [1, 2, 3]}
|
|
|
|
obj = '(1, 2, 3)'
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
JsonDetailedModel(json_obj=obj)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'json_invalid',
|
|
'loc': ('json_obj',),
|
|
'msg': 'Invalid JSON: expected value at line 1 column 1',
|
|
'input': '(1, 2, 3)',
|
|
'ctx': {'error': 'expected value at line 1 column 1'},
|
|
}
|
|
]
|
|
|
|
|
|
def test_valid_model_json():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: List[int]
|
|
|
|
class JsonDetailedModel(BaseModel):
|
|
json_obj: Json[Model]
|
|
|
|
obj = '{"a": 1, "b": [2, 3]}'
|
|
m = JsonDetailedModel(json_obj=obj)
|
|
assert isinstance(m.json_obj, Model)
|
|
assert m.json_obj.a == 1
|
|
assert m.model_dump() == {'json_obj': {'a': 1, 'b': [2, 3]}}
|
|
|
|
|
|
def test_invalid_model_json():
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: List[int]
|
|
|
|
class JsonDetailedModel(BaseModel):
|
|
json_obj: Json[Model]
|
|
|
|
obj = '{"a": 1, "c": [2, 3]}'
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
JsonDetailedModel(json_obj=obj)
|
|
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'missing', 'loc': ('json_obj', 'b'), 'msg': 'Field required', 'input': {'a': 1, 'c': [2, 3]}}
|
|
]
|
|
|
|
|
|
def test_invalid_detailed_json_type_error():
|
|
class JsonDetailedModel(BaseModel):
|
|
json_obj: Json[List[int]]
|
|
|
|
obj = '["a", "b", "c"]'
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
JsonDetailedModel(json_obj=obj)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('json_obj', 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('json_obj', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'b',
|
|
},
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('json_obj', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'c',
|
|
},
|
|
]
|
|
|
|
|
|
def test_json_not_str():
|
|
class JsonDetailedModel(BaseModel):
|
|
json_obj: Json[List[int]]
|
|
|
|
obj = 12
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
JsonDetailedModel(json_obj=obj)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'json_type',
|
|
'loc': ('json_obj',),
|
|
'msg': 'JSON input should be string, bytes or bytearray',
|
|
'input': 12,
|
|
}
|
|
]
|
|
|
|
|
|
def test_json_pre_validator():
|
|
call_count = 0
|
|
|
|
class JsonModel(BaseModel):
|
|
json_obj: Json
|
|
|
|
@validator('json_obj', mode='before')
|
|
def check(cls, v, **kwargs):
|
|
assert v == '"foobar"'
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return v
|
|
|
|
assert JsonModel(json_obj='"foobar"').model_dump() == {'json_obj': 'foobar'}
|
|
assert call_count == 1
|
|
|
|
|
|
def test_json_optional_simple():
|
|
class JsonOptionalModel(BaseModel):
|
|
json_obj: Optional[Json]
|
|
|
|
assert JsonOptionalModel(json_obj=None).model_dump() == {'json_obj': None}
|
|
assert JsonOptionalModel(json_obj='["x", "y", "z"]').model_dump() == {'json_obj': ['x', 'y', 'z']}
|
|
|
|
|
|
def test_json_optional_complex():
|
|
class JsonOptionalModel(BaseModel):
|
|
json_obj: Optional[Json[List[int]]]
|
|
|
|
JsonOptionalModel(json_obj=None)
|
|
|
|
good = JsonOptionalModel(json_obj='[1, 2, 3]')
|
|
assert good.json_obj == [1, 2, 3]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
JsonOptionalModel(json_obj='["i should fail"]')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('json_obj', 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'i should fail',
|
|
}
|
|
]
|
|
|
|
|
|
def test_json_required():
|
|
class JsonRequired(BaseModel):
|
|
json_obj: Json
|
|
|
|
assert JsonRequired(json_obj='["x", "y", "z"]').model_dump() == {'json_obj': ['x', 'y', 'z']}
|
|
with pytest.raises(ValidationError, match=r'JSON input should be string, bytes or bytearray \[type=json_type,'):
|
|
JsonRequired(json_obj=None)
|
|
with pytest.raises(ValidationError, match=r'Field required \[type=missing,'):
|
|
JsonRequired()
|
|
|
|
|
|
@pytest.mark.parametrize('pattern_type', [re.Pattern, Pattern])
|
|
def test_pattern(pattern_type):
|
|
class Foobar(BaseModel):
|
|
pattern: pattern_type
|
|
|
|
f = Foobar(pattern=r'^whatev.r\d$')
|
|
assert f.pattern.__class__.__name__ == 'Pattern'
|
|
# check it's really a proper pattern
|
|
assert f.pattern.match('whatever1')
|
|
assert not f.pattern.match(' whatever1')
|
|
|
|
# Check that pre-compiled patterns are accepted unchanged
|
|
p = re.compile(r'^whatev.r\d$')
|
|
f2 = Foobar(pattern=p)
|
|
assert f2.pattern is p
|
|
|
|
# assert Foobar.model_json_schema() == {
|
|
# 'type': 'object',
|
|
# 'title': 'Foobar',
|
|
# 'properties': {'pattern': {'type': 'string', 'format': 'regex', 'title': 'Pattern'}},
|
|
# 'required': ['pattern'],
|
|
# }
|
|
|
|
|
|
@pytest.mark.parametrize('pattern_type', [re.Pattern, Pattern])
|
|
def test_pattern_error(pattern_type):
|
|
class Foobar(BaseModel):
|
|
pattern: pattern_type
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(pattern='[xx')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'pattern_regex',
|
|
'loc': ('pattern',),
|
|
'msg': 'Input should be a valid regular expression',
|
|
'input': '[xx',
|
|
}
|
|
]
|
|
|
|
|
|
def test_secretstr():
|
|
class Foobar(BaseModel):
|
|
password: SecretStr
|
|
empty_password: SecretStr
|
|
|
|
# Initialize the model.
|
|
f = Foobar(password='1234', empty_password='')
|
|
|
|
# Assert correct types.
|
|
assert f.password.__class__.__name__ == 'SecretStr'
|
|
assert f.empty_password.__class__.__name__ == 'SecretStr'
|
|
|
|
# Assert str and repr are correct.
|
|
assert str(f.password) == '**********'
|
|
assert str(f.empty_password) == ''
|
|
assert repr(f.password) == "SecretStr('**********')"
|
|
assert repr(f.empty_password) == "SecretStr('')"
|
|
|
|
# Assert retrieval of secret value is correct
|
|
assert f.password.get_secret_value() == '1234'
|
|
assert f.empty_password.get_secret_value() == ''
|
|
|
|
|
|
def test_secretstr_is_secret_field():
|
|
assert issubclass(SecretStr, SecretField)
|
|
|
|
|
|
def test_secretstr_equality():
|
|
assert SecretStr('abc') == SecretStr('abc')
|
|
assert SecretStr('123') != SecretStr('321')
|
|
assert SecretStr('123') != '123'
|
|
assert SecretStr('123') is not SecretStr('123')
|
|
|
|
|
|
def test_secretstr_idempotent():
|
|
class Foobar(BaseModel):
|
|
password: SecretStr
|
|
|
|
# Should not raise an exception
|
|
m = Foobar(password=SecretStr('1234'))
|
|
assert m.password.get_secret_value() == '1234'
|
|
|
|
|
|
def test_secretstr_is_hashable():
|
|
assert type(hash(SecretStr('secret'))) is int
|
|
|
|
|
|
def test_secretstr_error():
|
|
class Foobar(BaseModel):
|
|
password: SecretStr
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(password=[6, 23, 'abc'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_type',
|
|
'loc': ('password',),
|
|
'msg': 'Input should be a valid string',
|
|
'input': [6, 23, 'abc'],
|
|
}
|
|
]
|
|
|
|
|
|
def test_secret_str_min_max_length():
|
|
class Foobar(BaseModel):
|
|
password: SecretStr = Field(min_length=6, max_length=10)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(password='')
|
|
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_too_short',
|
|
'loc': ('password',),
|
|
'msg': 'String should have at least 6 characters',
|
|
'input': '',
|
|
'ctx': {'min_length': 6},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(password='1' * 20)
|
|
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'string_too_long',
|
|
'loc': ('password',),
|
|
'msg': 'String should have at most 10 characters',
|
|
'input': '11111111111111111111',
|
|
'ctx': {'max_length': 10},
|
|
}
|
|
]
|
|
|
|
value = '1' * 8
|
|
assert Foobar(password=value).password.get_secret_value() == value
|
|
|
|
|
|
def test_secretbytes():
|
|
class Foobar(BaseModel):
|
|
password: SecretBytes
|
|
empty_password: SecretBytes
|
|
|
|
# Initialize the model.
|
|
f = Foobar(password=b'wearebytes', empty_password=b'')
|
|
|
|
# Assert correct types.
|
|
assert f.password.__class__.__name__ == 'SecretBytes'
|
|
assert f.empty_password.__class__.__name__ == 'SecretBytes'
|
|
|
|
# Assert str and repr are correct.
|
|
assert str(f.password) == "b'**********'"
|
|
assert str(f.empty_password) == "b''"
|
|
assert repr(f.password) == "SecretBytes(b'**********')"
|
|
assert repr(f.empty_password) == "SecretBytes(b'')"
|
|
|
|
# Assert retrieval of secret value is correct
|
|
assert f.password.get_secret_value() == b'wearebytes'
|
|
assert f.empty_password.get_secret_value() == b''
|
|
|
|
# Assert that SecretBytes is equal to SecretBytes if the secret is the same.
|
|
assert f == f.copy()
|
|
assert f != f.copy(update=dict(password=b'4321'))
|
|
|
|
|
|
def test_secretbytes_is_secret_field():
|
|
assert issubclass(SecretBytes, SecretField)
|
|
|
|
|
|
def test_secretbytes_equality():
|
|
assert SecretBytes(b'abc') == SecretBytes(b'abc')
|
|
assert SecretBytes(b'123') != SecretBytes(b'321')
|
|
assert SecretBytes(b'123') != b'123'
|
|
assert SecretBytes(b'123') is not SecretBytes(b'123')
|
|
|
|
|
|
def test_secretbytes_idempotent():
|
|
class Foobar(BaseModel):
|
|
password: SecretBytes
|
|
|
|
# Should not raise an exception.
|
|
_ = Foobar(password=SecretBytes(b'1234'))
|
|
|
|
|
|
def test_secretbytes_is_hashable():
|
|
assert type(hash(SecretBytes(b'secret'))) is int
|
|
|
|
|
|
def test_secretbytes_error():
|
|
class Foobar(BaseModel):
|
|
password: SecretBytes
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(password=[6, 23, 'abc'])
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'bytes_type',
|
|
'loc': ('password',),
|
|
'msg': 'Input should be a valid bytes',
|
|
'input': [6, 23, 'abc'],
|
|
}
|
|
]
|
|
|
|
|
|
def test_secret_bytes_min_max_length():
|
|
class Foobar(BaseModel):
|
|
password: SecretBytes = Field(min_length=6, max_length=10)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(password=b'')
|
|
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'bytes_too_short',
|
|
'loc': ('password',),
|
|
'msg': 'Data should have at least 6 bytes',
|
|
'input': b'',
|
|
'ctx': {'min_length': 6},
|
|
}
|
|
]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(password=b'1' * 20)
|
|
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'bytes_too_long',
|
|
'loc': ('password',),
|
|
'msg': 'Data should have at most 10 bytes',
|
|
'input': b'11111111111111111111',
|
|
'ctx': {'max_length': 10},
|
|
}
|
|
]
|
|
|
|
value = b'1' * 8
|
|
assert Foobar(password=value).password.get_secret_value() == value
|
|
|
|
|
|
def test_generic_without_params():
|
|
class Model(BaseModel):
|
|
generic_list: List
|
|
generic_dict: Dict
|
|
generic_tuple: Tuple
|
|
|
|
m = Model(generic_list=[0, 'a'], generic_dict={0: 'a', 'a': 0}, generic_tuple=(1, 'q'))
|
|
assert m.model_dump() == {'generic_list': [0, 'a'], 'generic_dict': {0: 'a', 'a': 0}, 'generic_tuple': (1, 'q')}
|
|
|
|
|
|
def test_generic_without_params_error():
|
|
class Model(BaseModel):
|
|
generic_list: List
|
|
generic_dict: Dict
|
|
generic_tuple: Tuple
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(generic_list=0, generic_dict=0, generic_tuple=0)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('generic_list',),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': 0,
|
|
},
|
|
{
|
|
'type': 'dict_type',
|
|
'loc': ('generic_dict',),
|
|
'msg': 'Input should be a valid dictionary',
|
|
'input': 0,
|
|
},
|
|
{'type': 'tuple_type', 'loc': ('generic_tuple',), 'msg': 'Input should be a valid tuple', 'input': 0},
|
|
]
|
|
|
|
|
|
def test_literal_single():
|
|
class Model(BaseModel):
|
|
a: Literal['a']
|
|
|
|
Model(a='a')
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a='b')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'literal_error',
|
|
'loc': ('a',),
|
|
'msg': "Input should be 'a'",
|
|
'input': 'b',
|
|
'ctx': {'expected': "'a'"},
|
|
}
|
|
]
|
|
|
|
|
|
def test_literal_multiple():
|
|
class Model(BaseModel):
|
|
a_or_b: Literal['a', 'b']
|
|
|
|
Model(a_or_b='a')
|
|
Model(a_or_b='b')
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(a_or_b='c')
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{
|
|
'type': 'literal_error',
|
|
'loc': ('a_or_b',),
|
|
'msg': "Input should be 'a' or 'b'",
|
|
'input': 'c',
|
|
'ctx': {'expected': "'a' or 'b'"},
|
|
}
|
|
]
|
|
|
|
|
|
def test_unsupported_field_type():
|
|
with pytest.raises(TypeError, match=r'Unable to generate pydantic-core schema MutableSet'):
|
|
|
|
class UnsupportedModel(BaseModel):
|
|
unsupported: MutableSet[int]
|
|
|
|
|
|
def test_frozenset_field():
|
|
class FrozenSetModel(BaseModel):
|
|
set: FrozenSet[int]
|
|
|
|
test_set = frozenset({1, 2, 3})
|
|
object_under_test = FrozenSetModel(set=test_set)
|
|
|
|
assert object_under_test.set == test_set
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'value,result',
|
|
[
|
|
([1, 2, 3], frozenset([1, 2, 3])),
|
|
({1, 2, 3}, frozenset([1, 2, 3])),
|
|
((1, 2, 3), frozenset([1, 2, 3])),
|
|
(deque([1, 2, 3]), frozenset([1, 2, 3])),
|
|
],
|
|
)
|
|
def test_frozenset_field_conversion(value, result):
|
|
class FrozenSetModel(BaseModel):
|
|
set: FrozenSet[int]
|
|
|
|
object_under_test = FrozenSetModel(set=value)
|
|
|
|
assert object_under_test.set == result
|
|
|
|
|
|
def test_frozenset_field_not_convertible():
|
|
class FrozenSetModel(BaseModel):
|
|
set: FrozenSet[int]
|
|
|
|
with pytest.raises(ValidationError, match=r'frozenset'):
|
|
FrozenSetModel(set=42)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'input_value,output,human_bin,human_dec',
|
|
(
|
|
('1', 1, '1B', '1B'),
|
|
('1.0', 1, '1B', '1B'),
|
|
('1b', 1, '1B', '1B'),
|
|
('1.5 KB', int(1.5e3), '1.5KiB', '1.5KB'),
|
|
('1.5 K', int(1.5e3), '1.5KiB', '1.5KB'),
|
|
('1.5 MB', int(1.5e6), '1.4MiB', '1.5MB'),
|
|
('1.5 M', int(1.5e6), '1.4MiB', '1.5MB'),
|
|
('5.1kib', 5222, '5.1KiB', '5.2KB'),
|
|
('6.2EiB', 7148113328562451456, '6.2EiB', '7.1EB'),
|
|
),
|
|
)
|
|
def test_bytesize_conversions(input_value, output, human_bin, human_dec):
|
|
class Model(BaseModel):
|
|
size: ByteSize
|
|
|
|
m = Model(size=input_value)
|
|
|
|
assert m.size == output
|
|
|
|
assert m.size.human_readable() == human_bin
|
|
assert m.size.human_readable(decimal=True) == human_dec
|
|
|
|
|
|
def test_bytesize_to():
|
|
class Model(BaseModel):
|
|
size: ByteSize
|
|
|
|
m = Model(size='1GiB')
|
|
|
|
assert m.size.to('MiB') == pytest.approx(1024)
|
|
assert m.size.to('MB') == pytest.approx(1073.741824)
|
|
assert m.size.to('TiB') == pytest.approx(0.0009765625)
|
|
|
|
|
|
def test_bytesize_raises():
|
|
class Model(BaseModel):
|
|
size: ByteSize
|
|
|
|
with pytest.raises(ValidationError, match='parse value'):
|
|
Model(size='d1MB')
|
|
|
|
with pytest.raises(ValidationError, match='byte unit'):
|
|
Model(size='1LiB')
|
|
|
|
# 1Gi is not a valid unit unlike 1G
|
|
with pytest.raises(ValidationError, match='byte unit'):
|
|
Model(size='1Gi')
|
|
|
|
m = Model(size='1MB')
|
|
with pytest.raises(PydanticCustomError, match='byte unit'):
|
|
m.size.to('bad_unit')
|
|
|
|
|
|
def test_deque_success():
|
|
class Model(BaseModel):
|
|
v: deque
|
|
|
|
assert Model(v=[1, 2, 3]).v == deque([1, 2, 3])
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'cls,value,result',
|
|
(
|
|
(int, [1, 2, 3], deque([1, 2, 3])),
|
|
(int, (1, 2, 3), deque((1, 2, 3))),
|
|
(int, deque((1, 2, 3)), deque((1, 2, 3))),
|
|
(float, [1.0, 2.0, 3.0], deque([1.0, 2.0, 3.0])),
|
|
(Set[int], [{1, 2}, {3, 4}, {5, 6}], deque([{1, 2}, {3, 4}, {5, 6}])),
|
|
(Tuple[int, str], ((1, 'a'), (2, 'b'), (3, 'c')), deque(((1, 'a'), (2, 'b'), (3, 'c')))),
|
|
(str, [w for w in 'one two three'.split()], deque(['one', 'two', 'three'])),
|
|
# (float, {1.0, 2.0, 3.0}, deque([1.0, 2.0, 3.0])),
|
|
# (int, frozenset([1, 2, 3]), deque([1, 2, 3])),
|
|
),
|
|
)
|
|
def test_deque_generic_success(cls, value, result):
|
|
class Model(BaseModel):
|
|
v: Deque[cls]
|
|
|
|
assert Model(v=value).v == result
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'cls,value,expected_error',
|
|
(
|
|
(
|
|
float,
|
|
{1, 2, 3},
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': {1, 2, 3},
|
|
},
|
|
),
|
|
(
|
|
float,
|
|
frozenset((1, 2, 3)),
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('v',),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': frozenset((1, 2, 3)),
|
|
},
|
|
),
|
|
(
|
|
int,
|
|
[1, 'a', 3],
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 1),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
),
|
|
(
|
|
int,
|
|
(1, 2, 'a'),
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 2),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
),
|
|
(
|
|
Tuple[int, str],
|
|
((1, 'a'), ('a', 'a'), (3, 'c')),
|
|
{
|
|
'type': 'int_parsing',
|
|
'loc': ('v', 1, 0),
|
|
'msg': 'Input should be a valid integer, unable to parse string as an integer',
|
|
'input': 'a',
|
|
},
|
|
),
|
|
(
|
|
List[int],
|
|
[{'a': 1, 'b': 2}, [1, 2], [2, 3]],
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('v', 0),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': {
|
|
'a': 1,
|
|
'b': 2,
|
|
},
|
|
},
|
|
),
|
|
),
|
|
)
|
|
def test_deque_fails(cls, value, expected_error):
|
|
class Model(BaseModel):
|
|
v: Deque[cls]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(v=value)
|
|
assert exc_info.value.error_count() == 1
|
|
# debug(exc_info.value.errors()[0])
|
|
assert exc_info.value.errors()[0] == expected_error
|
|
|
|
|
|
def test_deque_model():
|
|
class Model2(BaseModel):
|
|
x: int
|
|
|
|
class Model(BaseModel):
|
|
v: Deque[Model2]
|
|
|
|
seq = [Model2(x=1), Model2(x=2)]
|
|
assert Model(v=seq).v == deque(seq)
|
|
|
|
|
|
def test_deque_json():
|
|
class Model(BaseModel):
|
|
v: Deque[int]
|
|
|
|
assert Model(v=deque((1, 2, 3))).model_dump_json() == '{"v": [1, 2, 3]}'
|
|
|
|
|
|
@pytest.mark.parametrize('value_type', (None, type(None), None.__class__, Literal[None]))
|
|
def test_none(value_type):
|
|
class Model(BaseModel):
|
|
my_none: value_type
|
|
my_none_list: List[value_type]
|
|
my_none_dict: Dict[str, value_type]
|
|
my_json_none: Json[value_type]
|
|
|
|
Model(
|
|
my_none=None,
|
|
my_none_list=[None] * 3,
|
|
my_none_dict={'a': None, 'b': None},
|
|
my_json_none='null',
|
|
)
|
|
|
|
# assert Model.model_json_schema() == {
|
|
# 'title': 'Model',
|
|
# 'type': 'object',
|
|
# 'properties': {
|
|
# 'my_none': {'title': 'My None', 'type': 'null'},
|
|
# 'my_none_list': {
|
|
# 'title': 'My None List',
|
|
# 'type': 'array',
|
|
# 'items': {'type': 'null'},
|
|
# },
|
|
# 'my_none_dict': {
|
|
# 'title': 'My None Dict',
|
|
# 'type': 'object',
|
|
# 'additionalProperties': {'type': 'null'},
|
|
# },
|
|
# 'my_json_none': {'title': 'My Json None', 'type': 'null'},
|
|
# },
|
|
# 'required': ['my_none', 'my_none_list', 'my_none_dict', 'my_json_none'],
|
|
# }
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(
|
|
my_none='qwe',
|
|
my_none_list=[1, None, 'qwe'],
|
|
my_none_dict={'a': 1, 'b': None},
|
|
my_json_none='"a"',
|
|
)
|
|
# insert_assert(exc_info.value.errors())
|
|
assert exc_info.value.errors() == [
|
|
{'type': 'none_required', 'loc': ('my_none',), 'msg': 'Input should be None/null', 'input': 'qwe'},
|
|
{'type': 'none_required', 'loc': ('my_none_list', 0), 'msg': 'Input should be None/null', 'input': 1},
|
|
{
|
|
'type': 'none_required',
|
|
'loc': ('my_none_list', 2),
|
|
'msg': 'Input should be None/null',
|
|
'input': 'qwe',
|
|
},
|
|
{
|
|
'type': 'none_required',
|
|
'loc': ('my_none_dict', 'a'),
|
|
'msg': 'Input should be None/null',
|
|
'input': 1,
|
|
},
|
|
{'type': 'none_required', 'loc': ('my_json_none',), 'msg': 'Input should be None/null', 'input': 'a'},
|
|
]
|
|
|
|
|
|
def test_default_union_types():
|
|
class DefaultModel(BaseModel):
|
|
v: Union[int, bool, str]
|
|
|
|
# do it this way since `1 == True`
|
|
assert repr(DefaultModel(v=True).v) == 'True'
|
|
assert repr(DefaultModel(v=1).v) == '1'
|
|
assert repr(DefaultModel(v='1').v) == "'1'"
|
|
|
|
# assert DefaultModel.model_json_schema() == {
|
|
# 'title': 'DefaultModel',
|
|
# 'type': 'object',
|
|
# 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in ('integer', 'boolean', 'string')]}},
|
|
# 'required': ['v'],
|
|
# }
|
|
|
|
|
|
def test_default_union_class():
|
|
class A(BaseModel):
|
|
x: str
|
|
|
|
class B(BaseModel):
|
|
x: str
|
|
|
|
class Model(BaseModel):
|
|
y: Union[A, B]
|
|
|
|
assert isinstance(Model(y=A(x='a')).y, A)
|
|
assert isinstance(Model(y=B(x='b')).y, B)
|
|
|
|
|
|
def test_union_subclass():
|
|
class MyStr(str):
|
|
...
|
|
|
|
class Model(BaseModel):
|
|
x: Union[int, str]
|
|
|
|
# see https://github.com/pydantic/pydantic-core/pull/294, since subclasses are no-longer allowed as valid
|
|
# inputs to strict-string, this doesn't work
|
|
assert Model(x=MyStr('1')).x == 1
|
|
|
|
|
|
def test_union_compound_types():
|
|
class Model(BaseModel):
|
|
values: Union[Dict[str, str], List[str], Dict[str, List[str]]]
|
|
|
|
assert Model(values={'L': '1'}).model_dump() == {'values': {'L': '1'}}
|
|
assert Model(values=['L1']).model_dump() == {'values': ['L1']}
|
|
assert Model(values=('L1',)).model_dump() == {'values': ['L1']}
|
|
assert Model(values={'x': ['pika']}) == {'values': {'x': ['pika']}}
|
|
assert Model(values={'x': ('pika',)}).model_dump() == {'values': {'x': ['pika']}}
|
|
with pytest.raises(ValidationError) as e:
|
|
Model(values={'x': {'a': 'b'}})
|
|
# insert_assert(e.value.errors())
|
|
assert e.value.errors() == [
|
|
{
|
|
'type': 'string_type',
|
|
'loc': ('values', 'function-wrap[mapping_validator(), dict[str,str]]', 'x'),
|
|
'msg': 'Input should be a valid string',
|
|
'input': {'a': 'b'},
|
|
},
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('values', 'list[str]'),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': {'x': {'a': 'b'}},
|
|
},
|
|
{
|
|
'type': 'list_type',
|
|
'loc': ('values', 'function-wrap[mapping_validator(), dict[str,list[str]]]', 'x'),
|
|
'msg': 'Input should be a valid list/array',
|
|
'input': {'a': 'b'},
|
|
},
|
|
]
|
|
|
|
|
|
def test_smart_union_compounded_types_edge_case():
|
|
class Model(BaseModel):
|
|
x: Union[List[str], List[int]]
|
|
|
|
assert Model(x=[1, 2]).x == [1, 2]
|
|
assert Model(x=['1', '2']).x == ['1', '2']
|
|
assert Model(x=[1, '2']).x == [1, 2]
|
|
|
|
|
|
def test_union_typeddict():
|
|
class Dict1(TypedDict):
|
|
foo: str
|
|
|
|
class Dict2(TypedDict):
|
|
bar: str
|
|
|
|
class M(BaseModel):
|
|
d: Union[Dict2, Dict1]
|
|
|
|
assert M(d=dict(foo='baz')).d == {'foo': 'baz'}
|