mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
78921da353
* better str and repr for ModelField, fix #505 * better type display, fix tests * correct _type_display signature * fix for python3.6 differences * fix PyObjectStr * fix coverage
388 lines
10 KiB
Python
388 lines
10 KiB
Python
import sys
|
|
from typing import Dict, List, Optional, Union
|
|
from uuid import UUID, uuid4
|
|
|
|
import pytest
|
|
|
|
from pydantic import UUID1, BaseConfig, BaseModel, PydanticTypeError, ValidationError, conint, errors, validator
|
|
from pydantic.error_wrappers import flatten_errors, get_exc_type
|
|
|
|
try:
|
|
from typing_extensions import Literal
|
|
except ImportError:
|
|
Literal = None
|
|
|
|
|
|
def test_pydantic_error():
|
|
class TestError(PydanticTypeError):
|
|
code = 'test_code'
|
|
msg_template = 'test message template "{test_ctx}"'
|
|
|
|
def __init__(self, *, test_ctx: int) -> None:
|
|
super().__init__(test_ctx=test_ctx)
|
|
|
|
with pytest.raises(TestError) as exc_info:
|
|
raise TestError(test_ctx='test_value')
|
|
assert str(exc_info.value) == 'test message template "test_value"'
|
|
|
|
|
|
@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
|
|
def test_interval_validation_error():
|
|
class Foo(BaseModel):
|
|
model_type: Literal['foo']
|
|
f: int
|
|
|
|
class Bar(BaseModel):
|
|
model_type: Literal['bar']
|
|
b: int
|
|
|
|
class MyModel(BaseModel):
|
|
foobar: Union[Foo, Bar]
|
|
|
|
@validator('foobar', pre=True)
|
|
def check_action(cls, v):
|
|
if isinstance(v, dict):
|
|
model_type = v.get('model_type')
|
|
if model_type == 'foo':
|
|
return Foo(**v)
|
|
if model_type == 'bar':
|
|
return Bar(**v)
|
|
raise ValueError('not valid Foo or Bar')
|
|
|
|
m1 = MyModel(foobar={'model_type': 'foo', 'f': '1'})
|
|
assert m1.foobar.f == 1
|
|
assert isinstance(m1.foobar, Foo)
|
|
|
|
m2 = MyModel(foobar={'model_type': 'bar', 'b': '2'})
|
|
assert m2.foobar.b == 2
|
|
assert isinstance(m2.foobar, BaseModel)
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
MyModel(foobar={'model_type': 'foo', 'f': 'x'})
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('foobar', 'f'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6')
|
|
def test_error_on_optional():
|
|
class Foobar(BaseModel):
|
|
foo: Optional[str] = None
|
|
|
|
@validator('foo', always=True, pre=True)
|
|
def check_foo(cls, v):
|
|
raise ValueError('custom error')
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(foo='x')
|
|
assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'custom error', 'type': 'value_error'}]
|
|
assert repr(exc_info.value.raw_errors[0]) == "ErrorWrapper(exc=ValueError('custom error'), loc=('foo',))"
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Foobar(foo=None)
|
|
assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'custom error', 'type': 'value_error'}]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'result,expected',
|
|
(
|
|
(
|
|
'errors',
|
|
[
|
|
{'loc': ('a',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('b', 'x'), 'msg': 'field required', 'type': 'value_error.missing'},
|
|
{'loc': ('b', 'z'), 'msg': 'field required', 'type': 'value_error.missing'},
|
|
{'loc': ('c', 0, 'x'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('d',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('d',), 'msg': 'value is not a valid uuid', 'type': 'type_error.uuid'},
|
|
{'loc': ('e', '__key__'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'},
|
|
{'loc': ('f', 0), 'msg': 'none is not an allowed value', 'type': 'type_error.none.not_allowed'},
|
|
{
|
|
'loc': ('g',),
|
|
'msg': 'uuid version 1 expected',
|
|
'type': 'value_error.uuid.version',
|
|
'ctx': {'required_version': 1},
|
|
},
|
|
{
|
|
'loc': ('h',),
|
|
'msg': 'yet another error message template 42',
|
|
'type': 'value_error.number.not_gt',
|
|
'ctx': {'limit_value': 42},
|
|
},
|
|
],
|
|
),
|
|
(
|
|
'json',
|
|
"""\
|
|
[
|
|
{
|
|
"loc": [
|
|
"a"
|
|
],
|
|
"msg": "value is not a valid integer",
|
|
"type": "type_error.integer"
|
|
},
|
|
{
|
|
"loc": [
|
|
"b",
|
|
"x"
|
|
],
|
|
"msg": "field required",
|
|
"type": "value_error.missing"
|
|
},
|
|
{
|
|
"loc": [
|
|
"b",
|
|
"z"
|
|
],
|
|
"msg": "field required",
|
|
"type": "value_error.missing"
|
|
},
|
|
{
|
|
"loc": [
|
|
"c",
|
|
0,
|
|
"x"
|
|
],
|
|
"msg": "value is not a valid integer",
|
|
"type": "type_error.integer"
|
|
},
|
|
{
|
|
"loc": [
|
|
"d"
|
|
],
|
|
"msg": "value is not a valid integer",
|
|
"type": "type_error.integer"
|
|
},
|
|
{
|
|
"loc": [
|
|
"d"
|
|
],
|
|
"msg": "value is not a valid uuid",
|
|
"type": "type_error.uuid"
|
|
},
|
|
{
|
|
"loc": [
|
|
"e",
|
|
"__key__"
|
|
],
|
|
"msg": "value is not a valid integer",
|
|
"type": "type_error.integer"
|
|
},
|
|
{
|
|
"loc": [
|
|
"f",
|
|
0
|
|
],
|
|
"msg": "none is not an allowed value",
|
|
"type": "type_error.none.not_allowed"
|
|
},
|
|
{
|
|
"loc": [
|
|
"g"
|
|
],
|
|
"msg": "uuid version 1 expected",
|
|
"type": "value_error.uuid.version",
|
|
"ctx": {
|
|
"required_version": 1
|
|
}
|
|
},
|
|
{
|
|
"loc": [
|
|
"h"
|
|
],
|
|
"msg": "yet another error message template 42",
|
|
"type": "value_error.number.not_gt",
|
|
"ctx": {
|
|
"limit_value": 42
|
|
}
|
|
}
|
|
]""",
|
|
),
|
|
(
|
|
'__str__',
|
|
"""\
|
|
10 validation errors for Model
|
|
a
|
|
value is not a valid integer (type=type_error.integer)
|
|
b -> x
|
|
field required (type=value_error.missing)
|
|
b -> z
|
|
field required (type=value_error.missing)
|
|
c -> 0 -> x
|
|
value is not a valid integer (type=type_error.integer)
|
|
d
|
|
value is not a valid integer (type=type_error.integer)
|
|
d
|
|
value is not a valid uuid (type=type_error.uuid)
|
|
e -> __key__
|
|
value is not a valid integer (type=type_error.integer)
|
|
f -> 0
|
|
none is not an allowed value (type=type_error.none.not_allowed)
|
|
g
|
|
uuid version 1 expected (type=value_error.uuid.version; required_version=1)
|
|
h
|
|
yet another error message template 42 (type=value_error.number.not_gt; limit_value=42)""",
|
|
),
|
|
),
|
|
)
|
|
def test_validation_error(result, expected):
|
|
class SubModel(BaseModel):
|
|
x: int
|
|
y: int
|
|
z: str
|
|
|
|
class Model(BaseModel):
|
|
a: int
|
|
b: SubModel
|
|
c: List[SubModel]
|
|
d: Union[int, UUID]
|
|
e: Dict[int, str]
|
|
f: List[Union[int, str]]
|
|
g: UUID1
|
|
h: conint(gt=42)
|
|
|
|
class Config:
|
|
error_msg_templates = {'value_error.number.not_gt': 'yet another error message template {limit_value}'}
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model.parse_obj(
|
|
{
|
|
'a': 'not_int',
|
|
'b': {'y': 42},
|
|
'c': [{'x': 'not_int', 'y': 42, 'z': 'string'}],
|
|
'd': 'string',
|
|
'e': {'not_int': 'string'},
|
|
'f': [None],
|
|
'g': uuid4(),
|
|
'h': 21,
|
|
}
|
|
)
|
|
|
|
assert getattr(exc_info.value, result)() == expected
|
|
|
|
|
|
def test_errors_unknown_error_object():
|
|
with pytest.raises(RuntimeError):
|
|
list(flatten_errors([object], BaseConfig))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'exc,type_',
|
|
(
|
|
(TypeError(), 'type_error'),
|
|
(ValueError(), 'value_error'),
|
|
(AssertionError(), 'assertion_error'),
|
|
(errors.DecimalIsNotFiniteError(), 'value_error.decimal.not_finite'),
|
|
),
|
|
)
|
|
def test_get_exc_type(exc, type_):
|
|
if isinstance(type_, str):
|
|
assert get_exc_type(type(exc)) == type_
|
|
else:
|
|
with pytest.raises(type_) as exc_info:
|
|
get_exc_type(type(exc))
|
|
assert isinstance(exc_info.value, type_)
|
|
|
|
|
|
def test_single_error():
|
|
class Model(BaseModel):
|
|
x: int
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(x='x')
|
|
|
|
expected = """\
|
|
1 validation error for Model
|
|
x
|
|
value is not a valid integer (type=type_error.integer)"""
|
|
assert str(exc_info.value) == expected
|
|
assert str(exc_info.value) == expected # to check lru cache doesn't break anything
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model()
|
|
|
|
assert (
|
|
str(exc_info.value)
|
|
== """\
|
|
1 validation error for Model
|
|
x
|
|
field required (type=value_error.missing)"""
|
|
)
|
|
|
|
|
|
def test_nested_error():
|
|
class NestedModel3(BaseModel):
|
|
x: str
|
|
|
|
class NestedModel2(BaseModel):
|
|
data2: List[NestedModel3]
|
|
|
|
class NestedModel1(BaseModel):
|
|
data1: List[NestedModel2]
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
NestedModel1(data1=[{'data2': [{'y': 1}]}])
|
|
|
|
expected = [{'loc': ('data1', 0, 'data2', 0, 'x'), 'msg': 'field required', 'type': 'value_error.missing'}]
|
|
|
|
assert exc_info.value.errors() == expected
|
|
|
|
|
|
def test_validate_assignment_error():
|
|
class Model(BaseModel):
|
|
x: int
|
|
|
|
class Config:
|
|
validate_assignment = True
|
|
|
|
model = Model(x=1)
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
model.x = 'a'
|
|
assert (
|
|
str(exc_info.value)
|
|
== '1 validation error for Model\nx\n value is not a valid integer (type=type_error.integer)'
|
|
)
|
|
|
|
|
|
def test_submodel_override_validation_error():
|
|
class SubmodelA(BaseModel):
|
|
x: str
|
|
|
|
class SubmodelB(SubmodelA):
|
|
x: int
|
|
|
|
class Model(BaseModel):
|
|
submodel: SubmodelB
|
|
|
|
submodel = SubmodelA(x='a')
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(submodel=submodel)
|
|
assert exc_info.value.errors() == [
|
|
{'loc': ('submodel', 'x'), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}
|
|
]
|
|
|
|
|
|
def test_validation_error_methods():
|
|
class Model(BaseModel):
|
|
x: int
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Model(x='x')
|
|
e = exc_info.value
|
|
assert (
|
|
str(e)
|
|
== """\
|
|
1 validation error for Model
|
|
x
|
|
value is not a valid integer (type=type_error.integer)"""
|
|
)
|
|
assert e.errors() == [{'loc': ('x',), 'msg': 'value is not a valid integer', 'type': 'type_error.integer'}]
|
|
assert e.json(indent=None) == (
|
|
'[{"loc": ["x"], "msg": "value is not a valid integer", "type": "type_error.integer"}]'
|
|
)
|
|
assert repr(e) == (
|
|
"ValidationError(model='Model', errors=[{'loc': ('x',), 'msg': 'value is not a valid integer', "
|
|
"'type': 'type_error.integer'}])"
|
|
)
|