From 9718199ca5c996fd84ea79cee37cdd1f10cc282b Mon Sep 17 00:00:00 2001 From: ydidwania Date: Fri, 30 Nov 2018 06:11:36 +0530 Subject: [PATCH 1/5] allow a discrminant field in sub-validators --- voluptuous/validators.py | 61 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 81566b9..ad7f617 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -191,8 +191,10 @@ class _WithSubValidators(object): def __init__(self, *validators, **kwargs): self.validators = validators + print('..validators = ', self.validators) self.msg = kwargs.pop('msg', None) self.required = kwargs.pop('required', False) + self.discriminant = kwargs.pop('discriminant', None) def __voluptuous_compile__(self, schema): self._compiled = [] @@ -202,6 +204,12 @@ def __voluptuous_compile__(self, schema): return self._run def _run(self, path, value): + if self.discriminant is not None: + self._compiled = [ + self.schema._compile(v) + for v in self.discriminant(value, self.validators) + ] + return self._exec(self._compiled, value, path) def __call__(self, v): @@ -242,6 +250,11 @@ class Any(_WithSubValidators): def _exec(self, funcs, v, path=None): error = None + print ('..self = ', self) + print ('..v = ', v) + print ('..funcs = ', funcs) + print('..path = ', path) + for func in funcs: try: if path is None: @@ -252,6 +265,7 @@ def _exec(self, funcs, v, path=None): if error is None or len(e.path) > len(error.path): error = e else: + print ('..error = ', error) if error: raise error if self.msg is None else AnyInvalid( self.msg, path=path) @@ -262,6 +276,53 @@ def _exec(self, funcs, v, path=None): # Convenience alias Or = Any +class Switch(_WithSubValidators): +# """Use the first validated value among those selected by discrminant. + +# :param msg: Message to deliver to user if validation fails. +# :param kwargs: All other keyword arguments are passed to the sub-Schema constructors. +# :returns: Return value of the first validator that passes. + +# >>> validate = Schema(Any('true', 'false', +# ... All(Any(int, bool), Coerce(bool)))) +# >>> validate('true') +# 'true' +# >>> validate(1) +# True +# >>> with raises(MultipleInvalid, "not a valid value"): +# ... validate('moo') + +# msg argument is used + +# >>> validate = Schema(Any(1, 2, 3, msg="Expected 1 2 or 3")) +# >>> validate(1) +# 1 +# >>> with raises(MultipleInvalid, "Expected 1 2 or 3"): +# ... validate(4) +# """ + + def _exec(self, funcs, v, path=None): + error = None + for func in funcs: + try: + if path is None: + return func(v) + else: + return func(path, v) + except Invalid as e: + if error is None or len(e.path) > len(error.path): + error = e + else: + if error: + raise error if self.msg is None else AnyInvalid( + self.msg, path=path) + raise AnyInvalid(self.msg or 'no valid value found', + path=path) + + +# # Convenience alias +# Union = Switch + class All(_WithSubValidators): """Value must pass all validators. From 42240bccac203c0b6233fbf7340e4a429fbe2bd3 Mon Sep 17 00:00:00 2001 From: ydidwania Date: Thu, 15 Aug 2019 05:38:02 +0530 Subject: [PATCH 2/5] Add test for Union type --- voluptuous/tests/tests.py | 26 +++++++++++++++++++- voluptuous/validators.py | 50 ++++++++++++++++----------------------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 1c144eb..3ff71e4 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -11,7 +11,7 @@ Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, - raises) + raises, Union) from voluptuous.humanize import humanize_error from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper @@ -1406,3 +1406,27 @@ def test_exclusive(): "two or more values in the same group of exclusion 'stuff' @ data[]") else: assert False, "Did not raise Invalid for multiple values in Exclusive group" + +def test_any_with_discriminant(): + schema = Schema({ + 'implementation': Union({ + 'type': 'A', + 'a-value': str, + }, { + 'type': 'B', + 'b-value': int, + }, { + 'type': 'C', + 'c-value': bool, + }, discriminant=lambda value, alternatives: filter(lambda v : v['type'] == value['type'], alternatives)) + }) + try: + schema({ + 'implementation': { + 'type': 'C', + 'c-value': None,} + }) + except MultipleInvalid as e: + assert_equal(str(e),'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') + else: + assert False, "Did not raise correct Invalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index ad7f617..3981e96 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -191,13 +191,13 @@ class _WithSubValidators(object): def __init__(self, *validators, **kwargs): self.validators = validators - print('..validators = ', self.validators) self.msg = kwargs.pop('msg', None) self.required = kwargs.pop('required', False) self.discriminant = kwargs.pop('discriminant', None) def __voluptuous_compile__(self, schema): self._compiled = [] + self.schema = schema for v in self.validators: schema.required = self.required self._compiled.append(schema._compile(v)) @@ -250,11 +250,6 @@ class Any(_WithSubValidators): def _exec(self, funcs, v, path=None): error = None - print ('..self = ', self) - print ('..v = ', v) - print ('..funcs = ', funcs) - print('..path = ', path) - for func in funcs: try: if path is None: @@ -265,7 +260,6 @@ def _exec(self, funcs, v, path=None): if error is None or len(e.path) > len(error.path): error = e else: - print ('..error = ', error) if error: raise error if self.msg is None else AnyInvalid( self.msg, path=path) @@ -276,30 +270,28 @@ def _exec(self, funcs, v, path=None): # Convenience alias Or = Any -class Switch(_WithSubValidators): -# """Use the first validated value among those selected by discrminant. +class Union(_WithSubValidators): + """Use the first validated value among those selected by discrminant. + + :param msg: Message to deliver to user if validation fails. + :param discriminant: Function to filter the values + :param kwargs: All other keyword arguments are passed to the sub-Schema constructors. + :returns: Return value of the first validator that passes. -# :param msg: Message to deliver to user if validation fails. -# :param kwargs: All other keyword arguments are passed to the sub-Schema constructors. -# :returns: Return value of the first validator that passes. + discriminant(value, validators) is invoked in execution. -# >>> validate = Schema(Any('true', 'false', -# ... All(Any(int, bool), Coerce(bool)))) -# >>> validate('true') -# 'true' -# >>> validate(1) -# True -# >>> with raises(MultipleInvalid, "not a valid value"): -# ... validate('moo') + >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}, + ... discriminant=lambda val, alt: filter( + ... lambda v : v['type'] == val['type'] , alt))) + >>> validate({'type':'a', 'val':'1'}) + True + >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"): + ... validate({'type':'b', 'a_val':'5'}) -# msg argument is used + ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked -# >>> validate = Schema(Any(1, 2, 3, msg="Expected 1 2 or 3")) -# >>> validate(1) -# 1 -# >>> with raises(MultipleInvalid, "Expected 1 2 or 3"): -# ... validate(4) -# """ + Without the discriminant, the exception would be "extra keys not allowed @ data['b_val']" + """ def _exec(self, funcs, v, path=None): error = None @@ -320,8 +312,8 @@ def _exec(self, funcs, v, path=None): path=path) -# # Convenience alias -# Union = Switch +# Convenience alias +Switch = Union class All(_WithSubValidators): From 136672213e1ce97fa951bad0467c9e55502f8557 Mon Sep 17 00:00:00 2001 From: ydidwania Date: Thu, 15 Aug 2019 05:45:23 +0530 Subject: [PATCH 3/5] typo --- voluptuous/validators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 3981e96..d895dba 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -283,10 +283,10 @@ class Union(_WithSubValidators): >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}, ... discriminant=lambda val, alt: filter( ... lambda v : v['type'] == val['type'] , alt))) - >>> validate({'type':'a', 'val':'1'}) - True + >>> validate({'type':'a', 'a_val':'1'}) + {'a_val': '1', 'type': 'a'} >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"): - ... validate({'type':'b', 'a_val':'5'}) + ... validate({'type':'b', 'b_val':'5'}) ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked From 901f155449d7c1a2f568abef7b97af9eff2a99f0 Mon Sep 17 00:00:00 2001 From: ydidwania Date: Thu, 15 Aug 2019 05:49:06 +0530 Subject: [PATCH 4/5] fix for python3 --- voluptuous/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index d895dba..742b403 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -283,8 +283,8 @@ class Union(_WithSubValidators): >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}, ... discriminant=lambda val, alt: filter( ... lambda v : v['type'] == val['type'] , alt))) - >>> validate({'type':'a', 'a_val':'1'}) - {'a_val': '1', 'type': 'a'} + >>> validate({'type':'a', 'a_val':'1'}) == {'type':'a', 'a_val':'1'} + True >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"): ... validate({'type':'b', 'b_val':'5'}) From b99dad22966b1edb0d7f0324eeb9bb256db79df1 Mon Sep 17 00:00:00 2001 From: ydidwania Date: Thu, 15 Aug 2019 16:35:59 +0530 Subject: [PATCH 5/5] move the discrimnant signature into params description --- voluptuous/validators.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 742b403..328e2bf 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -274,12 +274,10 @@ class Union(_WithSubValidators): """Use the first validated value among those selected by discrminant. :param msg: Message to deliver to user if validation fails. - :param discriminant: Function to filter the values + :param discriminant(value, validators): Returns the filtered list of validators based on the value :param kwargs: All other keyword arguments are passed to the sub-Schema constructors. :returns: Return value of the first validator that passes. - discriminant(value, validators) is invoked in execution. - >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}, ... discriminant=lambda val, alt: filter( ... lambda v : v['type'] == val['type'] , alt)))