From 28580c56f7960ffa9f819e5af1a7cbd10acd8e26 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 | 64 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_utils.py | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/marshmallow/utils.py b/marshmallow/utils.py index 725ff95dd..153dc7b09 100755 --- a/marshmallow/utils.py +++ b/marshmallow/utils.py @@ -15,6 +15,7 @@ from pprint import pprint as py_pprint from marshmallow.compat import OrderedDict, binary_type, iteritems, text_type +from marshmallow.exceptions import ValidationError # Key used for schema-level validation errors @@ -407,3 +408,66 @@ def merge_errors(errors1, errors2): ) else: return [text_type(errors1), text_type(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): + parts = path.split('.', 1) + + if len(parts) == 1: + return {path: 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'}}} + + :param str path: '/'-separated 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 ee5cf60dd..70eb0cc9f 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, @@ -296,3 +297,54 @@ 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(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