From 3dbae4e671841082be1a115db1e83b15224a6d52 Mon Sep 17 00:00:00 2001 From: Maxim Kulkin Date: Fri, 17 Jun 2016 14:37:11 -0700 Subject: [PATCH] Add helper class for reporting multiple errors --- marshmallow/utils.py | 74 +++++++++++++++++++++++++++++++++++++++++++- tests/test_utils.py | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/marshmallow/utils.py b/marshmallow/utils.py index a9f051412..4217e4ef5 100755 --- a/marshmallow/utils.py +++ b/marshmallow/utils.py @@ -14,7 +14,9 @@ from email.utils import formatdate, parsedate from pprint import pprint as py_pprint -from marshmallow.compat import OrderedDict, binary_type, iteritems, text_type +from marshmallow.compat import OrderedDict, binary_type, iteritems, \ + string_types, text_type +from marshmallow.exceptions import ValidationError # Key used for schema-level validation errors @@ -407,3 +409,73 @@ def merge_errors(errors1, errors2): ) else: return [errors1, errors2] + + +class ValidationErrorBuilder(object): + """Helper class to report multiple errors. + + Example: + + @marshmallow.validates_schema + def validate_all(self, data): + builder = marshmallow.utils.ValidationErrorBuilder() + if data['foo']['bar'] >= data['baz']['bam']: + builder.add_error('foo.bar', 'Should be less than bam') + if data['foo']['quux'] >= data['baz']['bam']: + builder.add_fields('foo.quux', 'Should be less than bam') + ... + builder.raise_errors() + """ + + def __init__(self): + self.errors = {} + + def _make_error(self, path, error): + if isinstance(path, string_types): + parts = path.split('.') + else: + parts = path + + if len(parts) == 1: + return {parts[0]: error} + else: + return {parts[0]: self._make_error(parts[1:], error)} + + def add_error(self, path, error): + """Add error message for given field path. + + Example: + + builder = ValidationErrorBuilder() + builder.add_error('foo.bar.baz', 'Some error') + print builder.errors + # => {'foo': {'bar': {'baz': 'Some error'}}} + + builder.add_error(['foo', 'bar', 'baz'], 'Some error') + print builder.errors + # => {'foo': {'bar': {'baz': 'Some error'}}} + + :param str|list path: '.'-separated list or list of field names + :param str error: Error message + """ + self.errors = merge_errors(self.errors, self._make_error(path, error)) + + def merge_errors(self, errors): + """Add errors in dict format. + + Example: + + builder = ValidationErrorBuilder() + builder.add_errors({'foo': {'bar': 'Error 1'}}) + builder.add_errors({'foo': {'baz': 'Error 2'}, 'bam': 'Error 3'}) + print builder.errors + # => {'foo': {'bar': 'Error 1', 'baz': 'Error 2'}, 'bam': 'Error 3'} + + :param str, list or dict errors: Errors to merge + """ + self.errors = merge_errors(self.errors, errors) + + def raise_errors(self): + """Raise ValidationError if errors are not empty; do nothing otherwise.""" + if self.errors: + raise ValidationError(self.errors) diff --git a/tests/test_utils.py b/tests/test_utils.py index deadf6663..baff536c9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,7 @@ import pytest +from marshmallow.exceptions import ValidationError from marshmallow import utils from tests.base import ( assert_datetime_equal, @@ -341,3 +342,62 @@ def test_deep_merging_dicts(self): assert {'field1': {'field2': ['error1', 'error2']}} == \ utils.merge_errors({'field1': {'field2': 'error1'}}, {'field1': {'field2': 'error2'}}) + + +class TestValidationErrorBuilder: + + def test_empty_errors(self): + builder = utils.ValidationErrorBuilder() + assert {} == builder.errors + + def test_adding_field_error(self): + builder = utils.ValidationErrorBuilder() + builder.add_error('foo', 'error 1') + assert {'foo': 'error 1'} == builder.errors + + def test_adding_multiple_errors(self): + builder = utils.ValidationErrorBuilder() + builder.add_error('foo', 'error 1') + builder.add_error('bar', 'error 2') + builder.add_error('bam', 'error 3') + assert {'foo': 'error 1', 'bar': 'error 2', 'bam': 'error 3'} == \ + builder.errors + + def test_adding_nested_errors(self): + builder = utils.ValidationErrorBuilder() + builder.add_error('foo.bar', 'error 1') + assert {'foo': {'bar': 'error 1'}} == builder.errors + + def test_adding_multiple_nested_errors_described_with_string_path(self): + builder = utils.ValidationErrorBuilder() + builder.add_error('foo.bar', 'error 1') + builder.add_error('foo.baz.bam', 'error 2') + builder.add_error('quux', 'error 3') + assert {'foo': {'bar': 'error 1', 'baz': {'bam': 'error 2'}}, + 'quux': 'error 3'} == builder.errors + + def test_adding_multiple_nested_errors_described_with_list(self): + builder = utils.ValidationErrorBuilder() + builder.add_error(['foo', 'bar'], 'error 1') + builder.add_error(['foo', 'baz', 'bam'], 'error 2') + builder.add_error('quux', 'error 3') + assert {'foo': {'bar': 'error 1', 'baz': {'bam': 'error 2'}}, + 'quux': 'error 3'} == builder.errors + + def test_adding_merging_errors(self): + builder = utils.ValidationErrorBuilder() + builder.merge_errors({'foo': {'bar': 'error 1'}}) + builder.merge_errors({'foo': {'baz': 'error 2'}}) + assert {'foo': {'bar': 'error 1', 'baz': 'error 2'}} == builder.errors + + def test_raise_errors_on_empty_builder_does_nothing(self): + builder = utils.ValidationErrorBuilder() + builder.raise_errors() + + def test_raise_errors_on_non_empty_builder_raises_ValidationError(self): + builder = utils.ValidationErrorBuilder() + builder.add_error('foo', 'error 1') + with pytest.raises(ValidationError) as excinfo: + builder.raise_errors() + + assert excinfo.value.messages == builder.errors