# HG changeset patch # User Dave Hunt # Date 1527875783 -3600 # Fri Jun 01 18:56:23 2018 +0100 # Node ID 980204366869bc7506672bd124d2ec3d8488efee # Parent cbdc1c8ef5679d1608ee187e4bd1713dd0142974 Bug 1466211 - Vendor voluptuous via |mach vendor python|; r=ahal MozReview-Commit-ID: 21q9i0lStU3 diff --git a/Pipfile b/Pipfile --- a/Pipfile +++ b/Pipfile @@ -10,12 +10,13 @@ pipenv = "==2018.5.18" virtualenv = "==15.2.0" six = "==1.10.0" attrs = "==18.1.0" pytest = "==3.2.5" jsmin = "==2.1.0" python-hglib = "==2.4" requests = "==2.9.1" json-e = "==2.5.0" +voluptuous = "==0.10.5" [requires] python_version = "2.7" diff --git a/Pipfile.lock b/Pipfile.lock --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,12 +1,12 @@ { "_meta": { "hash": { - "sha256": "04222e29219efa63e83dca85d6692b34f362ecbb0ea154c2e309ba5df708e7b8" + "sha256": "af4e239c88ce3d74e2e3dd7d352c3e8a203ce476c7369b2a4dc0eea7114996ba" }, "pipfile-spec": 6, "requires": { "python_version": "2.7" }, "sources": [ { "name": "pypi", @@ -100,12 +100,19 @@ "version": "==15.2.0" }, "virtualenv-clone": { "hashes": [ "sha256:4507071d81013fd03ea9930ec26bc8648b997927a11fa80e8ee81198b57e0ac7", "sha256:b5cfe535d14dc68dfc1d1bb4ac1209ea28235b91156e2bba8e250d291c3fb4f8" ], "version": "==0.3.0" + }, + "voluptuous": { + "hashes": [ + "sha256:7a7466f8dc3666a292d186d1d871a47bf2120836ccb900d5ba904674957a2396" + ], + "index": "pypi", + "version": "==0.10.5" } }, "develop": {} } diff --git a/third_party/python/voluptuous/CHANGELOG.md b/third_party/python/voluptuous/CHANGELOG.md new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/CHANGELOG.md @@ -0,0 +1,75 @@ +# Changelog + +## [Unreleased] + +## [0.10.5] + +- [#278](https://github.com/alecthomas/voluptuous/pull/278): Unicode +translation to python 2 issue fixed. + +## [0.10.2] + +**Changes**: + +- [#195](https://github.com/alecthomas/voluptuous/pull/195): + `Range` raises `RangeInvalid` when testing `math.nan`. +- [#215](https://github.com/alecthomas/voluptuous/pull/215): + `{}` 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)`. +- [#224](https://github.com/alecthomas/voluptuous/pull/224): + Change the encoding of keys in error messages from Unicode to UTF-8. + +**New**: + +- [#185](https://github.com/alecthomas/voluptuous/pull/185): + Add argument validation decorator. +- [#199](https://github.com/alecthomas/voluptuous/pull/199): + Add `Unordered`. +- [#200](https://github.com/alecthomas/voluptuous/pull/200): + Add `Equal`. +- [#207](https://github.com/alecthomas/voluptuous/pull/207): + Add `Number`. +- [#210](https://github.com/alecthomas/voluptuous/pull/210): + Add `Schema` equality check. +- [#212](https://github.com/alecthomas/voluptuous/pull/212): + Add `coveralls`. +- [#227](https://github.com/alecthomas/voluptuous/pull/227): + Improve `Marker` management in `Schema`. +- [#232](https://github.com/alecthomas/voluptuous/pull/232): + Add `Maybe`. +- [#234](https://github.com/alecthomas/voluptuous/pull/234): + Add `Date`. +- [#236](https://github.com/alecthomas/voluptuous/pull/236), [#237](https://github.com/alecthomas/voluptuous/pull/237), and [#238](https://github.com/alecthomas/voluptuous/pull/238): + Add script for updating `gh-pages`. +- [#256](https://github.com/alecthomas/voluptuous/pull/256): + Add support for `OrderedDict` validation. +- [#258](https://github.com/alecthomas/voluptuous/pull/258): + Add `Contains`. + +**Fixes**: + +- [#197](https://github.com/alecthomas/voluptuous/pull/197): + `ExactSequence` checks sequences are the same length. +- [#201](https://github.com/alecthomas/voluptuous/pull/201): + Empty lists are evaluated as is. +- [#205](https://github.com/alecthomas/voluptuous/pull/205): + Filepath validators correctly handle `None`. +- [#206](https://github.com/alecthomas/voluptuous/pull/206): + Handle non-subscriptable types in `humanize_error`. +- [#231](https://github.com/alecthomas/voluptuous/pull/231): + Validate `namedtuple` as a `tuple`. +- [#235](https://github.com/alecthomas/voluptuous/pull/235): + Update docstring. +- [#249](https://github.com/alecthomas/voluptuous/pull/249): + Update documentation. +- [#262](https://github.com/alecthomas/voluptuous/pull/262): + Fix a performance issue of exponential complexity where all of the dict keys were matched against all keys in the schema. + This resulted in O(n*m) complexity where n is the number of keys in the dict being validated and m is the number of keys in the schema. + The fix ensures that each key in the dict is matched against the relevant schema keys only. It now works in O(n). +- [#266](https://github.com/alecthomas/voluptuous/pull/266): + Remove setuptools as a dependency. + +## 0.9.3 (2016-08-03) + +Changelog not kept for 0.9.3 and earlier releases. diff --git a/third_party/python/voluptuous/COPYING b/third_party/python/voluptuous/COPYING new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/COPYING @@ -0,0 +1,25 @@ +Copyright (c) 2010, Alec Thomas +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + - Neither the name of SwapOff.org nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/python/voluptuous/MANIFEST.in b/third_party/python/voluptuous/MANIFEST.in new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/MANIFEST.in @@ -0,0 +1,4 @@ +include *.md +include COPYING +include voluptuous/tests/*.py +include voluptuous/tests/*.md diff --git a/third_party/python/voluptuous/PKG-INFO b/third_party/python/voluptuous/PKG-INFO new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/PKG-INFO @@ -0,0 +1,666 @@ +Metadata-Version: 1.1 +Name: voluptuous +Version: 0.10.5 +Summary: Voluptuous is a Python data validation library +Home-page: https://github.com/alecthomas/voluptuous +Author: Alec Thomas +Author-email: alec@swapoff.org +License: BSD +Download-URL: https://pypi.python.org/pypi/voluptuous +Description: Voluptuous is a Python data validation library + ============================================== + + |Build Status| |Coverage Status| |Gitter chat| + + Voluptuous, *despite* the name, is a Python data validation library. It + is primarily intended for validating data coming into Python as JSON, + YAML, etc. + + It has three goals: + + 1. Simplicity. + 2. Support for complex data structures. + 3. Provide useful error messages. + + Contact + ------- + + Voluptuous now has a mailing list! Send a mail to + ` `__ to + subscribe. Instructions will follow. + + You can also contact me directly via `email `__ + or `Twitter `__. + + To file a bug, create a `new + issue `__ on GitHub + with a short example of how to replicate the issue. + + Documentation + ------------- + + The documentation is provided [here] + (http://alecthomas.github.io/voluptuous/). + + Changelog + --------- + + See `CHANGELOG.md `__. + + Show me an example + ------------------ + + Twitter's `user search + API `__ accepts + query URLs like: + + :: + + $ curl 'http://api.twitter.com/1.1/users/search.json?q=python&per_page=20&page=1' + + To validate this we might use a schema like: + + .. code:: pycon + + >>> from voluptuous import Schema + >>> schema = Schema({ + ... 'q': str, + ... 'per_page': int, + ... 'page': int, + ... }) + + This schema very succinctly and roughly describes the data required by + the API, and will work fine. But it has a few problems. Firstly, it + doesn't fully express the constraints of the API. According to the API, + ``per_page`` should be restricted to at most 20, defaulting to 5, for + example. To describe the semantics of the API more accurately, our + schema will need to be more thoroughly defined: + + .. code:: pycon + + >>> from voluptuous import Required, All, Length, Range + >>> schema = Schema({ + ... Required('q'): All(str, Length(min=1)), + ... Required('per_page', default=5): All(int, Range(min=1, max=20)), + ... 'page': All(int, Range(min=0)), + ... }) + + This schema fully enforces the interface defined in Twitter's + documentation, and goes a little further for completeness. + + "q" is required: + + .. code:: pycon + + >>> from voluptuous import MultipleInvalid, Invalid + >>> try: + ... schema({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data['q']" + True + + ...must be a string: + + .. code:: pycon + + >>> try: + ... schema({'q': 123}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected str for dictionary value @ data['q']" + True + + ...and must be at least one character in length: + + .. code:: pycon + + >>> try: + ... schema({'q': ''}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "length of value must be at least 1 for dictionary value @ data['q']" + True + >>> schema({'q': '#topic'}) == {'q': '#topic', 'per_page': 5} + True + + "per\_page" is a positive integer no greater than 20: + + .. code:: pycon + + >>> try: + ... schema({'q': '#topic', 'per_page': 900}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value must be at most 20 for dictionary value @ data['per_page']" + True + >>> try: + ... schema({'q': '#topic', 'per_page': -10}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value must be at least 1 for dictionary value @ data['per_page']" + True + + "page" is an integer >= 0: + + .. code:: pycon + + >>> try: + ... schema({'q': '#topic', 'per_page': 'one'}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "expected int for dictionary value @ data['per_page']" + >>> schema({'q': '#topic', 'page': 1}) == {'q': '#topic', 'page': 1, 'per_page': 5} + True + + Defining schemas + ---------------- + + Schemas are nested data structures consisting of dictionaries, lists, + scalars and *validators*. Each node in the input schema is pattern + matched against corresponding nodes in the input data. + + Literals + ~~~~~~~~ + + Literals in the schema are matched using normal equality checks: + + .. code:: pycon + + >>> schema = Schema(1) + >>> schema(1) + 1 + >>> schema = Schema('a string') + >>> schema('a string') + 'a string' + + Types + ~~~~~ + + Types in the schema are matched by checking if the corresponding value + is an instance of the type: + + .. code:: pycon + + >>> schema = Schema(int) + >>> schema(1) + 1 + >>> try: + ... schema('one') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected int" + True + + URL's + ~~~~~ + + URL's in the schema are matched by using ``urlparse`` library. + + .. code:: pycon + + >>> from voluptuous import Url + >>> schema = Schema(Url()) + >>> schema('http://w3.org') + 'http://w3.org' + >>> try: + ... schema('one') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected a URL" + True + + Lists + ~~~~~ + + Lists in the schema are treated as a set of valid values. Each element + in the schema list is compared to each value in the input data: + + .. code:: pycon + + >>> schema = Schema([1, 'a', 'string']) + >>> schema([1]) + [1] + >>> schema([1, 1, 1]) + [1, 1, 1] + >>> schema(['a', 1, 'string', 1, 'string']) + ['a', 1, 'string', 1, 'string'] + + However, an empty list (``[]``) is treated as is. If you want to specify + a list that can contain anything, specify it as ``list``: + + .. code:: pycon + + >>> schema = Schema([]) + >>> 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) + >>> schema([]) + [] + >>> schema([1, 2]) + [1, 2] + + Validation functions + ~~~~~~~~~~~~~~~~~~~~ + + Validators are simple callables that raise an ``Invalid`` exception when + they encounter invalid data. The criteria for determining validity is + entirely up to the implementation; it may check that a value is a valid + username with ``pwd.getpwnam()``, it may check that a value is of a + specific type, and so on. + + The simplest kind of validator is a Python function that raises + ValueError when its argument is invalid. Conveniently, many builtin + Python functions have this property. Here's an example of a date + validator: + + .. code:: pycon + + >>> from datetime import datetime + >>> def Date(fmt='%Y-%m-%d'): + ... return lambda v: datetime.strptime(v, fmt) + + .. code:: pycon + + >>> schema = Schema(Date()) + >>> schema('2013-03-03') + datetime.datetime(2013, 3, 3, 0, 0) + >>> try: + ... schema('2013-03') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value" + True + + In addition to simply determining if a value is valid, validators may + mutate the value into a valid form. An example of this is the + ``Coerce(type)`` function, which returns a function that coerces its + argument to the given type: + + .. code:: python + + def Coerce(type, msg=None): + """Coerce a value to a type. + + If the type constructor throws a ValueError, the value will be marked as + Invalid. + """ + def f(v): + try: + return type(v) + except ValueError: + raise Invalid(msg or ('expected %s' % type.__name__)) + return f + + This example also shows a common idiom where an optional human-readable + message can be provided. This can vastly improve the usefulness of the + resulting error messages. + + Dictionaries + ~~~~~~~~~~~~ + + Each key-value pair in a schema dictionary is validated against each + key-value pair in the corresponding data dictionary: + + .. code:: pycon + + >>> schema = Schema({1: 'one', 2: 'two'}) + >>> schema({1: 'one'}) + {1: 'one'} + + Extra dictionary keys + ^^^^^^^^^^^^^^^^^^^^^ + + By default any additional keys in the data, not in the schema will + trigger exceptions: + + .. code:: pycon + + >>> schema = Schema({2: 3}) + >>> try: + ... schema({1: 2, 2: 3}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data[1]" + True + + This behaviour can be altered on a per-schema basis. To allow additional + keys use ``Schema(..., extra=ALLOW_EXTRA)``: + + .. code:: pycon + + >>> from voluptuous import ALLOW_EXTRA + >>> schema = Schema({2: 3}, extra=ALLOW_EXTRA) + >>> schema({1: 2, 2: 3}) + {1: 2, 2: 3} + + To remove additional keys use ``Schema(..., extra=REMOVE_EXTRA)``: + + .. code:: pycon + + >>> from voluptuous import REMOVE_EXTRA + >>> schema = Schema({2: 3}, extra=REMOVE_EXTRA) + >>> schema({1: 2, 2: 3}) + {2: 3} + + It can also be overridden per-dictionary by using the catch-all marker + token ``extra`` as a key: + + .. code:: pycon + + >>> from voluptuous import Extra + >>> schema = Schema({1: {Extra: object}}) + >>> schema({1: {'foo': 'bar'}}) + {1: {'foo': 'bar'}} + + However, an empty dict (``{}``) is treated as is. If you want to specify + a list that can contain anything, specify it as ``dict``: + + .. code:: 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: + + .. code:: pycon + + >>> schema = Schema({1: 2, 3: 4}) + >>> schema({3: 4}) + {3: 4} + + Similarly to how extra\_ keys work, this behaviour can be overridden + per-schema: + + .. code:: pycon + + >>> schema = Schema({1: 2, 3: 4}, required=True) + >>> try: + ... schema({3: 4}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True + + And per-key, with the marker token ``Required(key)``: + + .. code:: pycon + + >>> schema = Schema({Required(1): 2, 3: 4}) + >>> try: + ... schema({3: 4}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True + >>> schema({1: 2}) + {1: 2} + + Optional dictionary keys + ^^^^^^^^^^^^^^^^^^^^^^^^ + + If a schema has ``required=True``, keys may be individually marked as + optional using the marker token ``Optional(key)``: + + .. code:: pycon + + >>> from voluptuous import Optional + >>> schema = Schema({1: 2, Optional(3): 4}, required=True) + >>> try: + ... schema({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True + >>> schema({1: 2}) + {1: 2} + >>> try: + ... schema({1: 2, 4: 5}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data[4]" + True + + .. code:: pycon + + >>> schema({1: 2, 3: 4}) + {1: 2, 3: 4} + + Recursive schema + ~~~~~~~~~~~~~~~~ + + There is no syntax to have a recursive schema. The best way to do it is + to have a wrapper like this: + + .. code:: pycon + + >>> from voluptuous import Schema, Any + >>> def s2(v): + ... return s1(v) + ... + >>> s1 = Schema({"key": Any(s2, "value")}) + >>> s1({"key": {"key": "value"}}) + {'key': {'key': 'value'}} + + Extending an existing Schema + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Often it comes handy to have a base ``Schema`` that is extended with + more requirements. In that case you can use ``Schema.extend`` to create + a new ``Schema``: + + .. code:: pycon + + >>> from voluptuous import Schema + >>> person = Schema({'name': str}) + >>> person_with_age = person.extend({'age': int}) + >>> sorted(list(person_with_age.schema.keys())) + ['age', 'name'] + + The original ``Schema`` remains unchanged. + + Objects + ~~~~~~~ + + Each key-value pair in a schema dictionary is validated against each + attribute-value pair in the corresponding object: + + .. code:: pycon + + >>> from voluptuous import Object + >>> class Structure(object): + ... def __init__(self, q=None): + ... self.q = q + ... def __repr__(self): + ... return ''.format(self) + ... + >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) + >>> schema(Structure(q='one')) + + + Allow None values + ~~~~~~~~~~~~~~~~~ + + To allow value to be None as well, use Any: + + .. code:: pycon + + >>> from voluptuous import Any + + >>> schema = Schema(Any(None, int)) + >>> schema(None) + >>> schema(5) + 5 + + Error reporting + --------------- + + Validators must throw an ``Invalid`` exception if invalid data is passed + to them. All other exceptions are treated as errors in the validator and + will not be caught. + + Each ``Invalid`` exception has an associated ``path`` attribute + representing the path in the data structure to our currently validating + value, as well as an ``error_message`` attribute that contains the + message of the original exception. This is especially useful when you + want to catch ``Invalid`` exceptions and give some feedback to the user, + for instance in the context of an HTTP API. + + .. code:: pycon + + >>> def validate_email(email): + ... """Validate email.""" + ... if not "@" in email: + ... raise Invalid("This email is invalid.") + ... return email + >>> schema = Schema({"email": validate_email}) + >>> exc = None + >>> try: + ... schema({"email": "whatever"}) + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "This email is invalid. for dictionary value @ data['email']" + >>> exc.path + ['email'] + >>> exc.msg + 'This email is invalid.' + >>> exc.error_message + 'This email is invalid.' + + The ``path`` attribute is used during error reporting, but also during + matching to determine whether an error should be reported to the user or + if the next match should be attempted. This is determined by comparing + the depth of the path where the check is, to the depth of the path where + the error occurred. If the error is more than one level deeper, it is + reported. + + The upshot of this is that *matching is depth-first and fail-fast*. + + To illustrate this, here is an example schema: + + .. code:: pycon + + >>> schema = Schema([[2, 3], 6]) + + Each value in the top-level list is matched depth-first in-order. Given + input data of ``[[6]]``, the inner list will match the first element of + the schema, but the literal ``6`` will not match any of the elements of + that list. This error will be reported back to the user immediately. No + backtracking is attempted: + + .. code:: pycon + + >>> try: + ... schema([[6]]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value @ data[0][0]" + True + + If we pass the data ``[6]``, the ``6`` is not a list type and so will + not recurse into the first element of the schema. Matching will continue + on to the second element in the schema, and succeed: + + .. code:: pycon + + >>> schema([6]) + [6] + + Running tests. + -------------- + + Voluptuous is using nosetests: + + :: + + $ nosetests + + Why use Voluptuous over another validation library? + --------------------------------------------------- + + **Validators are simple callables** + No need to subclass anything, just use a function. + **Errors are simple exceptions.** + A validator can just ``raise Invalid(msg)`` and expect the user to + get useful messages. + **Schemas are basic Python data structures.** + Should your data be a dictionary of integer keys to strings? + ``{int: str}`` does what you expect. List of integers, floats or + strings? ``[int, float, str]``. + **Designed from the ground up for validating more than just forms.** + Nested data structures are treated in the same way as any other + type. Need a list of dictionaries? ``[{}]`` + **Consistency.** + Types in the schema are checked as types. Values are compared as + values. Callables are called to validate. Simple. + + Other libraries and inspirations + -------------------------------- + + Voluptuous is heavily inspired by + `Validino `__, and to a lesser + extent, `jsonvalidator `__ and + `json\_schema `__. + + I greatly prefer the light-weight style promoted by these libraries to + the complexity of libraries like FormEncode. + + .. |Build Status| image:: https://travis-ci.org/alecthomas/voluptuous.png + :target: https://travis-ci.org/alecthomas/voluptuous + .. |Coverage Status| image:: https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master + :target: https://coveralls.io/github/alecthomas/voluptuous?branch=master + .. |Gitter chat| image:: https://badges.gitter.im/alecthomas.png + :target: https://gitter.im/alecthomas/Lobby + +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.1 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 diff --git a/third_party/python/voluptuous/README.md b/third_party/python/voluptuous/README.md new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/README.md @@ -0,0 +1,649 @@ +# Voluptuous is a Python data validation library + +[![Build Status](https://travis-ci.org/alecthomas/voluptuous.png)](https://travis-ci.org/alecthomas/voluptuous) +[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) [![Gitter chat](https://badges.gitter.im/alecthomas.png)](https://gitter.im/alecthomas/Lobby) + +Voluptuous, *despite* the name, is a Python data validation library. It +is primarily intended for validating data coming into Python as JSON, +YAML, etc. + +It has three goals: + +1. Simplicity. +2. Support for complex data structures. +3. Provide useful error messages. + +## Contact + +Voluptuous now has a mailing list! Send a mail to +[](mailto:voluptuous@librelist.com) to subscribe. Instructions +will follow. + +You can also contact me directly via [email](mailto:alec@swapoff.org) or +[Twitter](https://twitter.com/alecthomas). + +To file a bug, create a [new issue](https://github.com/alecthomas/voluptuous/issues/new) on GitHub with a short example of how to replicate the issue. + +## Documentation + +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/rest/reference/get/users/search) accepts +query URLs like: + +``` +$ curl 'http://api.twitter.com/1.1/users/search.json?q=python&per_page=20&page=1' +``` + +To validate this we might use a schema like: + +```pycon +>>> from voluptuous import Schema +>>> schema = Schema({ +... 'q': str, +... 'per_page': int, +... 'page': int, +... }) + +``` + +This schema very succinctly and roughly describes the data required by +the API, and will work fine. But it has a few problems. Firstly, it +doesn't fully express the constraints of the API. According to the API, +`per_page` should be restricted to at most 20, defaulting to 5, for +example. To describe the semantics of the API more accurately, our +schema will need to be more thoroughly defined: + +```pycon +>>> from voluptuous import Required, All, Length, Range +>>> schema = Schema({ +... Required('q'): All(str, Length(min=1)), +... Required('per_page', default=5): All(int, Range(min=1, max=20)), +... 'page': All(int, Range(min=0)), +... }) + +``` + +This schema fully enforces the interface defined in Twitter's +documentation, and goes a little further for completeness. + +"q" is required: + +```pycon +>>> from voluptuous import MultipleInvalid, Invalid +>>> try: +... schema({}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "required key not provided @ data['q']" +True + +``` + +...must be a string: + +```pycon +>>> try: +... schema({'q': 123}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "expected str for dictionary value @ data['q']" +True + +``` + +...and must be at least one character in length: + +```pycon +>>> try: +... schema({'q': ''}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "length of value must be at least 1 for dictionary value @ data['q']" +True +>>> schema({'q': '#topic'}) == {'q': '#topic', 'per_page': 5} +True + +``` + +"per\_page" is a positive integer no greater than 20: + +```pycon +>>> try: +... schema({'q': '#topic', 'per_page': 900}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "value must be at most 20 for dictionary value @ data['per_page']" +True +>>> try: +... schema({'q': '#topic', 'per_page': -10}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "value must be at least 1 for dictionary value @ data['per_page']" +True + +``` + +"page" is an integer \>= 0: + +```pycon +>>> try: +... schema({'q': '#topic', 'per_page': 'one'}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) +"expected int for dictionary value @ data['per_page']" +>>> schema({'q': '#topic', 'page': 1}) == {'q': '#topic', 'page': 1, 'per_page': 5} +True + +``` + +## Defining schemas + +Schemas are nested data structures consisting of dictionaries, lists, +scalars and *validators*. Each node in the input schema is pattern +matched against corresponding nodes in the input data. + +### Literals + +Literals in the schema are matched using normal equality checks: + +```pycon +>>> schema = Schema(1) +>>> schema(1) +1 +>>> schema = Schema('a string') +>>> schema('a string') +'a string' + +``` + +### Types + +Types in the schema are matched by checking if the corresponding value +is an instance of the type: + +```pycon +>>> schema = Schema(int) +>>> schema(1) +1 +>>> try: +... schema('one') +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "expected int" +True + +``` + +### URL's + +URL's in the schema are matched by using `urlparse` library. + +```pycon +>>> from voluptuous import Url +>>> schema = Schema(Url()) +>>> schema('http://w3.org') +'http://w3.org' +>>> try: +... schema('one') +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "expected a URL" +True + +``` + +### Lists + +Lists in the schema are treated as a set of valid values. Each element +in the schema list is compared to each value in the input data: + +```pycon +>>> schema = Schema([1, 'a', 'string']) +>>> schema([1]) +[1] +>>> schema([1, 1, 1]) +[1, 1, 1] +>>> schema(['a', 1, 'string', 1, 'string']) +['a', 1, 'string', 1, 'string'] + +``` + +However, an empty list (`[]`) is treated as is. If you want to specify a list that can +contain anything, specify it as `list`: + +```pycon +>>> schema = Schema([]) +>>> 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) +>>> schema([]) +[] +>>> schema([1, 2]) +[1, 2] + +``` + +### Validation functions + +Validators are simple callables that raise an `Invalid` exception when +they encounter invalid data. The criteria for determining validity is +entirely up to the implementation; it may check that a value is a valid +username with `pwd.getpwnam()`, it may check that a value is of a +specific type, and so on. + +The simplest kind of validator is a Python function that raises +ValueError when its argument is invalid. Conveniently, many builtin +Python functions have this property. Here's an example of a date +validator: + +```pycon +>>> from datetime import datetime +>>> def Date(fmt='%Y-%m-%d'): +... return lambda v: datetime.strptime(v, fmt) + +``` + +```pycon +>>> schema = Schema(Date()) +>>> schema('2013-03-03') +datetime.datetime(2013, 3, 3, 0, 0) +>>> try: +... schema('2013-03') +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "not a valid value" +True + +``` + +In addition to simply determining if a value is valid, validators may +mutate the value into a valid form. An example of this is the +`Coerce(type)` function, which returns a function that coerces its +argument to the given type: + +```python +def Coerce(type, msg=None): + """Coerce a value to a type. + + If the type constructor throws a ValueError, the value will be marked as + Invalid. + """ + def f(v): + try: + return type(v) + except ValueError: + raise Invalid(msg or ('expected %s' % type.__name__)) + return f + +``` + +This example also shows a common idiom where an optional human-readable +message can be provided. This can vastly improve the usefulness of the +resulting error messages. + +### Dictionaries + +Each key-value pair in a schema dictionary is validated against each +key-value pair in the corresponding data dictionary: + +```pycon +>>> schema = Schema({1: 'one', 2: 'two'}) +>>> schema({1: 'one'}) +{1: 'one'} + +``` + +#### Extra dictionary keys + +By default any additional keys in the data, not in the schema will +trigger exceptions: + +```pycon +>>> schema = Schema({2: 3}) +>>> try: +... schema({1: 2, 2: 3}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "extra keys not allowed @ data[1]" +True + +``` + +This behaviour can be altered on a per-schema basis. To allow +additional keys use +`Schema(..., extra=ALLOW_EXTRA)`: + +```pycon +>>> from voluptuous import ALLOW_EXTRA +>>> schema = Schema({2: 3}, extra=ALLOW_EXTRA) +>>> schema({1: 2, 2: 3}) +{1: 2, 2: 3} + +``` + +To remove additional keys use +`Schema(..., extra=REMOVE_EXTRA)`: + +```pycon +>>> from voluptuous import REMOVE_EXTRA +>>> schema = Schema({2: 3}, extra=REMOVE_EXTRA) +>>> schema({1: 2, 2: 3}) +{2: 3} + +``` + +It can also be overridden per-dictionary by using the catch-all marker +token `extra` as a key: + +```pycon +>>> from voluptuous import Extra +>>> schema = Schema({1: {Extra: object}}) +>>> schema({1: {'foo': 'bar'}}) +{1: {'foo': 'bar'}} + +``` + +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: + +```pycon +>>> schema = Schema({1: 2, 3: 4}) +>>> schema({3: 4}) +{3: 4} + +``` + +Similarly to how extra\_ keys work, this behaviour can be overridden +per-schema: + +```pycon +>>> schema = Schema({1: 2, 3: 4}, required=True) +>>> try: +... schema({3: 4}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "required key not provided @ data[1]" +True + +``` + +And per-key, with the marker token `Required(key)`: + +```pycon +>>> schema = Schema({Required(1): 2, 3: 4}) +>>> try: +... schema({3: 4}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "required key not provided @ data[1]" +True +>>> schema({1: 2}) +{1: 2} + +``` + +#### Optional dictionary keys + +If a schema has `required=True`, keys may be individually marked as +optional using the marker token `Optional(key)`: + +```pycon +>>> from voluptuous import Optional +>>> schema = Schema({1: 2, Optional(3): 4}, required=True) +>>> try: +... schema({}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "required key not provided @ data[1]" +True +>>> schema({1: 2}) +{1: 2} +>>> try: +... schema({1: 2, 4: 5}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "extra keys not allowed @ data[4]" +True + +``` + +```pycon +>>> schema({1: 2, 3: 4}) +{1: 2, 3: 4} + +``` + +### Recursive schema + +There is no syntax to have a recursive schema. The best way to do it is to have a wrapper like this: + +```pycon +>>> from voluptuous import Schema, Any +>>> def s2(v): +... return s1(v) +... +>>> s1 = Schema({"key": Any(s2, "value")}) +>>> s1({"key": {"key": "value"}}) +{'key': {'key': 'value'}} + +``` + +### Extending an existing Schema + +Often it comes handy to have a base `Schema` that is extended with more +requirements. In that case you can use `Schema.extend` to create a new +`Schema`: + +```pycon +>>> from voluptuous import Schema +>>> person = Schema({'name': str}) +>>> person_with_age = person.extend({'age': int}) +>>> sorted(list(person_with_age.schema.keys())) +['age', 'name'] + +``` + +The original `Schema` remains unchanged. + +### Objects + +Each key-value pair in a schema dictionary is validated against each +attribute-value pair in the corresponding object: + +```pycon +>>> from voluptuous import Object +>>> class Structure(object): +... def __init__(self, q=None): +... self.q = q +... def __repr__(self): +... return ''.format(self) +... +>>> schema = Schema(Object({'q': 'one'}, cls=Structure)) +>>> schema(Structure(q='one')) + + +``` + +### Allow None values + +To allow value to be None as well, use Any: + +```pycon +>>> from voluptuous import Any + +>>> schema = Schema(Any(None, int)) +>>> schema(None) +>>> schema(5) +5 + +``` + +## Error reporting + +Validators must throw an `Invalid` exception if invalid data is passed +to them. All other exceptions are treated as errors in the validator and +will not be caught. + +Each `Invalid` exception has an associated `path` attribute representing +the path in the data structure to our currently validating value, as well +as an `error_message` attribute that contains the message of the original +exception. This is especially useful when you want to catch `Invalid` +exceptions and give some feedback to the user, for instance in the context of +an HTTP API. + + +```pycon +>>> def validate_email(email): +... """Validate email.""" +... if not "@" in email: +... raise Invalid("This email is invalid.") +... return email +>>> schema = Schema({"email": validate_email}) +>>> exc = None +>>> try: +... schema({"email": "whatever"}) +... except MultipleInvalid as e: +... exc = e +>>> str(exc) +"This email is invalid. for dictionary value @ data['email']" +>>> exc.path +['email'] +>>> exc.msg +'This email is invalid.' +>>> exc.error_message +'This email is invalid.' + +``` + +The `path` attribute is used during error reporting, but also during matching +to determine whether an error should be reported to the user or if the next +match should be attempted. This is determined by comparing the depth of the +path where the check is, to the depth of the path where the error occurred. If +the error is more than one level deeper, it is reported. + +The upshot of this is that *matching is depth-first and fail-fast*. + +To illustrate this, here is an example schema: + +```pycon +>>> schema = Schema([[2, 3], 6]) + +``` + +Each value in the top-level list is matched depth-first in-order. Given +input data of `[[6]]`, the inner list will match the first element of +the schema, but the literal `6` will not match any of the elements of +that list. This error will be reported back to the user immediately. No +backtracking is attempted: + +```pycon +>>> try: +... schema([[6]]) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "not a valid value @ data[0][0]" +True + +``` + +If we pass the data `[6]`, the `6` is not a list type and so will not +recurse into the first element of the schema. Matching will continue on +to the second element in the schema, and succeed: + +```pycon +>>> schema([6]) +[6] + +``` + +## Running tests. + +Voluptuous is using nosetests: + + $ nosetests + + +## Why use Voluptuous over another validation library? + +**Validators are simple callables** +: No need to subclass anything, just use a function. + +**Errors are simple exceptions.** +: A validator can just `raise Invalid(msg)` and expect the user to get +useful messages. + +**Schemas are basic Python data structures.** +: Should your data be a dictionary of integer keys to strings? +`{int: str}` does what you expect. List of integers, floats or +strings? `[int, float, str]`. + +**Designed from the ground up for validating more than just forms.** +: Nested data structures are treated in the same way as any other +type. Need a list of dictionaries? `[{}]` + +**Consistency.** +: Types in the schema are checked as types. Values are compared as +values. Callables are called to validate. Simple. + +## Other libraries and inspirations + +Voluptuous is heavily inspired by +[Validino](http://code.google.com/p/validino/), and to a lesser extent, +[jsonvalidator](http://code.google.com/p/jsonvalidator/) and +[json\_schema](http://blog.sendapatch.se/category/json_schema.html). + +I greatly prefer the light-weight style promoted by these libraries to +the complexity of libraries like FormEncode. diff --git a/third_party/python/voluptuous/README.rst b/third_party/python/voluptuous/README.rst new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/README.rst @@ -0,0 +1,644 @@ +Voluptuous is a Python data validation library +============================================== + +|Build Status| |Coverage Status| |Gitter chat| + +Voluptuous, *despite* the name, is a Python data validation library. It +is primarily intended for validating data coming into Python as JSON, +YAML, etc. + +It has three goals: + +1. Simplicity. +2. Support for complex data structures. +3. Provide useful error messages. + +Contact +------- + +Voluptuous now has a mailing list! Send a mail to +` `__ to +subscribe. Instructions will follow. + +You can also contact me directly via `email `__ +or `Twitter `__. + +To file a bug, create a `new +issue `__ on GitHub +with a short example of how to replicate the issue. + +Documentation +------------- + +The documentation is provided [here] +(http://alecthomas.github.io/voluptuous/). + +Changelog +--------- + +See `CHANGELOG.md `__. + +Show me an example +------------------ + +Twitter's `user search +API `__ accepts +query URLs like: + +:: + + $ curl 'http://api.twitter.com/1.1/users/search.json?q=python&per_page=20&page=1' + +To validate this we might use a schema like: + +.. code:: pycon + + >>> from voluptuous import Schema + >>> schema = Schema({ + ... 'q': str, + ... 'per_page': int, + ... 'page': int, + ... }) + +This schema very succinctly and roughly describes the data required by +the API, and will work fine. But it has a few problems. Firstly, it +doesn't fully express the constraints of the API. According to the API, +``per_page`` should be restricted to at most 20, defaulting to 5, for +example. To describe the semantics of the API more accurately, our +schema will need to be more thoroughly defined: + +.. code:: pycon + + >>> from voluptuous import Required, All, Length, Range + >>> schema = Schema({ + ... Required('q'): All(str, Length(min=1)), + ... Required('per_page', default=5): All(int, Range(min=1, max=20)), + ... 'page': All(int, Range(min=0)), + ... }) + +This schema fully enforces the interface defined in Twitter's +documentation, and goes a little further for completeness. + +"q" is required: + +.. code:: pycon + + >>> from voluptuous import MultipleInvalid, Invalid + >>> try: + ... schema({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data['q']" + True + +...must be a string: + +.. code:: pycon + + >>> try: + ... schema({'q': 123}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected str for dictionary value @ data['q']" + True + +...and must be at least one character in length: + +.. code:: pycon + + >>> try: + ... schema({'q': ''}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "length of value must be at least 1 for dictionary value @ data['q']" + True + >>> schema({'q': '#topic'}) == {'q': '#topic', 'per_page': 5} + True + +"per\_page" is a positive integer no greater than 20: + +.. code:: pycon + + >>> try: + ... schema({'q': '#topic', 'per_page': 900}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value must be at most 20 for dictionary value @ data['per_page']" + True + >>> try: + ... schema({'q': '#topic', 'per_page': -10}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "value must be at least 1 for dictionary value @ data['per_page']" + True + +"page" is an integer >= 0: + +.. code:: pycon + + >>> try: + ... schema({'q': '#topic', 'per_page': 'one'}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "expected int for dictionary value @ data['per_page']" + >>> schema({'q': '#topic', 'page': 1}) == {'q': '#topic', 'page': 1, 'per_page': 5} + True + +Defining schemas +---------------- + +Schemas are nested data structures consisting of dictionaries, lists, +scalars and *validators*. Each node in the input schema is pattern +matched against corresponding nodes in the input data. + +Literals +~~~~~~~~ + +Literals in the schema are matched using normal equality checks: + +.. code:: pycon + + >>> schema = Schema(1) + >>> schema(1) + 1 + >>> schema = Schema('a string') + >>> schema('a string') + 'a string' + +Types +~~~~~ + +Types in the schema are matched by checking if the corresponding value +is an instance of the type: + +.. code:: pycon + + >>> schema = Schema(int) + >>> schema(1) + 1 + >>> try: + ... schema('one') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected int" + True + +URL's +~~~~~ + +URL's in the schema are matched by using ``urlparse`` library. + +.. code:: pycon + + >>> from voluptuous import Url + >>> schema = Schema(Url()) + >>> schema('http://w3.org') + 'http://w3.org' + >>> try: + ... schema('one') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "expected a URL" + True + +Lists +~~~~~ + +Lists in the schema are treated as a set of valid values. Each element +in the schema list is compared to each value in the input data: + +.. code:: pycon + + >>> schema = Schema([1, 'a', 'string']) + >>> schema([1]) + [1] + >>> schema([1, 1, 1]) + [1, 1, 1] + >>> schema(['a', 1, 'string', 1, 'string']) + ['a', 1, 'string', 1, 'string'] + +However, an empty list (``[]``) is treated as is. If you want to specify +a list that can contain anything, specify it as ``list``: + +.. code:: pycon + + >>> schema = Schema([]) + >>> 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) + >>> schema([]) + [] + >>> schema([1, 2]) + [1, 2] + +Validation functions +~~~~~~~~~~~~~~~~~~~~ + +Validators are simple callables that raise an ``Invalid`` exception when +they encounter invalid data. The criteria for determining validity is +entirely up to the implementation; it may check that a value is a valid +username with ``pwd.getpwnam()``, it may check that a value is of a +specific type, and so on. + +The simplest kind of validator is a Python function that raises +ValueError when its argument is invalid. Conveniently, many builtin +Python functions have this property. Here's an example of a date +validator: + +.. code:: pycon + + >>> from datetime import datetime + >>> def Date(fmt='%Y-%m-%d'): + ... return lambda v: datetime.strptime(v, fmt) + +.. code:: pycon + + >>> schema = Schema(Date()) + >>> schema('2013-03-03') + datetime.datetime(2013, 3, 3, 0, 0) + >>> try: + ... schema('2013-03') + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value" + True + +In addition to simply determining if a value is valid, validators may +mutate the value into a valid form. An example of this is the +``Coerce(type)`` function, which returns a function that coerces its +argument to the given type: + +.. code:: python + + def Coerce(type, msg=None): + """Coerce a value to a type. + + If the type constructor throws a ValueError, the value will be marked as + Invalid. + """ + def f(v): + try: + return type(v) + except ValueError: + raise Invalid(msg or ('expected %s' % type.__name__)) + return f + +This example also shows a common idiom where an optional human-readable +message can be provided. This can vastly improve the usefulness of the +resulting error messages. + +Dictionaries +~~~~~~~~~~~~ + +Each key-value pair in a schema dictionary is validated against each +key-value pair in the corresponding data dictionary: + +.. code:: pycon + + >>> schema = Schema({1: 'one', 2: 'two'}) + >>> schema({1: 'one'}) + {1: 'one'} + +Extra dictionary keys +^^^^^^^^^^^^^^^^^^^^^ + +By default any additional keys in the data, not in the schema will +trigger exceptions: + +.. code:: pycon + + >>> schema = Schema({2: 3}) + >>> try: + ... schema({1: 2, 2: 3}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data[1]" + True + +This behaviour can be altered on a per-schema basis. To allow additional +keys use ``Schema(..., extra=ALLOW_EXTRA)``: + +.. code:: pycon + + >>> from voluptuous import ALLOW_EXTRA + >>> schema = Schema({2: 3}, extra=ALLOW_EXTRA) + >>> schema({1: 2, 2: 3}) + {1: 2, 2: 3} + +To remove additional keys use ``Schema(..., extra=REMOVE_EXTRA)``: + +.. code:: pycon + + >>> from voluptuous import REMOVE_EXTRA + >>> schema = Schema({2: 3}, extra=REMOVE_EXTRA) + >>> schema({1: 2, 2: 3}) + {2: 3} + +It can also be overridden per-dictionary by using the catch-all marker +token ``extra`` as a key: + +.. code:: pycon + + >>> from voluptuous import Extra + >>> schema = Schema({1: {Extra: object}}) + >>> schema({1: {'foo': 'bar'}}) + {1: {'foo': 'bar'}} + +However, an empty dict (``{}``) is treated as is. If you want to specify +a list that can contain anything, specify it as ``dict``: + +.. code:: 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: + +.. code:: pycon + + >>> schema = Schema({1: 2, 3: 4}) + >>> schema({3: 4}) + {3: 4} + +Similarly to how extra\_ keys work, this behaviour can be overridden +per-schema: + +.. code:: pycon + + >>> schema = Schema({1: 2, 3: 4}, required=True) + >>> try: + ... schema({3: 4}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True + +And per-key, with the marker token ``Required(key)``: + +.. code:: pycon + + >>> schema = Schema({Required(1): 2, 3: 4}) + >>> try: + ... schema({3: 4}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True + >>> schema({1: 2}) + {1: 2} + +Optional dictionary keys +^^^^^^^^^^^^^^^^^^^^^^^^ + +If a schema has ``required=True``, keys may be individually marked as +optional using the marker token ``Optional(key)``: + +.. code:: pycon + + >>> from voluptuous import Optional + >>> schema = Schema({1: 2, Optional(3): 4}, required=True) + >>> try: + ... schema({}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "required key not provided @ data[1]" + True + >>> schema({1: 2}) + {1: 2} + >>> try: + ... schema({1: 2, 4: 5}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "extra keys not allowed @ data[4]" + True + +.. code:: pycon + + >>> schema({1: 2, 3: 4}) + {1: 2, 3: 4} + +Recursive schema +~~~~~~~~~~~~~~~~ + +There is no syntax to have a recursive schema. The best way to do it is +to have a wrapper like this: + +.. code:: pycon + + >>> from voluptuous import Schema, Any + >>> def s2(v): + ... return s1(v) + ... + >>> s1 = Schema({"key": Any(s2, "value")}) + >>> s1({"key": {"key": "value"}}) + {'key': {'key': 'value'}} + +Extending an existing Schema +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Often it comes handy to have a base ``Schema`` that is extended with +more requirements. In that case you can use ``Schema.extend`` to create +a new ``Schema``: + +.. code:: pycon + + >>> from voluptuous import Schema + >>> person = Schema({'name': str}) + >>> person_with_age = person.extend({'age': int}) + >>> sorted(list(person_with_age.schema.keys())) + ['age', 'name'] + +The original ``Schema`` remains unchanged. + +Objects +~~~~~~~ + +Each key-value pair in a schema dictionary is validated against each +attribute-value pair in the corresponding object: + +.. code:: pycon + + >>> from voluptuous import Object + >>> class Structure(object): + ... def __init__(self, q=None): + ... self.q = q + ... def __repr__(self): + ... return ''.format(self) + ... + >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) + >>> schema(Structure(q='one')) + + +Allow None values +~~~~~~~~~~~~~~~~~ + +To allow value to be None as well, use Any: + +.. code:: pycon + + >>> from voluptuous import Any + + >>> schema = Schema(Any(None, int)) + >>> schema(None) + >>> schema(5) + 5 + +Error reporting +--------------- + +Validators must throw an ``Invalid`` exception if invalid data is passed +to them. All other exceptions are treated as errors in the validator and +will not be caught. + +Each ``Invalid`` exception has an associated ``path`` attribute +representing the path in the data structure to our currently validating +value, as well as an ``error_message`` attribute that contains the +message of the original exception. This is especially useful when you +want to catch ``Invalid`` exceptions and give some feedback to the user, +for instance in the context of an HTTP API. + +.. code:: pycon + + >>> def validate_email(email): + ... """Validate email.""" + ... if not "@" in email: + ... raise Invalid("This email is invalid.") + ... return email + >>> schema = Schema({"email": validate_email}) + >>> exc = None + >>> try: + ... schema({"email": "whatever"}) + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "This email is invalid. for dictionary value @ data['email']" + >>> exc.path + ['email'] + >>> exc.msg + 'This email is invalid.' + >>> exc.error_message + 'This email is invalid.' + +The ``path`` attribute is used during error reporting, but also during +matching to determine whether an error should be reported to the user or +if the next match should be attempted. This is determined by comparing +the depth of the path where the check is, to the depth of the path where +the error occurred. If the error is more than one level deeper, it is +reported. + +The upshot of this is that *matching is depth-first and fail-fast*. + +To illustrate this, here is an example schema: + +.. code:: pycon + + >>> schema = Schema([[2, 3], 6]) + +Each value in the top-level list is matched depth-first in-order. Given +input data of ``[[6]]``, the inner list will match the first element of +the schema, but the literal ``6`` will not match any of the elements of +that list. This error will be reported back to the user immediately. No +backtracking is attempted: + +.. code:: pycon + + >>> try: + ... schema([[6]]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == "not a valid value @ data[0][0]" + True + +If we pass the data ``[6]``, the ``6`` is not a list type and so will +not recurse into the first element of the schema. Matching will continue +on to the second element in the schema, and succeed: + +.. code:: pycon + + >>> schema([6]) + [6] + +Running tests. +-------------- + +Voluptuous is using nosetests: + +:: + + $ nosetests + +Why use Voluptuous over another validation library? +--------------------------------------------------- + +**Validators are simple callables** + No need to subclass anything, just use a function. +**Errors are simple exceptions.** + A validator can just ``raise Invalid(msg)`` and expect the user to + get useful messages. +**Schemas are basic Python data structures.** + Should your data be a dictionary of integer keys to strings? + ``{int: str}`` does what you expect. List of integers, floats or + strings? ``[int, float, str]``. +**Designed from the ground up for validating more than just forms.** + Nested data structures are treated in the same way as any other + type. Need a list of dictionaries? ``[{}]`` +**Consistency.** + Types in the schema are checked as types. Values are compared as + values. Callables are called to validate. Simple. + +Other libraries and inspirations +-------------------------------- + +Voluptuous is heavily inspired by +`Validino `__, and to a lesser +extent, `jsonvalidator `__ and +`json\_schema `__. + +I greatly prefer the light-weight style promoted by these libraries to +the complexity of libraries like FormEncode. + +.. |Build Status| image:: https://travis-ci.org/alecthomas/voluptuous.png + :target: https://travis-ci.org/alecthomas/voluptuous +.. |Coverage Status| image:: https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master + :target: https://coveralls.io/github/alecthomas/voluptuous?branch=master +.. |Gitter chat| image:: https://badges.gitter.im/alecthomas.png + :target: https://gitter.im/alecthomas/Lobby diff --git a/third_party/python/voluptuous/setup.cfg b/third_party/python/voluptuous/setup.cfg new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/setup.cfg @@ -0,0 +1,9 @@ +[nosetests] +doctest-extension = md +with-doctest = 1 +where = . + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/third_party/python/voluptuous/setup.py b/third_party/python/voluptuous/setup.py new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/setup.py @@ -0,0 +1,51 @@ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +import sys +import os +import atexit +sys.path.insert(0, '.') +version = __import__('voluptuous').__version__ + +try: + import pypandoc + long_description = pypandoc.convert('README.md', 'rst') + with open('README.rst', 'w') as f: + f.write(long_description) + atexit.register(lambda: os.unlink('README.rst')) +except (ImportError, OSError): + print('WARNING: Could not locate pandoc, using Markdown long_description.') + with open('README.md') as f: + long_description = f.read() + +description = long_description.splitlines()[0].strip() + + +setup( + name='voluptuous', + url='https://github.com/alecthomas/voluptuous', + download_url='https://pypi.python.org/pypi/voluptuous', + version=version, + description=description, + long_description=long_description, + license='BSD', + platforms=['any'], + packages=['voluptuous'], + author='Alec Thomas', + author_email='alec@swapoff.org', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.1', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ] +) diff --git a/third_party/python/voluptuous/voluptuous/tests/__init__.py b/third_party/python/voluptuous/voluptuous/tests/__init__.py new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/voluptuous/tests/__init__.py @@ -0,0 +1,1 @@ +__author__ = 'tusharmakkar08' diff --git a/third_party/python/voluptuous/voluptuous/tests/tests.md b/third_party/python/voluptuous/voluptuous/tests/tests.md new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/voluptuous/tests/tests.md @@ -0,0 +1,268 @@ +Error reporting should be accurate: + + >>> from voluptuous import * + >>> schema = Schema(['one', {'two': 'three', 'four': ['five'], + ... 'six': {'seven': 'eight'}}]) + >>> schema(['one']) + ['one'] + >>> schema([{'two': 'three'}]) + [{'two': 'three'}] + +It should show the exact index and container type, in this case a list +value: + + >>> try: + ... schema(['one', 'two']) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) == 'expected a dictionary @ data[1]' + True + +It should also be accurate for nested values: + + >>> try: + ... schema([{'two': 'nine'}]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "not a valid value for dictionary value @ data[0]['two']" + + >>> try: + ... schema([{'four': ['nine']}]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "not a valid value @ data[0]['four'][0]" + + >>> try: + ... schema([{'six': {'seven': 'nine'}}]) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "not a valid value for dictionary value @ data[0]['six']['seven']" + +Errors should be reported depth-first: + + >>> validate = Schema({'one': {'two': 'three', 'four': 'five'}}) + >>> try: + ... validate({'one': {'four': 'six'}}) + ... except Invalid as e: + ... print(e) + ... print(e.path) + not a valid value for dictionary value @ data['one']['four'] + ['one', 'four'] + +Voluptuous supports validation when extra fields are present in the +data: + + >>> schema = Schema({'one': 1, Extra: object}) + >>> schema({'two': 'two', 'one': 1}) == {'two': 'two', 'one': 1} + True + >>> schema = Schema({'one': 1}) + >>> try: + ... schema({'two': 2}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "extra keys not allowed @ data['two']" + +dict, list, and tuple should be available as type validators: + + >>> Schema(dict)({'a': 1, 'b': 2}) == {'a': 1, 'b': 2} + True + >>> Schema(list)([1,2,3]) + [1, 2, 3] + >>> Schema(tuple)((1,2,3)) + (1, 2, 3) + +Validation should return instances of the right types when the types are +subclasses of dict or list: + + >>> class Dict(dict): + ... pass + >>> + >>> d = Schema(dict)(Dict(a=1, b=2)) + >>> d == {'a': 1, 'b': 2} + True + >>> type(d) is Dict + True + >>> class List(list): + ... pass + >>> + >>> l = Schema(list)(List([1,2,3])) + >>> l + [1, 2, 3] + >>> type(l) is List + True + +Multiple errors are reported: + + >>> schema = Schema({'one': 1, 'two': 2}) + >>> try: + ... schema({'one': 2, 'two': 3, 'three': 4}) + ... except MultipleInvalid as e: + ... errors = sorted(e.errors, key=lambda k: str(k)) + ... print([str(i) for i in errors]) # doctest: +NORMALIZE_WHITESPACE + ["extra keys not allowed @ data['three']", + "not a valid value for dictionary value @ data['one']", + "not a valid value for dictionary value @ data['two']"] + >>> schema = Schema([[1], [2], [3]]) + >>> try: + ... schema([1, 2, 3]) + ... except MultipleInvalid as e: + ... print([str(i) for i in e.errors]) # doctest: +NORMALIZE_WHITESPACE + ['expected a list @ data[0]', + 'expected a list @ data[1]', + 'expected a list @ data[2]'] + +Required fields in dictionary which are invalid should not have required : + + >>> from voluptuous import * + >>> schema = Schema({'one': {'two': 3}}, required=True) + >>> try: + ... schema({'one': {'two': 2}}) + ... except MultipleInvalid as e: + ... errors = e.errors + >>> 'required' in ' '.join([x.msg for x in errors]) + False + +Multiple errors for nested fields in dicts and objects: + +> \>\>\> from collections import namedtuple \>\>\> validate = Schema({ +> ... 'anobject': Object({ ... 'strfield': str, ... 'intfield': int ... +> }) ... }) \>\>\> try: ... SomeObj = namedtuple('SomeObj', ('strfield', +> 'intfield')) ... validate({'anobject': SomeObj(strfield=123, +> intfield='one')}) ... except MultipleInvalid as e: ... +> print(sorted(str(i) for i in e.errors)) \# doctest: +> +NORMALIZE\_WHITESPACE ["expected int for object value @ +> data['anobject']['intfield']", "expected str for object value @ +> data['anobject']['strfield']"] + +Custom classes validate as schemas: + + >>> class Thing(object): + ... pass + >>> schema = Schema(Thing) + >>> t = schema(Thing()) + >>> type(t) is Thing + True + +Classes with custom metaclasses should validate as schemas: + + >>> class MyMeta(type): + ... pass + >>> class Thing(object): + ... __metaclass__ = MyMeta + >>> schema = Schema(Thing) + >>> t = schema(Thing()) + >>> type(t) is Thing + True + +Schemas built with All() should give the same error as the original +validator (Issue \#26): + + >>> schema = Schema({ + ... Required('items'): All([{ + ... Required('foo'): str + ... }]) + ... }) + + >>> try: + ... schema({'items': [{}]}) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "required key not provided @ data['items'][0]['foo']" + +Validator should return same instance of the same type for object: + + >>> class Structure(object): + ... def __init__(self, q=None): + ... self.q = q + ... def __repr__(self): + ... return '{0.__name__}(q={1.q!r})'.format(type(self), self) + ... + >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) + >>> type(schema(Structure(q='one'))) is Structure + True + +Object validator should treat cls argument as optional. In this case it +shouldn't check object type: + + >>> from collections import namedtuple + >>> NamedTuple = namedtuple('NamedTuple', ('q',)) + >>> schema = Schema(Object({'q': 'one'})) + >>> named = NamedTuple(q='one') + >>> schema(named) == named + True + >>> schema(named) + NamedTuple(q='one') + +If cls argument passed to object validator we should check object type: + + >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) + >>> schema(NamedTuple(q='one')) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + MultipleInvalid: expected a + >>> schema = Schema(Object({'q': 'one'}, cls=NamedTuple)) + >>> schema(NamedTuple(q='one')) + NamedTuple(q='one') + +Ensure that objects with \_\_slots\_\_ supported properly: + + >>> class SlotsStructure(Structure): + ... __slots__ = ['q'] + ... + >>> schema = Schema(Object({'q': 'one'})) + >>> schema(SlotsStructure(q='one')) + SlotsStructure(q='one') + >>> class DictStructure(object): + ... __slots__ = ['q', '__dict__'] + ... def __init__(self, q=None, page=None): + ... self.q = q + ... self.page = page + ... def __repr__(self): + ... return '{0.__name__}(q={1.q!r}, page={1.page!r})'.format(type(self), self) + ... + >>> structure = DictStructure(q='one') + >>> structure.page = 1 + >>> try: + ... schema(structure) + ... raise AssertionError('MultipleInvalid not raised') + ... except MultipleInvalid as e: + ... exc = e + >>> str(exc) + "extra keys not allowed @ data['page']" + + >>> schema = Schema(Object({'q': 'one', Extra: object})) + >>> schema(structure) + DictStructure(q='one', page=1) + +Ensure that objects can be used with other validators: + + >>> schema = Schema({'meta': Object({'q': 'one'})}) + >>> schema({'meta': Structure(q='one')}) + {'meta': Structure(q='one')} + +Ensure that subclasses of Invalid of are raised as is. + + >>> class SpecialInvalid(Invalid): + ... pass + ... + >>> def custom_validator(value): + ... raise SpecialInvalid('boom') + ... + >>> schema = Schema({'thing': custom_validator}) + >>> try: + ... schema({'thing': 'not an int'}) + ... except MultipleInvalid as e: + ... exc = e + >>> exc.errors[0].__class__.__name__ + 'SpecialInvalid' diff --git a/third_party/python/voluptuous/voluptuous/tests/tests.py b/third_party/python/voluptuous/voluptuous/tests/tests.py new file mode 100644 --- /dev/null +++ b/third_party/python/voluptuous/voluptuous/tests/tests.py @@ -0,0 +1,830 @@ +import copy +import collections +import sys +from nose.tools import assert_equal, assert_raises, assert_true + +from voluptuous import ( + Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, + Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, + Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, + validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, + Contains, Marker) +from voluptuous.humanize import humanize_error +from voluptuous.util import u + + +def test_exact_sequence(): + schema = Schema(ExactSequence([int, int])) + try: + schema([1, 2, 3]) + except Invalid: + assert True + else: + assert False, "Did not raise Invalid" + assert_equal(schema([1, 2]), [1, 2]) + + +def test_required(): + """Verify that Required works.""" + schema = Schema({Required('q'): 1}) + # Can't use nose's raises (because we need to access the raised + # exception, nor assert_raises which fails with Python 2.6.9. + try: + schema({}) + except Invalid as e: + assert_equal(str(e), "required key not provided @ data['q']") + else: + assert False, "Did not raise Invalid" + + +def test_extra_with_required(): + """Verify that Required does not break Extra.""" + schema = Schema({Required('toaster'): str, Extra: object}) + r = schema({'toaster': 'blue', 'another_valid_key': 'another_valid_value'}) + assert_equal( + r, {'toaster': 'blue', 'another_valid_key': 'another_valid_value'}) + + +def test_iterate_candidates(): + """Verify that the order for iterating over mapping candidates is right.""" + schema = { + "toaster": str, + Extra: object, + } + # toaster should be first. + from voluptuous.schema_builder import _iterate_mapping_candidates + assert_equal(_iterate_mapping_candidates(schema)[0][0], 'toaster') + + +def test_in(): + """Verify that In works.""" + schema = Schema({"color": In(frozenset(["blue", "red", "yellow"]))}) + schema({"color": "blue"}) + + +def test_not_in(): + """Verify that NotIn works.""" + schema = Schema({"color": NotIn(frozenset(["blue", "red", "yellow"]))}) + schema({"color": "orange"}) + try: + schema({"color": "blue"}) + except Invalid as e: + assert_equal(str(e), "value is not allowed for dictionary value @ data['color']") + else: + assert False, "Did not raise NotInInvalid" + + +def test_contains(): + """Verify contains validation method.""" + schema = Schema({'color': Contains('red')}) + schema({'color': ['blue', 'red', 'yellow']}) + try: + schema({'color': ['blue', 'yellow']}) + except Invalid as e: + assert_equal(str(e), + "value is not allowed for dictionary value @ data['color']") + + +def test_remove(): + """Verify that Remove works.""" + # remove dict keys + schema = Schema({"weight": int, + Remove("color"): str, + Remove("amount"): int}) + out_ = schema({"weight": 10, "color": "red", "amount": 1}) + assert "color" not in out_ and "amount" not in out_ + + # remove keys by type + schema = Schema({"weight": float, + "amount": int, + # remvove str keys with int values + Remove(str): int, + # keep str keys with str values + str: str}) + out_ = schema({"weight": 73.4, + "condition": "new", + "amount": 5, + "left": 2}) + # amount should stay since it's defined + # other string keys with int values will be removed + assert "amount" in out_ and "left" not in out_ + # string keys with string values will stay + assert "condition" in out_ + + # remove value from list + schema = Schema([Remove(1), int]) + out_ = schema([1, 2, 3, 4, 1, 5, 6, 1, 1, 1]) + assert_equal(out_, [2, 3, 4, 5, 6]) + + # remove values from list by type + schema = Schema([1.0, Remove(float), int]) + out_ = schema([1, 2, 1.0, 2.0, 3.0, 4]) + assert_equal(out_, [1, 2, 1.0, 4]) + + +def test_extra_empty_errors(): + schema = Schema({'a': {Extra: object}}, required=True) + schema({'a': {}}) + + +def test_literal(): + """ test with Literal """ + + schema = Schema([Literal({"a": 1}), Literal({"b": 1})]) + schema([{"a": 1}]) + schema([{"b": 1}]) + schema([{"a": 1}, {"b": 1}]) + + try: + schema([{"c": 1}]) + except Invalid as e: + assert_equal(str(e), "{'c': 1} not match for {'b': 1} @ data[0]") + else: + assert False, "Did not raise Invalid" + + schema = Schema(Literal({"a": 1})) + try: + schema({"b": 1}) + except MultipleInvalid as e: + assert_equal(str(e), "{'b': 1} not match for {'a': 1}") + assert_equal(len(e.errors), 1) + assert_equal(type(e.errors[0]), LiteralInvalid) + else: + assert False, "Did not raise Invalid" + + +def test_email_validation(): + """ test with valid email """ + schema = Schema({"email": Email()}) + out_ = schema({"email": "example@example.com"}) + + assert 'example@example.com"', out_.get("url") + + +def test_email_validation_with_none(): + """ test with invalid None Email""" + schema = Schema({"email": Email()}) + try: + schema({"email": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an Email for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_email_validation_with_empty_string(): + """ test with empty string Email""" + schema = Schema({"email": Email()}) + try: + schema({"email": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an Email for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_email_validation_without_host(): + """ test with empty host name in email """ + schema = Schema({"email": Email()}) + try: + schema({"email": 'a@.com'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an Email for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_fqdn_url_validation(): + """ test with valid fully qualified domain name url """ + schema = Schema({"url": FqdnUrl()}) + out_ = schema({"url": "http://example.com/"}) + + assert 'http://example.com/', out_.get("url") + + +def test_fqdn_url_without_domain_name(): + """ test with invalid fully qualified domain name url """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": "http://localhost/"}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_fqdnurl_validation_with_none(): + """ test with invalid None FQDN url""" + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_fqdnurl_validation_with_empty_string(): + """ test with empty string FQDN URL """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_fqdnurl_validation_without_host(): + """ test with empty host FQDN URL """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": 'http://'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_url_validation(): + """ test with valid URL """ + schema = Schema({"url": Url()}) + out_ = schema({"url": "http://example.com/"}) + + assert 'http://example.com/', out_.get("url") + + +def test_url_validation_with_none(): + """ test with invalid None url""" + schema = Schema({"url": Url()}) + try: + schema({"url": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_url_validation_with_empty_string(): + """ test with empty string URL """ + schema = Schema({"url": Url()}) + try: + schema({"url": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_url_validation_without_host(): + """ test with empty host URL """ + schema = Schema({"url": Url()}) + try: + schema({"url": 'http://'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_copy_dict_undefined(): + """ test with a copied dictionary """ + fields = { + Required("foo"): int + } + copied_fields = copy.deepcopy(fields) + + schema = Schema(copied_fields) + + # This used to raise a `TypeError` because the instance of `Undefined` + # was a copy, so object comparison would not work correctly. + try: + schema({"foo": "bar"}) + except Exception as e: + assert isinstance(e, MultipleInvalid) + + +def test_sorting(): + """ Expect alphabetic sorting """ + foo = Required('foo') + bar = Required('bar') + items = [foo, bar] + expected = [bar, foo] + result = sorted(items) + assert result == expected + + +def test_schema_extend(): + """Verify that Schema.extend copies schema keys from both.""" + + base = Schema({'a': int}, required=True) + extension = {'b': str} + extended = base.extend(extension) + + assert base.schema == {'a': int} + assert extension == {'b': str} + assert extended.schema == {'a': int, 'b': str} + assert extended.required == base.required + assert extended.extra == base.extra + + +def test_schema_extend_overrides(): + """Verify that Schema.extend can override required/extra parameters.""" + + base = Schema({'a': int}, required=True) + extended = base.extend({'b': str}, required=False, extra=ALLOW_EXTRA) + + assert base.required is True + assert base.extra == PREVENT_EXTRA + assert extended.required is False + assert extended.extra == ALLOW_EXTRA + + +def test_schema_extend_key_swap(): + """Verify that Schema.extend can replace keys, even when different markers are used""" + + base = Schema({Optional('a'): int}) + extension = {Required('a'): int} + extended = base.extend(extension) + + assert_equal(len(base.schema), 1) + assert_true(isinstance(list(base.schema)[0], Optional)) + assert_equal(len(extended.schema), 1) + assert_true((list(extended.schema)[0], Required)) + + +def test_subschema_extension(): + """Verify that Schema.extend adds and replaces keys in a subschema""" + + base = Schema({'a': {'b': int, 'c': float}}) + extension = {'d': str, 'a': {'b': str, 'e': int}} + extended = base.extend(extension) + + assert_equal(base.schema, {'a': {'b': int, 'c': float}}) + assert_equal(extension, {'d': str, 'a': {'b': str, 'e': int}}) + assert_equal(extended.schema, {'a': {'b': str, 'c': float, 'e': int}, 'd': str}) + + +def test_repr(): + """Verify that __repr__ returns valid Python expressions""" + match = Match('a pattern', msg='message') + replace = Replace('you', 'I', msg='you and I') + range_ = Range(min=0, max=42, min_included=False, + max_included=False, msg='number not in range') + coerce_ = Coerce(int, msg="moo") + all_ = All('10', Coerce(int), msg='all msg') + maybe_int = Maybe(int) + + assert_equal(repr(match), "Match('a pattern', msg='message')") + assert_equal(repr(replace), "Replace('you', 'I', msg='you and I')") + assert_equal( + repr(range_), + "Range(min=0, max=42, min_included=False, max_included=False, msg='number not in range')" + ) + assert_equal(repr(coerce_), "Coerce(int, msg='moo')") + assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") + assert_equal(repr(maybe_int), "Maybe(%s)" % str(int)) + + +def test_list_validation_messages(): + """ Make sure useful error messages are available """ + + def is_even(value): + if value % 2: + raise Invalid('%i is not even' % value) + return value + + schema = Schema(dict(even_numbers=[All(int, is_even)])) + + try: + schema(dict(even_numbers=[3])) + except Invalid as e: + assert_equal(len(e.errors), 1, e.errors) + assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") + assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + else: + assert False, "Did not raise Invalid" + + +def test_nested_multiple_validation_errors(): + """ Make sure useful error messages are available """ + + def is_even(value): + if value % 2: + raise Invalid('%i is not even' % value) + return value + + schema = Schema(dict(even_numbers=All([All(int, is_even)], + Length(min=1)))) + + try: + schema(dict(even_numbers=[3])) + except Invalid as e: + assert_equal(len(e.errors), 1, e.errors) + assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") + assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + else: + assert False, "Did not raise Invalid" + + +def test_humanize_error(): + data = { + 'a': 'not an int', + 'b': [123] + } + schema = Schema({ + 'a': int, + 'b': [str] + }) + try: + schema(data) + except MultipleInvalid as e: + assert_equal( + humanize_error(data, e), + "expected int for dictionary value @ data['a']. Got 'not an int'\n" + "expected str @ data['b'][0]. Got 123" + ) + else: + assert False, 'Did not raise MultipleInvalid' + + +def test_fix_157(): + s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) + assert_equal(['one'], s(['one'])) + assert_raises(MultipleInvalid, s, ['four']) + + +def test_range_exlcudes_nan(): + s = Schema(Range(min=0, max=10)) + assert_raises(MultipleInvalid, s, float('nan')) + + +def test_equal(): + s = Schema(Equal(1)) + s(1) + assert_raises(Invalid, s, 2) + s = Schema(Equal('foo')) + s('foo') + assert_raises(Invalid, s, 'bar') + s = Schema(Equal([1, 2])) + s([1, 2]) + assert_raises(Invalid, s, []) + assert_raises(Invalid, s, [1, 2, 3]) + # 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]) + + +def test_maybe(): + assert_raises(TypeError, Maybe, lambda x: x) + + s = Schema(Maybe(int)) + assert s(1) == 1 + assert s(None) is None + + assert_raises(Invalid, s, 'foo') + + +def test_empty_list_as_exact(): + s = Schema([]) + assert_raises(Invalid, s, [1]) + 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): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_with_args(): + @validate(int) + def fn(arg): + return arg + + assert_raises(Invalid, fn, 1.0) + + +def test_schema_decorator_match_with_kwargs(): + @validate(arg=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_with_kwargs(): + @validate(arg=int) + def fn(arg): + return arg + + assert_raises(Invalid, fn, 1.0) + + +def test_schema_decorator_match_return_with_args(): + @validate(int, __return__=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_return_with_args(): + @validate(int, __return__=int) + def fn(arg): + return "hello" + + assert_raises(Invalid, fn, 1) + + +def test_schema_decorator_match_return_with_kwargs(): + @validate(arg=int, __return__=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_return_with_kwargs(): + @validate(arg=int, __return__=int) + def fn(arg): + return "hello" + + assert_raises(Invalid, fn, 1) + + +def test_schema_decorator_return_only_match(): + @validate(__return__=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_return_only_unmatch(): + @validate(__return__=int) + def fn(arg): + return "hello" + + assert_raises(Invalid, fn, 1) + + +def test_unicode_as_key(): + if sys.version_info >= (3,): + text_type = str + else: + text_type = unicode + schema = Schema({text_type: int}) + schema({u("foobar"): 1}) + + +def test_number_validation_with_string(): + """ test with Number with string""" + schema = Schema({"number": Number(precision=6, scale=2)}) + try: + schema({"number": 'teststr'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Value must be a number enclosed with string for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_invalid_precision_invalid_scale(): + """ test with Number with invalid precision and scale""" + schema = Schema({"number": Number(precision=6, scale=2)}) + try: + schema({"number": '123456.712'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_true(): + """ test with Number with valid precision and scale""" + schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=True)}) + out_ = schema({"number": '1234.00'}) + assert_equal(float(out_.get("number")), 1234.00) + + +def test_number_when_precision_scale_none_yield_decimal_true(): + """ test with Number with no precision and scale""" + schema = Schema({"number": Number(yield_decimal=True)}) + out_ = schema({"number": '12345678901234'}) + assert_equal(out_.get("number"), 12345678901234) + + +def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): + """ test with Number with no precision and valid scale case 1""" + schema = Schema({"number": Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789.34'}) + assert_equal(float(out_.get("number")), 123456789.34) + + +def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): + """ test with Number with no precision and valid scale case 2 with zero in decimal part""" + schema = Schema({"number": Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789012.00'}) + assert_equal(float(out_.get("number")), 123456789012.00) + + +def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number": Number(scale=2, yield_decimal=True)}) + try: + schema({"number": '12345678901.234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): + """ test with Number with no precision and valid scale""" + schema = Schema({"number": Number(precision=14, yield_decimal=True)}) + out_ = schema({"number": '1234567.8901234'}) + assert_equal(float(out_.get("number")), 1234567.8901234) + + +def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number": Number(precision=14, yield_decimal=True)}) + try: + schema({"number": '12345674.8901234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 14 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_false(): + """ test with Number with valid precision, scale and no yield_decimal""" + schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=False)}) + out_ = schema({"number": '1234.00'}) + assert_equal(out_.get("number"), '1234.00') + + +def test_named_tuples_validate_as_tuples(): + NT = collections.namedtuple('NT', ['a', 'b']) + nt = NT(1, 2) + t = (1, 2) + + Schema((int, int))(nt) + Schema((int, int))(t) + Schema(NT(int, int))(nt) + Schema(NT(int, int))(t) + + +def test_datetime(): + schema = Schema({"datetime": Datetime()}) + schema({"datetime": "2016-10-24T14:01:57.102152Z"}) + assert_raises(MultipleInvalid, schema, {"datetime": "2016-10-24T14:01:57"}) + + +def test_date(): + schema = Schema({"date": Date()}) + schema({"date": "2016-10-24"}) + assert_raises(MultipleInvalid, schema, {"date": "2016-10-2"}) + assert_raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) + + +def test_ordered_dict(): + if not hasattr(collections, 'OrderedDict'): + # collections.OrderedDict was added in Python2.7; only run if present + return + schema = Schema({Number(): Number()}) # x, y pairs (for interpolation or something) + data = collections.OrderedDict([(5.0, 3.7), (24.0, 8.7), (43.0, 1.5), + (62.0, 2.1), (71.5, 6.7), (90.5, 4.1), + (109.0, 3.9)]) + out = schema(data) + assert isinstance(out, collections.OrderedDict), 'Collection is no longer ordered' + assert data.keys() == out.keys(), 'Order is not consistent' + + +def test_marker_hashable(): + """Verify that you can get schema keys, even if markers were used""" + definition = { + Required('x'): int, Optional('y'): float, + Remove('j'): int, Remove(int): str, int: int + } + assert_equal(definition.get('x'), int) + assert_equal(definition.get('y'), float) + assert_true(Required('x') == Required('x')) + assert_true(Required('x') != Required('y')) + # Remove markers are not hashable + assert_equal(definition.get('j'), None) + + +def test_validation_performance(): + """ + This test comes to make sure the validation complexity of dictionaries is done in a linear time. + to achieve this a custom marker is used in the scheme that counts each time it is evaluated. + By doing so we can determine if the validation is done in linear complexity. + Prior to issue https://github.com/alecthomas/voluptuous/issues/259 this was exponential + """ + num_of_keys = 1000 + + schema_dict = {} + data = {} + data_extra_keys = {} + + counter = [0] + + class CounterMarker(Marker): + def __call__(self, *args, **kwargs): + counter[0] += 1 + return super(CounterMarker, self).__call__(*args, **kwargs) + + for i in range(num_of_keys): + schema_dict[CounterMarker(str(i))] = str + data[str(i)] = str(i) + data_extra_keys[str(i*2)] = str(i) # half of the keys are present, and half aren't + + schema = Schema(schema_dict, extra=ALLOW_EXTRA) + + schema(data) + + assert counter[0] <= num_of_keys, "Validation complexity is not linear! %s > %s" % (counter[0], num_of_keys) + + counter[0] = 0 # reset counter + schema(data_extra_keys) + + assert counter[0] <= num_of_keys, "Validation complexity is not linear! %s > %s" % (counter[0], num_of_keys)