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'}])" )