From 2f36d98f896cb7f1d1cdc99025ed6db7ad5bb2c1 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Sun, 11 Sep 2016 13:30:32 +0300 Subject: [PATCH] Add Unordered validator --- voluptuous/tests/tests.py | 30 +++++++++++++++++++- voluptuous/validators.py | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index d545cf6..766dd87 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -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 @@ -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]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index a2576e0..d751d73 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -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))