From 93203b0b68236a8d525bab0a798459c3bf51cd54 Mon Sep 17 00:00:00 2001 From: Rogdham Date: Sat, 18 May 2013 23:35:57 +0100 Subject: [PATCH] Reference values in error messages. Fixes #4. --- README.rst | 9 ++++++--- schema.py | 43 +++++++++++++++++++++++++------------------ test_schema.py | 19 +++++++++++++++++-- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index f715711..939b33e 100644 --- a/README.rst +++ b/README.rst @@ -243,19 +243,22 @@ User-friendly error reporting You can pass a keyword argument ``error`` to any of validatable classes (such as ``Schema``, ``And``, ``Or``, ``Use``) to report this error instead of -a built-in one. +a built-in one. The format ``{value}`` will get replaced with the value failing +the validation. .. code:: python - >>> Schema(Use(int, error='Invalid year')).validate('XVII') + >>> Schema(Use(int, error='Invalid year: {value}')).validate('XVII') Traceback (most recent call last): ... - SchemaError: Invalid year + SchemaError: Invalid year: XVII You can see all errors that occured by accessing exception's ``exc.autos`` for auto-generated error messages, and ``exc.errors`` for errors which had ``error`` text passed to them. +You can use ``exc.value`` to directly access the faulty value. + You can exit with ``sys.exit(exc.code)`` if you want to show the messages to the user without traceback. ``error`` messages are given precedence in that case. diff --git a/schema.py b/schema.py index 5a3ed3b..6654fe0 100644 --- a/schema.py +++ b/schema.py @@ -9,9 +9,12 @@ class SchemaError(Exception): """Error during Schema validation.""" - def __init__(self, autos, errors): + def __init__(self, autos, errors, value): self.autos = autos if type(autos) is list else [autos] self.errors = errors if type(errors) is list else [errors] + self.value = value + self.errors = [None if e is None else e.format(value=self.value) + for e in self.errors] Exception.__init__(self, self.code) @property @@ -47,14 +50,14 @@ def validate(self, data): class Or(And): def validate(self, data): - x = SchemaError([], []) + x = SchemaError([], [], None) for s in [Schema(s, error=self._error) for s in self._args]: try: return s.validate(data) except SchemaError as _x: x = _x raise SchemaError(['%r did not validate %r' % (self, data)] + x.autos, - [self._error] + x.errors) + [self._error] + x.errors, data) class Use(object): @@ -71,10 +74,11 @@ def validate(self, data): try: return self._callable(data) except SchemaError as x: - raise SchemaError([None] + x.autos, [self._error] + x.errors) + raise SchemaError([None] + x.autos, [self._error] + x.errors, data) except BaseException as x: f = self._callable.__name__ - raise SchemaError('%s(%r) raised %r' % (f, data, x), self._error) + raise SchemaError('%s(%r) raised %r' % (f, data, x), self._error, + data) def priority(s): @@ -118,9 +122,9 @@ def validate(self, data): for skey in sorted(s, key=priority): svalue = s[skey] try: - nkey = Schema(skey, error=e).validate(key) + nkey = Schema(skey).validate(key) # error got forgotten try: - nvalue = Schema(svalue, error=e).validate(value) + nvalue = Schema(svalue).validate(value) except SchemaError as _x: x = _x raise @@ -135,44 +139,47 @@ def validate(self, data): elif skey is not None: if x is not None: raise SchemaError(['key %r is required' % key] + - x.autos, [e] + x.errors) + x.autos, [e] + x.errors, data) else: - raise SchemaError('key %r is required' % skey, e) + raise SchemaError('key %r is required' % skey, e, data) coverage = set(k for k in coverage if type(k) is not Optional) required = set(k for k in s if type(k) is not Optional) if coverage != required: - raise SchemaError('missed keys %r' % (required - coverage), e) + raise SchemaError('missed keys %r' % (required - coverage), e, + data) if len(new) != len(data): - raise SchemaError('wrong keys %r in %r' % (new, data), e) + raise SchemaError('wrong keys %r in %r' % (new, data), e, data) return new if hasattr(s, 'validate'): try: return s.validate(data) except SchemaError as x: - raise SchemaError([None] + x.autos, [e] + x.errors) + raise SchemaError([None] + x.autos, [e] + x.errors, data) except BaseException as x: raise SchemaError('%r.validate(%r) raised %r' % (s, data, x), - self._error) + self._error, data) if type(s) is type: if isinstance(data, s): return data else: - raise SchemaError('%r should be instance of %r' % (data, s), e) + raise SchemaError('%r should be instance of %r' % (data, s), e, + data) if callable(s): f = s.__name__ try: if s(data): return data except SchemaError as x: - raise SchemaError([None] + x.autos, [e] + x.errors) + raise SchemaError([None] + x.autos, [e] + x.errors, data) except BaseException as x: raise SchemaError('%s(%r) raised %r' % (f, data, x), - self._error) - raise SchemaError('%s(%r) should evaluate to True' % (f, data), e) + self._error, data) + raise SchemaError('%s(%r) should evaluate to True' % (f, data), e, + data) if s == data: return data else: - raise SchemaError('%r does not match %r' % (s, data), e) + raise SchemaError('%r does not match %r' % (s, data), e, data) class Optional(Schema): diff --git a/test_schema.py b/test_schema.py index 724f9aa..a69a56a 100644 --- a/test_schema.py +++ b/test_schema.py @@ -19,8 +19,8 @@ def ve(_): raise ValueError() -def se(_): - raise SchemaError('first auto', 'first error') +def se(data): + raise SchemaError('first auto', 'first error', data) def test_schema(): @@ -164,6 +164,21 @@ def test_nice_errors(): assert e.code == 'should be a number' +def test_nice_errors_format(): + try: + Schema(int, error='{value} should be integer').validate('x') + except SchemaError as e: + assert e.errors == ['x should be integer'] + try: + Schema(Use(float), error='{value} should be a number').validate('x') + except SchemaError as e: + assert e.code == 'x should be a number' + try: + Schema({Optional('i'): Use(int, error='{value} should be a number')}).validate({'i': 'x'}) + except SchemaError as e: + assert e.code == 'x should be a number' + + def test_use_error_handling(): try: Use(ve).validate('x')