Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions monk/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class ValidationError(Exception):
"""


class ExpectationError(ValidationError):
"""
Raised when a value does not match given expectation defined by
a validator.
"""


class StructureSpecificationError(ValidationError):
"""
Raised when malformed document structure is detected.
Expand Down
41 changes: 23 additions & 18 deletions monk/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
from . import compat
from .errors import (
CombinedValidationError, AtLeastOneFailed, AllFailed, ValidationError,
NoDefaultValue, InvalidKey, MissingKey, StructureSpecificationError
NoDefaultValue, InvalidKey, MissingKey, StructureSpecificationError,
ExpectationError,
)


Expand Down Expand Up @@ -104,6 +105,8 @@ def get_default_for(self, value, silent=True):
class BaseCombinator(BaseValidator):
error_class = CombinedValidationError
break_on_first_fail = False
#: separator for error messages of nested validators
err_sep = '; '

def __init__(self, specs, default=None, first_is_default=False):
assert specs
Expand Down Expand Up @@ -133,13 +136,13 @@ def __call__(self, value):
if not self.can_tolerate(errors):
def fmt_err(e):
# only display error class if it's not obvious
if type(e) is ValidationError:
if type(e) in (ValidationError, ExpectationError):
tmpl = '{err}'
else:
tmpl = '{cls}: {err}'
return tmpl.format(cls=e.__class__.__name__, err=e)

errors_str = '; '.join((fmt_err(e) for e in errors))
errors_str = self.err_sep.join((fmt_err(e) for e in errors))
raise self.error_class(
'{value!r} ({errors})'.format(value=value, errors=errors_str))

Expand Down Expand Up @@ -181,6 +184,7 @@ class All(BaseCombinator):
"""
error_class = AtLeastOneFailed
break_on_first_fail = True
err_sep = ' and '

def can_tolerate(self, errors):
# TODO: fail early, work as `or` does
Expand All @@ -194,6 +198,7 @@ class Any(BaseCombinator):
Requires that the value passes at least one of nested validators.
"""
error_class = AllFailed
err_sep = ' or '

def can_tolerate(self, errors):
if len(errors) < len(self._specs):
Expand Down Expand Up @@ -248,8 +253,8 @@ def __init__(self, expected_type, default=None):

def _check(self, value):
if not isinstance(value, self.expected_type):
raise ValidationError('must be {type}'
.format(type=self.expected_type.__name__))
raise ExpectationError('is {type}'
.format(type=self.expected_type.__name__))

def _represent(self):
if self._default:
Expand All @@ -269,8 +274,8 @@ def __init__(self, expected_value):

def _check(self, value):
if self._expected_value != value:
raise ValidationError('!= {expected!r}'
.format(expected=self._expected_value))
raise ExpectationError('equals {expected!r}'
.format(expected=self._expected_value))

def _represent(self):
return repr(self._expected_value)
Expand All @@ -291,7 +296,7 @@ def __init__(self, default=None):

def _check(self, value):
if value is not MISSING:
raise ValidationError('must not exist')
raise ExpectationError('does not exist')

def _represent(self):
return ''
Expand All @@ -308,8 +313,8 @@ class ListOf(BaseRequirement):
>>> v([123, 'hello', 5.5])
Traceback (most recent call last):
...
ValidationError: #2: 5.5 (ValidationError: must be int;
ValidationError: must be str)
ValidationError: #2: 5.5 (ValidationError: is int;
ValidationError: is str)

"""
is_recursive = True
Expand Down Expand Up @@ -382,13 +387,13 @@ class DictOf(BaseRequirement):
>>> v({'name': 'John', 'age': 25.5})
Traceback (most recent call last):
...
monk.errors.ValidationError: 'age': must be int
monk.errors.ValidationError: 'age': is int
>>> v({'name': 'John', 'age': 25, 'note': 'custom field'})
>>> v({'name': 'John', 'age': 25, 'note': 5.5})
Traceback (most recent call last):
...
AllFailed: 'note': 5.5 (ValidationError: must be str;
ValidationError: must be int)
AllFailed: 'note': 5.5 (ValidationError: is str;
ValidationError: is int)

Note that this validator supports :class:`NotExists` to mark keys that can
be missing.
Expand Down Expand Up @@ -502,11 +507,11 @@ def _check(self, value):
if value is MISSING:
raise InvalidKey(value)
if self._min is not None and self._min > value:
raise ValidationError('must be ≥ {expected}'
.format(expected=self._min))
raise ExpectationError('≥ {expected}'
.format(expected=self._min))
if self._max is not None and self._max < value:
raise ValidationError('must be ≤ {expected}'
.format(expected=self._max))
raise ExpectationError('≤ {expected}'
.format(expected=self._max))

def _represent(self):
def _fmt(x):
Expand All @@ -523,7 +528,7 @@ def _check(self, value):
try:
super(Length, self)._check(len(value))
except ValidationError as e:
raise ValidationError('length ' + str(e))
raise ExpectationError('length ' + str(e))


def translate(value):
Expand Down
10 changes: 5 additions & 5 deletions tests/rules_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def test_flat(self):
schema('foo')
with pytest.raises(errors.ValidationError) as excinfo:
schema({})
assert "AllFailed: {} (must be int; must be str)" in excinfo.exconly()
assert "AllFailed: {} (is int or is str)" in excinfo.exconly()

def test_nested(self):
schema = Any([
Expand All @@ -103,11 +103,11 @@ def test_nested(self):
with pytest.raises(errors.ValidationError) as excinfo:
schema({'foo': 'hi'})
assert ("AllFailed: {'foo': 'hi'} "
"('foo': must be int; InvalidKey: 'foo')") in excinfo.exconly()
"('foo': is int or InvalidKey: 'foo')") in excinfo.exconly()
with pytest.raises(errors.ValidationError) as excinfo:
schema({'bar': 123})
assert ("AllFailed: {'bar': 123} "
"(InvalidKey: 'bar'; 'bar': must be str)") in excinfo.exconly()
"(InvalidKey: 'bar' or 'bar': is str)") in excinfo.exconly()


class TestShortcuts:
Expand All @@ -121,7 +121,7 @@ def test_one_of(self):
v('foo')
with pytest.raises(errors.ValidationError) as excinfo:
v('quux')
assert "AllFailed: 'quux' (!= 'foo'; != 'bar')" in excinfo.exconly()
assert "AllFailed: 'quux' (equals 'foo' or equals 'bar')" in excinfo.exconly()

# non-literals → rules (behaviour explicitly turned on)

Expand All @@ -134,7 +134,7 @@ def test_one_of(self):
v(456)
with pytest.raises(errors.ValidationError) as excinfo:
v(5.5)
assert 'AllFailed: 5.5 (must be str; must be int)' in excinfo.exconly()
assert 'AllFailed: 5.5 (is str or is int)' in excinfo.exconly()

def test_optional(self):
assert optional(str) == IsA(str) | NotExists()
Expand Down
Loading