Files
pydantic/tests/test_errors.py
T
Samuel Colvin c3098a30cf Consistent __repr__ and __str__ methods for all types (#884)
* Consistent __repr__ and __str__ methods for all types

* add change description

* devtools integration and feedback on repr methods

* fix Color repr

* tests for truncate

* add devtools section to docs

* tests for devtools

* ValidationError inheriting from Representation

* fix imports

* tweaks

* tweak docs

* exec_examples.py integration with __repr__ changes
2019-10-14 17:37:04 +01:00

389 lines
10 KiB
Python

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'}
]
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]) in (
"ErrorWrapper(exc=ValueError('custom error'), loc=('foo',))", # python 3.7
"ErrorWrapper(exc=ValueError('custom error',), loc=('foo',))", # python 3.6
), repr(exc_info.value.raw_errors[0])
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'}])"
)