Files
pydantic/tests/test_errors.py
T
PrettyWood e5fff9ccd0 fix: make pydantic errors (un)pickable (#1630)
* fix: make pydantic errors (un)pickable

closes #1616

* add typing

* refactor: rename kwargs into ctx
2020-06-27 19:31:23 +01:00

397 lines
11 KiB
Python

import pickle
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
from pydantic.errors import StrRegexError
from pydantic.typing import Literal
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"'
def test_pydantic_error_pickable():
"""
Pydantic errors should be (un)pickable.
(this test does not create a custom local error as we can't pickle local objects)
"""
p = pickle.dumps(StrRegexError(pattern='pika'))
error = pickle.loads(p)
assert isinstance(error, StrRegexError)
assert error.pattern == 'pika'
@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'}])"
)