From 4d742e4863964ec5b5c3707a04b6ee2da8af97de Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Sun, 24 Jun 2018 00:24:18 +0100 Subject: [PATCH] Added support for sets and frozensets --- README.md | 53 +++++++++++++++++++++++++++ voluptuous/schema_builder.py | 42 ++++++++++++++++++++++ voluptuous/tests/tests.py | 70 ++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/README.md b/README.md index 0ab31ff..46e2288 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,59 @@ True ``` +### Sets and frozensets + +Sets and frozensets are treated as a set of valid values. Each element +in the schema set is compared to each value in the input data: + +```pycon +>>> schema = Schema({42}) +>>> schema({42}) == {42} +True +>>> try: +... schema({43}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "invalid value in set" +True +>>> schema = Schema({int}) +>>> schema({1, 2, 3}) == {1, 2, 3} +True +>>> schema = Schema({int, str}) +>>> schema({1, 2, 'abc'}) == {1, 2, 'abc'} +True +>>> schema = Schema(frozenset([int])) +>>> try: +... schema({3}) +... raise AssertionError('Invalid not raised') +... except Invalid as e: +... exc = e +>>> str(exc) == 'expected a frozenset' +True + +``` + +However, an empty set (`set()`) is treated as is. If you want to specify a set +that can contain anything, specify it as `set`: + +```pycon +>>> schema = Schema(set()) +>>> try: +... schema({1}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "invalid value in set" +True +>>> schema(set()) == set() +True +>>> schema = Schema(set) +>>> schema({1, 2}) == {1, 2} +True + +``` + ### Validation functions Validators are simple callables that raise an `Invalid` exception when diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 9f3a022..fa29b34 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -286,6 +286,8 @@ def _compile(self, schema): return self._compile_list(schema) elif isinstance(schema, tuple): return self._compile_tuple(schema) + elif isinstance(schema, (frozenset, set)): + return self._compile_set(schema) type_ = type(schema) if inspect.isclass(schema): type_ = schema @@ -675,6 +677,46 @@ def _compile_list(self, schema): """ return self._compile_sequence(schema, list) + def _compile_set(self, schema): + """Validate a set. + + A set is an unordered collection of unique elements. + + >>> validator = Schema({int}) + >>> validator(set([42])) == set([42]) + True + >>> with raises(er.Invalid, 'expected a set'): + ... validator(42) + >>> with raises(er.MultipleInvalid, 'invalid value in set'): + ... validator(set(['a'])) + """ + type_ = type(schema) + type_name = type_.__name__ + + def validate_set(path, data): + if not isinstance(data, type_): + raise er.Invalid('expected a %s' % type_name, path) + + _compiled = [self._compile(s) for s in schema] + errors = [] + for value in data: + for validate in _compiled: + try: + validate(path, value) + break + except er.Invalid: + pass + else: + invalid = er.Invalid('invalid value in %s' % type_name, path) + errors.append(invalid) + + if errors: + raise er.MultipleInvalid(errors) + + return data + + return validate_set + def extend(self, schema, required=None, extra=None): """Create a new `Schema` by merging this and the provided `schema`. diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 816958a..2b1ab92 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1141,3 +1141,73 @@ def test_SomeOf_on_bounds_assertion(): def test_comparing_voluptuous_object_to_str(): assert_true(Optional('Classification') < 'Name') + + +def test_set_of_integers(): + schema = Schema({int}) + with raises(Invalid, 'expected a set'): + schema(42) + with raises(Invalid, 'expected a set'): + schema(frozenset([42])) + + schema(set()) + schema(set([42])) + schema(set([42, 43, 44])) + try: + schema(set(['abc'])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in set") + else: + assert False, "Did not raise Invalid" + + +def test_frozenset_of_integers(): + schema = Schema(frozenset([int])) + with raises(Invalid, 'expected a frozenset'): + schema(42) + with raises(Invalid, 'expected a frozenset'): + schema(set([42])) + + schema(frozenset()) + schema(frozenset([42])) + schema(frozenset([42, 43, 44])) + try: + schema(frozenset(['abc'])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in frozenset") + else: + assert False, "Did not raise Invalid" + + +def test_set_of_integers_and_strings(): + schema = Schema({int, str}) + with raises(Invalid, 'expected a set'): + schema(42) + + schema(set()) + schema(set([42])) + schema(set(['abc'])) + schema(set([42, 'abc'])) + try: + schema(set([None])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in set") + else: + assert False, "Did not raise Invalid" + + +def test_frozenset_of_integers_and_strings(): + schema = Schema(frozenset([int, str])) + with raises(Invalid, 'expected a frozenset'): + schema(42) + + schema(frozenset()) + schema(frozenset([42])) + schema(frozenset(['abc'])) + schema(frozenset([42, 'abc'])) + try: + schema(frozenset([None])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in frozenset") + else: + assert False, "Did not raise Invalid"