diff options
author | Alec Thomas <alec@swapoff.org> | 2011-10-20 02:55:05 -0400 |
---|---|---|
committer | Alec Thomas <alec@swapoff.org> | 2011-10-20 21:59:16 -0400 |
commit | baf8143ac70be52cd606cc8d06e6a6456cfd8990 (patch) | |
tree | 1a28a0d11c3e0fc5e8b57b3309c558507ff9be11 | |
parent | 5c3ef8b5f838aa8c86507ce937b3f1ec8a38373a (diff) | |
download | voluptuous-multiple_errors.tar.gz |
Return as many validation errors as possible.multiple_errors
Errors are now returned in an InvalidList exception, which has the same
interface as Invalid but also includes a .errors attribute with all
encountered errors.
-rw-r--r-- | README.rst | 28 | ||||
-rw-r--r-- | tests.rst | 32 | ||||
-rw-r--r-- | voluptuous.py | 98 |
3 files changed, 106 insertions, 52 deletions
@@ -51,21 +51,21 @@ and goes a little further for completeness. >>> schema({}) Traceback (most recent call last): ... - Invalid: required key 'q' not provided + InvalidList: required key 'q' not provided ...must be a string:: >>> schema({'q': 123}) Traceback (most recent call last): ... - Invalid: expected str for dictionary value @ data['q'] + InvalidList: expected str for dictionary value @ data['q'] ...and must be at least one character in length:: >>> schema({'q': ''}) Traceback (most recent call last): ... - Invalid: length of value must be at least 1 for dictionary value @ data['q'] + InvalidList: length of value must be at least 1 for dictionary value @ data['q'] >>> schema({'q': '#topic'}) {'q': '#topic'} @@ -74,18 +74,18 @@ and goes a little further for completeness. >>> schema({'q': '#topic', 'per_page': 900}) Traceback (most recent call last): ... - Invalid: value must be at most 20 for dictionary value @ data['per_page'] + InvalidList: value must be at most 20 for dictionary value @ data['per_page'] >>> schema({'q': '#topic', 'per_page': -10}) Traceback (most recent call last): ... - Invalid: value must be at least 1 for dictionary value @ data['per_page'] + InvalidList: value must be at least 1 for dictionary value @ data['per_page'] "page" is an integer >= 0:: >>> schema({'q': '#topic', 'page': 'one'}) Traceback (most recent call last): ... - Invalid: expected int for dictionary value @ data['page'] + InvalidList: expected int for dictionary value @ data['page'] >>> schema({'q': '#topic', 'page': 1}) {'q': '#topic', 'page': 1} @@ -117,7 +117,7 @@ instance of the type:: >>> schema('one') Traceback (most recent call last): ... - Invalid: expected int + InvalidList: expected int Lists ~~~~~ @@ -174,7 +174,7 @@ pair in the corresponding data dictionary:: >>> schema({3: 'three'}) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary key @ data[3] + InvalidList: not a valid value for dictionary key @ data[3] Extra dictionary keys ````````````````````` @@ -185,7 +185,7 @@ exceptions:: >>> schema({1: 2}) Traceback (most recent call last): ... - Invalid: extra keys not allowed @ data[1] + InvalidList: extra keys not allowed @ data[1] This behaviour can be altered on a per-schema basis with ``Schema(..., extra=True)``:: @@ -215,7 +215,7 @@ Similarly to how extra_ keys work, this behaviour can be overridden per-schema:: >>> schema({3: 4}) Traceback (most recent call last): ... - Invalid: required key 1 not provided + InvalidList: required key 1 not provided And per-key, with the marker token ``required(key)``:: @@ -223,7 +223,7 @@ And per-key, with the marker token ``required(key)``:: >>> schema({3: 4}) Traceback (most recent call last): ... - Invalid: required key 1 not provided + InvalidList: required key 1 not provided >>> schema({1: 2}) {1: 2} @@ -237,13 +237,13 @@ using the marker token ``optional(key)``:: >>> schema({}) Traceback (most recent call last): ... - Invalid: required key 1 not provided + InvalidList: required key 1 not provided >>> schema({1: 2}) {1: 2} >>> schema({1: 2, 4: 5}) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary key @ data[4] + InvalidList: not a valid value for dictionary key @ data[4] >>> schema({1: 2, 3: 4}) {1: 2, 3: 4} @@ -276,7 +276,7 @@ attempted:: >>> schema([[6]]) Traceback (most recent call last): ... - Invalid: invalid list value @ data[0][0] + InvalidList: invalid list value @ data[0][0] If we pass the data ``[6]``, the ``6`` is not a list type and so will not recurse into the first element of the schema. Matching will continue on to the @@ -13,22 +13,22 @@ It should show the exact index and container type, in this case a list value:: >>> schema(['one', 'two']) Traceback (most recent call last): ... - Invalid: invalid list value @ data[1] + InvalidList: invalid list value @ data[1] It should also be accurate for nested values:: >>> schema([{'two': 'nine'}]) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary value @ data[0]['two'] + InvalidList: not a valid value for dictionary value @ data[0]['two'] >>> schema([{'four': ['nine']}]) Traceback (most recent call last): ... - Invalid: invalid list value @ data[0]['four'][0] + InvalidList: invalid list value @ data[0]['four'][0] >>> schema([{'six': {'seven': 'nine'}}]) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary value @ data[0]['six']['seven'] + InvalidList: not a valid value for dictionary value @ data[0]['six']['seven'] Errors should be reported depth-first:: @@ -50,7 +50,7 @@ Voluptuous supports validation when extra fields are present in the data:: >>> schema({'two': 2}) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary key @ data['two'] + InvalidList: not a valid value for dictionary key @ data['two'] dict and list should be available as type validators:: @@ -61,7 +61,7 @@ dict and list should be available as type validators:: [1, 2, 3] -validation should return instances of the right types when the types are +Validation should return instances of the right types when the types are subclasses of dict or list:: >>> class Dict(dict): @@ -80,3 +80,23 @@ subclasses of dict or list:: [1, 2, 3] >>> type(l) is List True + +Multiple errors are reported:: + + >>> schema = Schema({'one': 1, 'two': 2}) + >>> try: + ... schema({'one': 2, 'two': 3, 'three': 4}) + ... except InvalidList, e: + ... errors = sorted(e.errors, key=lambda k: str(k)) + ... print [str(i) for i in errors] # doctest: +NORMALIZE_WHITESPACE + ["not a valid value for dictionary key @ data['three']", + "not a valid value for dictionary value @ data['one']", + "not a valid value for dictionary value @ data['two']"] + >>> schema = Schema([[1], [2], [3]]) + >>> try: + ... schema([1, 2, 3]) + ... except InvalidList, e: + ... print [str(i) for i in e.errors] # doctest: +NORMALIZE_WHITESPACE + ['invalid list value @ data[0]', + 'invalid list value @ data[1]', + 'invalid list value @ data[2]'] diff --git a/voluptuous.py b/voluptuous.py index 20da1a1..2b5f488 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -133,6 +133,25 @@ class Invalid(Error): return Exception.__str__(self) + path +class InvalidList(Invalid): + def __init__(self, errors=None): + self.errors = errors[:] if errors else [] + + @property + def msg(self): + return self.errors[0].msg + + @property + def path(self): + return self.errors[0].path + + def add(self, error): + self.errors.append(error) + + def __str__(self): + return str(self.errors[0]) + + class Schema(object): """A validation schema. @@ -160,16 +179,21 @@ class Schema(object): return self.validate([], self.schema, data) def validate(self, path, schema, data): - if isinstance(schema, dict): - return self.validate_dict(path, schema, data) - elif isinstance(schema, list): - return self.validate_list(path, schema, data) - type_ = type(schema) - if type_ is type: - type_ = schema - if type_ in (int, long, str, unicode, float, complex, object, - list, dict, types.NoneType) or callable(schema): - return self.validate_scalar(path, schema, data) + try: + if isinstance(schema, dict): + return self.validate_dict(path, schema, data) + elif isinstance(schema, list): + return self.validate_list(path, schema, data) + type_ = type(schema) + if type_ is type: + type_ = schema + if type_ in (int, long, str, unicode, float, complex, object, + list, dict, types.NoneType) or callable(schema): + return self.validate_scalar(path, schema, data) + except InvalidList: + raise + except Invalid, e: + raise InvalidList([e]) raise SchemaError('unsupported schema data type %r' % type(schema).__name__) @@ -185,7 +209,7 @@ class Schema(object): >>> validate([]) Traceback (most recent call last): ... - Invalid: expected a dictionary + InvalidList: expected a dictionary An invalid dictionary value: @@ -193,14 +217,14 @@ class Schema(object): >>> validate({'one': 'three'}) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary value @ data['one'] + InvalidList: not a valid value for dictionary value @ data['one'] An invalid key: >>> validate({'two': 'three'}) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary key @ data['two'] + InvalidList: not a valid value for dictionary key @ data['two'] Validation function, in this case the "int" type: @@ -218,7 +242,7 @@ class Schema(object): >>> validate({'10': 'twenty'}) Traceback (most recent call last): ... - Invalid: not a valid value for dictionary key @ data['10'] + InvalidList: not a valid value for dictionary key @ data['10'] Wrap them in the coerce() function to achieve this: @@ -240,6 +264,7 @@ class Schema(object): isinstance(key, required)) invalid = None error = None + errors = [] for key, value in data.iteritems(): key_path = path + [key] for skey, svalue in schema.iteritems(): @@ -261,8 +286,11 @@ class Schema(object): out[new_key] = self.validate(key_path, svalue, value) except Invalid, e: if len(e.path) > len(key_path): - raise - raise Invalid(e.msg + ' for dictionary value', e.path) + errors.append(e) + else: + errors.append(Invalid(e.msg + ' for dictionary value', + e.path)) + break # Key and value okay, mark any required() fields as found. required_keys.discard(skey) @@ -273,18 +301,21 @@ class Schema(object): else: if invalid: if len(error.path) > len(path) + 1: - raise error + errors.append(error) else: - raise Invalid(invalid, key_path) + errors.append(Invalid(invalid, key_path)) else: - raise Invalid('extra keys not allowed', key_path) + errors.append(Invalid('extra keys not allowed', + key_path)) if required_keys: if len(required_keys) > 1: message = 'required keys %s not provided' \ % ', '.join(map(repr, map(str, required_keys))) else: message = 'required key %r not provided' % required_keys.pop() - raise Invalid(message, path) + errors.append(Invalid(message, path)) + if errors: + raise InvalidList(errors) return out def validate_list(self, path, schema, data): @@ -298,7 +329,7 @@ class Schema(object): >>> validator([3.5]) Traceback (most recent call last): ... - Invalid: invalid list value @ data[0] + InvalidList: invalid list value @ data[0] >>> validator([1]) [1] """ @@ -311,9 +342,11 @@ class Schema(object): out = type(data)() invalid = None + errors = [] index_path = UNDEFINED for i, value in enumerate(data): index_path = path + [i] + invalid = None for s in schema: try: out.append(self.validate(index_path, s, value)) @@ -323,10 +356,11 @@ class Schema(object): raise invalid = e else: - if len(invalid.path) > len(index_path): - raise invalid - else: - raise Invalid('invalid list value', index_path) + if len(invalid.path) <= len(index_path): + invalid = Invalid('invalid list value', index_path) + errors.append(invalid) + if errors: + raise InvalidList(errors) return out @staticmethod @@ -413,7 +447,7 @@ def msg(schema, msg): >>> validate(['three']) Traceback (most recent call last): ... - Invalid: should be one of "one", "two" or an integer + InvalidList: should be one of "one", "two" or an integer Messages are only applied to invalid direct descendants of the schema: @@ -421,7 +455,7 @@ def msg(schema, msg): >>> validate([['three']]) Traceback (most recent call last): ... - Invalid: invalid list value @ data[0][0] + InvalidList: invalid list value @ data[0][0] """ def f(v): try: @@ -460,13 +494,13 @@ def true(msg=None): >>> validate([]) Traceback (most recent call last): ... - Invalid: value was not true + InvalidList: value was not true >>> validate([1]) [1] >>> validate(False) Traceback (most recent call last): ... - Invalid: value was not true + InvalidList: value was not true ...and so on. """ @@ -505,7 +539,7 @@ def boolean(msg=None): >>> validate('moo') Traceback (most recent call last): ... - Invalid: expected boolean + InvalidList: expected boolean """ def f(v): try: @@ -537,7 +571,7 @@ def any(*validators, **kwargs): >>> validate('moo') Traceback (most recent call last): ... - Invalid: no valid value found + InvalidList: no valid value found """ msg = kwargs.pop('msg', None) @@ -586,7 +620,7 @@ def match(pattern, msg=None): >>> validate('123EF4') Traceback (most recent call last): ... - Invalid: does not match regular expression + InvalidList: does not match regular expression Pattern may also be a compiled regular expression: |