Source code for lollipop.errors

from lollipop.compat import iteritems, string_types


__all__ = [
    'SCHEMA',
    'ValidationError',
    'ValidationErrorBuilder',
    'merge_errors',
]


#: Name of an error key for cases when you have both errors for the object and
#: for it's fields::
#:
#:     {'field1': 'Field error', '_schema': 'Whole object error'}
SCHEMA = '_schema'

MISSING_ERROR_MESSAGE = \
    'Error message "{error_key}" in class {class_name} does not exist'


[docs]class ValidationError(Exception): """Exception to report validation errors. Examples of valid error messages: :: raise ValidationError('Error') raise ValidationError(['Error 1', 'Error 2']) raise ValidationError({ 'field1': 'Error 1', 'field2': {'subfield1': ['Error 2', 'Error 3']} }) :param messages: Validation error messages. String, list of strings or dict where keys are nested fields and values are error messages. """ def __init__(self, messages): super(ValidationError, self).__init__('Invalid data: %r' % messages) # TODO: normalize messages self.messages = messages
class ErrorMessagesMixin(object): def __init__(self, error_messages=None, *args, **kwargs): super(ErrorMessagesMixin, self).__init__(*args, **kwargs) self._error_messages = {} for cls in reversed(self.__class__.__mro__): self._error_messages.update(getattr(cls, 'default_error_messages', {})) self._error_messages.update(error_messages or {}) def _fail(self, error_key, **kwargs): if error_key not in self._error_messages: msg = MISSING_ERROR_MESSAGE.format( class_name=self.__class__.__name__, error_key=error_key ) raise ValueError(msg) msg = self._error_messages[error_key] if isinstance(msg, str): msg = msg.format(**kwargs) raise ValidationError(msg)
[docs]def merge_errors(errors1, errors2): """Deeply merges two error messages. Error messages can be string, list of strings or dict of error messages (recursively). Format is the same as accepted by :exc:`ValidationError`. Returns new error messages. """ if errors1 is None: return errors2 elif errors2 is None: return errors1 if isinstance(errors1, list): if not errors1: return errors2 if isinstance(errors2, list): return errors1 + errors2 elif isinstance(errors2, dict): return dict( errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))} ) else: return errors1 + [errors2] elif isinstance(errors1, dict): if isinstance(errors2, list): return dict( errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)} ) elif isinstance(errors2, dict): errors = dict(errors1) for k, v in iteritems(errors2): if k in errors: errors[k] = merge_errors(errors[k], v) else: errors[k] = v return errors else: return dict( errors1, **{SCHEMA: merge_errors(errors1.get(SCHEMA), errors2)} ) else: if isinstance(errors2, list): return [errors1] + errors2 if errors2 else errors1 elif isinstance(errors2, dict): return dict( errors2, **{SCHEMA: merge_errors(errors1, errors2.get(SCHEMA))} ) else: return [errors1, errors2]
[docs]class ValidationErrorBuilder(object): """Helper class to report multiple errors. Example: :: def validate_all(data): builder = 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 = None def _make_error(self, path, error): parts = path.split('.', 1) if isinstance(path, string_types) else [path] if len(parts) == 1: return {path: error} else: return {parts[0]: self._make_error(parts[1], error)}
[docs] 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))
[docs] def add_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)
[docs] def raise_errors(self): """Raise :exc:`ValidationError` if errors are not empty; do nothing otherwise. """ if self.errors: raise ValidationError(self.errors)