summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlec Thomas <alec@swapoff.org>2011-10-20 02:55:05 -0400
committerAlec Thomas <alec@swapoff.org>2011-10-20 21:59:16 -0400
commitbaf8143ac70be52cd606cc8d06e6a6456cfd8990 (patch)
tree1a28a0d11c3e0fc5e8b57b3309c558507ff9be11
parent5c3ef8b5f838aa8c86507ce937b3f1ec8a38373a (diff)
downloadvoluptuous-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.rst28
-rw-r--r--tests.rst32
-rw-r--r--voluptuous.py98
3 files changed, 106 insertions, 52 deletions
diff --git a/README.rst b/README.rst
index ca10227..dccafdf 100644
--- a/README.rst
+++ b/README.rst
@@ -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
diff --git a/tests.rst b/tests.rst
index 31ac002..29047e1 100644
--- a/tests.rst
+++ b/tests.rst
@@ -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: