Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Unordered validator #199

Merged
Merged
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
30 changes: 29 additions & 1 deletion voluptuous/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Schema, Required, Extra, Invalid, In, Remove, Literal,
Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email,
Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA,
validate_schema, ExactSequence, Equal
validate_schema, ExactSequence, Equal, Unordered
)
from voluptuous.humanize import humanize_error

Expand Down Expand Up @@ -451,3 +451,31 @@ def test_equal():
# Evaluates exactly, not through validators
s = Schema(Equal(str))
assert_raises(Invalid, s, 'foo')


def test_unordered():
# Any order is OK
s = Schema(Unordered([2, 1]))
s([2, 1])
s([1, 2])
# Amount of errors is OK
assert_raises(Invalid, s, [2, 0])
assert_raises(MultipleInvalid, s, [0, 0])
# Different length is NOK
assert_raises(Invalid, s, [1])
assert_raises(Invalid, s, [1, 2, 0])
assert_raises(MultipleInvalid, s, [1, 2, 0, 0])
# Other type than list or tuple is NOK
assert_raises(Invalid, s, 'foo')
assert_raises(Invalid, s, 10)
# Validators are evaluated through as schemas
s = Schema(Unordered([int, str]))
s([1, '2'])
s(['1', 2])
s = Schema(Unordered([{'foo': int}, []]))
s([{'foo': 3}, []])
# Most accurate validators must be positioned on left
s = Schema(Unordered([int, 3]))
assert_raises(Invalid, s, [3, 2])
s = Schema(Unordered([3, int]))
s([3, 2])
59 changes: 59 additions & 0 deletions voluptuous/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,3 +720,62 @@ def __call__(self, v):

def __repr__(self):
return 'Equal({})'.format(self.target)


class Unordered(object):
"""Ensures sequence contains values in unspecified order.

>>> s = Schema(Unordered([2, 1]))
>>> s([2, 1])
[2, 1]
>>> s([1, 2])
[1, 2]
>>> s = Schema(Unordered([str, int]))
>>> s(['foo', 1])
['foo', 1]
>>> s([1, 'foo'])
[1, 'foo']
"""

def __init__(self, validators, msg=None, **kwargs):
self.validators = validators
self.msg = msg
self._schemas = [Schema(val, **kwargs) for val in validators]

def __call__(self, v):
if not isinstance(v, (list, tuple)):
raise Invalid(self.msg or 'Value {} is not sequence!'.format(v))

if len(v) != len(self._schemas):
raise Invalid(self.msg or 'List lengths differ, value:{} != target:{}'.format(len(v), len(self._schemas)))

consumed = set()
missing = []
for index, value in enumerate(v):
found = False
for i, s in enumerate(self._schemas):
if i in consumed:
continue
try:
s(value)
except Invalid:
pass
else:
found = True
consumed.add(i)
break
if not found:
missing.append((index, value))

if len(missing) == 1:
el = missing[0]
raise Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format(el[0], el[1]))
elif missing:
raise MultipleInvalid([
Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format(el[0], el[1]))
for el in missing
])
return v

def __repr__(self):
return 'Unordered([{}])'.format(", ".join(repr(v) for v in self.validators))