From 20619ea9017be07c4467e64517ae0ea980712e7d Mon Sep 17 00:00:00 2001 From: David Montague Date: Fri, 19 Jul 2019 00:26:15 -0700 Subject: [PATCH 1/4] Add type name to ValidationError error message --- pydantic/dataclasses.py | 2 +- pydantic/error_wrappers.py | 8 ++++++-- pydantic/main.py | 10 +++++----- tests/test_error_wrappers.py | 6 +++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index a0d9ebc..96f8dc8 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -44,7 +44,7 @@ def setattr_validate_assignment(self: 'DataclassType', name: str, value: Any) -> d.pop(name) value, error_ = self.__pydantic_model__.__fields__[name].validate(value, d, loc=name, cls=self.__class__) if error_: - raise ValidationError([error_]) + raise ValidationError([error_], self) object.__setattr__(self, name, value) diff --git a/pydantic/error_wrappers.py b/pydantic/error_wrappers.py index daf0e53..59de2c0 100644 --- a/pydantic/error_wrappers.py +++ b/pydantic/error_wrappers.py @@ -51,8 +51,9 @@ ErrorList = Union[Sequence[Any], ErrorWrapper] class ValidationError(ValueError): __slots__ = ('raw_errors',) - def __init__(self, errors: Sequence[ErrorList]) -> None: + def __init__(self, errors: Sequence[ErrorList], obj_or_type: Any) -> None: self.raw_errors = errors + self.obj_type: Type[Any] = obj_or_type if isinstance(obj_or_type, type) else type(obj_or_type) @lru_cache() def errors(self) -> List[Dict[str, Any]]: @@ -64,7 +65,10 @@ class ValidationError(ValueError): def __str__(self) -> str: errors = self.errors() no_errors = len(errors) - return f'{no_errors} validation error{"" if no_errors == 1 else "s"}\n{display_errors(errors)}' + return ( + f'{no_errors} validation error{"" if no_errors == 1 else "s"} for {self.obj_type.__name__}\n' + f'{display_errors(errors)}' + ) def display_errors(errors: List[Dict[str, Any]]) -> str: diff --git a/pydantic/main.py b/pydantic/main.py index 624ba2a..6f58333 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -293,7 +293,7 @@ class BaseModel(metaclass=MetaModel): elif self.__config__.validate_assignment: value_, error_ = self.fields[name].validate(value, self.dict(exclude={name}), loc=name) if error_: - raise ValidationError([error_]) + raise ValidationError([error_], self) else: self.__values__[name] = value_ self.__fields_set__.add(name) @@ -372,7 +372,7 @@ class BaseModel(metaclass=MetaModel): obj = dict(obj) except (TypeError, ValueError) as e: exc = TypeError(f'{cls.__name__} expected dict not {type(obj).__name__}') - raise ValidationError([ErrorWrapper(exc, loc='__obj__')]) from e + raise ValidationError([ErrorWrapper(exc, loc='__obj__')], cls) from e return cls(**obj) @classmethod @@ -390,7 +390,7 @@ class BaseModel(metaclass=MetaModel): b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle ) except (ValueError, TypeError, UnicodeDecodeError) as e: - raise ValidationError([ErrorWrapper(e, loc='__obj__')]) + raise ValidationError([ErrorWrapper(e, loc='__obj__')], cls) return cls.parse_obj(obj) @classmethod @@ -784,8 +784,8 @@ def validate_model( # noqa: C901 (ignore complexity) errors.append(ErrorWrapper(ExtraError(), loc=f, config=config)) if not raise_exc: - return values, fields_set, ValidationError(errors) if errors else None + return values, fields_set, ValidationError(errors, model) if errors else None if errors: - raise ValidationError(errors) + raise ValidationError(errors, model) return values, fields_set, None diff --git a/tests/test_error_wrappers.py b/tests/test_error_wrappers.py index 9092d03..1738c2e 100644 --- a/tests/test_error_wrappers.py +++ b/tests/test_error_wrappers.py @@ -135,7 +135,7 @@ from pydantic.error_wrappers import ValidationError, flatten_errors, get_exc_typ ( '__str__', """\ -11 validation errors +11 validation errors for Model a value is not a valid integer (type=type_error.integer) b -> x @@ -227,7 +227,7 @@ def test_single_error(): Model(x='x') expected = """\ -1 validation error +1 validation error for Model x value is not a valid integer (type=type_error.integer)""" assert str(exc_info.value) == expected @@ -239,7 +239,7 @@ x assert ( str(exc_info.value) == """\ -1 validation error +1 validation error for Model x field required (type=value_error.missing)""" ) From fd7ec970be1312ccf6198f52892e669bc2f1a7c6 Mon Sep 17 00:00:00 2001 From: David Montague Date: Fri, 19 Jul 2019 00:42:08 -0700 Subject: [PATCH 2/4] Update history --- HISTORY.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HISTORY.rst b/HISTORY.rst index 86c7200..9148a8c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ History ------- +v0.32 (unreleased) +.................. +* add model name to ``ValidationError`` error message, #676 by @dmontagu + v0.31 (2019-07-24) .................. * better support for floating point `multiple_of` values, #652 by @justindujardin @@ -15,6 +19,7 @@ v0.31 (2019-07-24) * add ``Config.keep_untouched`` for custom descriptors support, #679 by @MrMrRobat * use ``inspect.cleandoc`` internally to get model description, #657 by @tiangolo * add ``Color`` to schema generation, by @euri10 +* add documentation for Literal type, #651 by @dmontagu v0.30.1 (2019-07-15) .................... From b960f71586f65de9b1d85ef715949aa5e172c6b3 Mon Sep 17 00:00:00 2001 From: David Montague Date: Fri, 19 Jul 2019 11:55:55 -0700 Subject: [PATCH 3/4] Incorporate feedback --- pydantic/dataclasses.py | 2 +- pydantic/error_wrappers.py | 8 ++++---- pydantic/main.py | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index 96f8dc8..06c4a48 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -44,7 +44,7 @@ def setattr_validate_assignment(self: 'DataclassType', name: str, value: Any) -> d.pop(name) value, error_ = self.__pydantic_model__.__fields__[name].validate(value, d, loc=name, cls=self.__class__) if error_: - raise ValidationError([error_], self) + raise ValidationError([error_], type(self)) object.__setattr__(self, name, value) diff --git a/pydantic/error_wrappers.py b/pydantic/error_wrappers.py index 59de2c0..f483fa6 100644 --- a/pydantic/error_wrappers.py +++ b/pydantic/error_wrappers.py @@ -49,11 +49,11 @@ ErrorList = Union[Sequence[Any], ErrorWrapper] class ValidationError(ValueError): - __slots__ = ('raw_errors',) + __slots__ = ('raw_errors', 'model') - def __init__(self, errors: Sequence[ErrorList], obj_or_type: Any) -> None: + def __init__(self, errors: Sequence[ErrorList], model: Type[Any]) -> None: self.raw_errors = errors - self.obj_type: Type[Any] = obj_or_type if isinstance(obj_or_type, type) else type(obj_or_type) + self.model = model @lru_cache() def errors(self) -> List[Dict[str, Any]]: @@ -66,7 +66,7 @@ class ValidationError(ValueError): errors = self.errors() no_errors = len(errors) return ( - f'{no_errors} validation error{"" if no_errors == 1 else "s"} for {self.obj_type.__name__}\n' + f'{no_errors} validation error{"" if no_errors == 1 else "s"} for {self.model.__name__}\n' f'{display_errors(errors)}' ) diff --git a/pydantic/main.py b/pydantic/main.py index 6f58333..cd68d4b 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -784,8 +784,10 @@ def validate_model( # noqa: C901 (ignore complexity) errors.append(ErrorWrapper(ExtraError(), loc=f, config=config)) if not raise_exc: - return values, fields_set, ValidationError(errors, model) if errors else None + model_type = model if isinstance(model, type) else type(model) + return values, fields_set, ValidationError(errors, model_type) if errors else None if errors: - raise ValidationError(errors, model) + model_type = model if isinstance(model, type) else type(model) + raise ValidationError(errors, model_type) return values, fields_set, None From 8135f470e445fa98ab2d5ebca234f164e2f38138 Mon Sep 17 00:00:00 2001 From: David Montague Date: Thu, 25 Jul 2019 00:15:54 -0700 Subject: [PATCH 4/4] Incorporate feedback --- pydantic/main.py | 16 ++++++++++------ tests/test_error_wrappers.py | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pydantic/main.py b/pydantic/main.py index cd68d4b..d59736d 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -293,7 +293,7 @@ class BaseModel(metaclass=MetaModel): elif self.__config__.validate_assignment: value_, error_ = self.fields[name].validate(value, self.dict(exclude={name}), loc=name) if error_: - raise ValidationError([error_], self) + raise ValidationError([error_], type(self)) else: self.__values__[name] = value_ self.__fields_set__.add(name) @@ -783,11 +783,15 @@ def validate_model( # noqa: C901 (ignore complexity) for f in sorted(extra): errors.append(ErrorWrapper(ExtraError(), loc=f, config=config)) - if not raise_exc: - model_type = model if isinstance(model, type) else type(model) - return values, fields_set, ValidationError(errors, model_type) if errors else None - + err = None if errors: model_type = model if isinstance(model, type) else type(model) - raise ValidationError(errors, model_type) + err = ValidationError(errors, model_type) + + if not raise_exc: + return values, fields_set, err + + if err: + raise err + return values, fields_set, None diff --git a/tests/test_error_wrappers.py b/tests/test_error_wrappers.py index 1738c2e..fc7bfce 100644 --- a/tests/test_error_wrappers.py +++ b/tests/test_error_wrappers.py @@ -261,3 +261,19 @@ def test_nested_error(): 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)' + )