diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5112703 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## [Unreleased] + +**Changes**: + +- [#198](https://github.com/alecthomas/voluptuous/issues/198): + `{}` and `[]` now always evaluate as is, instead of as any dict or any list. + To specify a free-form list, use `list` instead of `[]`. To specify a + free-form dict, use `dict` instead of `Schema({}, extra=ALLOW_EXTRA)`. + +**New**: + +**Fixes**: + +## 0.9.3 (2016-08-03) + +Changelog not kept for 0.9.3 and earlier releases. diff --git a/README.md b/README.md index 1eae118..f85f838 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ To file a bug, create a [new issue](https://github.com/alecthomas/voluptuous/iss The documentation is provided [here] (http://alecthomas.github.io/voluptuous/). +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). + ## Show me an example Twitter's [user search API](https://dev.twitter.com/docs/api/1/get/users/search) accepts @@ -224,10 +228,13 @@ contain anything, specify it as `list`: ```pycon >>> schema = Schema([]) ->>> schema([1]) # doctest: +IGNORE_EXCEPTION_DETAIL -Traceback (most recent call last): - ... -MultipleInvalid: not a valid value +>>> try: +... schema([1]) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "not a valid value" +True >>> schema([]) [] >>> schema = Schema(list) @@ -360,6 +367,28 @@ token `extra` as a key: ``` +However, an empty dict (`{}`) is treated as is. If you want to specify a list that can +contain anything, specify it as `dict`: + +```pycon +>>> schema = Schema({}, extra=ALLOW_EXTRA) # don't do this +>>> try: +... schema({'extra': 1}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "not a valid value" +True +>>> schema({}) +{} +>>> schema = Schema(dict) # do this instead +>>> schema({}) +{} +>>> schema({'extra': 1}) +{'extra': 1} + +``` + #### Required dictionary keys By default, keys in the schema are not required to be in the data: diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8f93863..04fc86b 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -201,7 +201,7 @@ def _compile(self, schema): return lambda _, v: v if isinstance(schema, Object): return self._compile_object(schema) - if isinstance(schema, collections.Mapping): + if isinstance(schema, collections.Mapping) and len(schema): return self._compile_dict(schema) elif isinstance(schema, list) and len(schema): return self._compile_list(schema) @@ -366,7 +366,7 @@ def _compile_dict(self, schema): A dictionary schema will only validate a dictionary: - >>> validate = Schema({}) + >>> validate = Schema({'prop': str}) >>> with raises(er.MultipleInvalid, 'expected a dictionary'): ... validate([]) @@ -381,7 +381,6 @@ def _compile_dict(self, schema): >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['two']"): ... validate({'two': 'three'}) - Validation function, in this case the "int" type: >>> validate = Schema({'one': 'two', 'three': 'four', int: str}) @@ -391,10 +390,17 @@ def _compile_dict(self, schema): >>> validate({10: 'twenty'}) {10: 'twenty'} + An empty dictionary is matched as value: + + >>> validate = Schema({}) + >>> with raises(er.MultipleInvalid, 'not a valid value'): + ... validate([]) + By default, a "type" in the schema (in this case "int") will be used purely to validate that the corresponding value is of that type. It will not Coerce the value: + >>> validate = Schema({'one': 'two', 'three': 'four', int: str}) >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['10']"): ... validate({'10': 'twenty'}) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 86a43ce..debd09a 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -479,6 +479,40 @@ def test_empty_list_as_exact(): s([]) +def test_empty_dict_as_exact(): + # {} always evaluates as {} + s = Schema({}) + assert_raises(Invalid, s, {'extra': 1}) + s = Schema({}, extra=ALLOW_EXTRA) # this should not be used + assert_raises(Invalid, s, {'extra': 1}) + + # {...} evaluates as Schema({...}) + s = Schema({'foo': int}) + assert_raises(Invalid, s, {'foo': 1, 'extra': 1}) + s = Schema({'foo': int}, extra=ALLOW_EXTRA) + s({'foo': 1, 'extra': 1}) + + # dict matches {} or {...} + s = Schema(dict) + s({'extra': 1}) + s({}) + s = Schema(dict, extra=PREVENT_EXTRA) + s({'extra': 1}) + s({}) + + # nested {} evaluate as {} + s = Schema({ + 'inner': {} + }, extra=ALLOW_EXTRA) + assert_raises(Invalid, s, {'inner': {'extra': 1}}) + s({}) + s = Schema({ + 'inner': Schema({}, extra=ALLOW_EXTRA) + }) + assert_raises(Invalid, s, {'inner': {'extra': 1}}) + s({}) + + def test_schema_decorator_match_with_args(): @validate(int) def fn(arg):