mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-07 18:04:46 +00:00
Bug 1281004: vendor voluptuous; r=gps
MozReview-Commit-ID: Hzz7EFf4coX --HG-- extra : rebase_source : 08bd1896b08596b30a5fe8d735add194fa724fea
This commit is contained in:
parent
a317245052
commit
e8b852658b
@ -51,6 +51,7 @@ SEARCH_PATHS = [
|
||||
'python/pyyaml/lib',
|
||||
'python/requests',
|
||||
'python/slugid',
|
||||
'python/voluptuous',
|
||||
'build',
|
||||
'build/pymake',
|
||||
'config',
|
||||
|
25
python/voluptuous/COPYING
Normal file
25
python/voluptuous/COPYING
Normal file
@ -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.
|
2
python/voluptuous/MANIFEST.in
Normal file
2
python/voluptuous/MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
include *.md
|
||||
include COPYING
|
611
python/voluptuous/PKG-INFO
Normal file
611
python/voluptuous/PKG-INFO
Normal file
@ -0,0 +1,611 @@
|
||||
Metadata-Version: 1.1
|
||||
Name: voluptuous
|
||||
Version: 0.8.11
|
||||
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| |Stories in Ready|
|
||||
|
||||
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.
|
||||
|
||||
Show me an example
|
||||
------------------
|
||||
|
||||
Twitter's `user search
|
||||
API <https://dev.twitter.com/docs/api/1/get/users/search>`__ accepts
|
||||
query URLs like:
|
||||
|
||||
::
|
||||
|
||||
$ curl 'http://api.twitter.com/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']
|
||||
|
||||
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'}}
|
||||
|
||||
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 '<Structure(q={0.q!r})>'.format(self)
|
||||
...
|
||||
>>> schema = Schema(Object({'q': 'one'}, cls=Structure))
|
||||
>>> schema(Structure(q='one'))
|
||||
<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 <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.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/alecthomas/voluptuous.png
|
||||
:target: https://travis-ci.org/alecthomas/voluptuous
|
||||
.. |Stories in Ready| image:: https://badge.waffle.io/alecthomas/voluptuous.png?label=ready&title=Ready
|
||||
:target: https://waffle.io/alecthomas/voluptuous
|
||||
|
||||
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
|
596
python/voluptuous/README.md
Normal file
596
python/voluptuous/README.md
Normal file
@ -0,0 +1,596 @@
|
||||
# Voluptuous is a Python data validation library
|
||||
|
||||
[![Build Status](https://travis-ci.org/alecthomas/voluptuous.png)](https://travis-ci.org/alecthomas/voluptuous) [![Stories in Ready](https://badge.waffle.io/alecthomas/voluptuous.png?label=ready&title=Ready)](https://waffle.io/alecthomas/voluptuous)
|
||||
|
||||
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
|
||||
[<voluptuous@librelist.com>](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.
|
||||
|
||||
## Show me an example
|
||||
|
||||
Twitter's [user search API](https://dev.twitter.com/docs/api/1/get/users/search) accepts
|
||||
query URLs like:
|
||||
|
||||
```
|
||||
$ curl 'http://api.twitter.com/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']
|
||||
|
||||
```
|
||||
|
||||
### 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'}}
|
||||
|
||||
```
|
||||
|
||||
#### 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 '<Structure(q={0.q!r})>'.format(self)
|
||||
...
|
||||
>>> schema = Schema(Object({'q': 'one'}, cls=Structure))
|
||||
>>> schema(Structure(q='one'))
|
||||
<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.
|
589
python/voluptuous/README.rst
Normal file
589
python/voluptuous/README.rst
Normal file
@ -0,0 +1,589 @@
|
||||
Voluptuous is a Python data validation library
|
||||
==============================================
|
||||
|
||||
|Build Status| |Stories in Ready|
|
||||
|
||||
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.
|
||||
|
||||
Show me an example
|
||||
------------------
|
||||
|
||||
Twitter's `user search
|
||||
API <https://dev.twitter.com/docs/api/1/get/users/search>`__ accepts
|
||||
query URLs like:
|
||||
|
||||
::
|
||||
|
||||
$ curl 'http://api.twitter.com/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']
|
||||
|
||||
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'}}
|
||||
|
||||
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 '<Structure(q={0.q!r})>'.format(self)
|
||||
...
|
||||
>>> schema = Schema(Object({'q': 'one'}, cls=Structure))
|
||||
>>> schema(Structure(q='one'))
|
||||
<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 <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.
|
||||
|
||||
.. |Build Status| image:: https://travis-ci.org/alecthomas/voluptuous.png
|
||||
:target: https://travis-ci.org/alecthomas/voluptuous
|
||||
.. |Stories in Ready| image:: https://badge.waffle.io/alecthomas/voluptuous.png?label=ready&title=Ready
|
||||
:target: https://waffle.io/alecthomas/voluptuous
|
10
python/voluptuous/setup.cfg
Normal file
10
python/voluptuous/setup.cfg
Normal file
@ -0,0 +1,10 @@
|
||||
[nosetests]
|
||||
doctest-extension = md
|
||||
with-doctest = 1
|
||||
where = .
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
tag_svn_revision = 0
|
||||
|
54
python/voluptuous/setup.py
Normal file
54
python/voluptuous/setup.py
Normal file
@ -0,0 +1,54 @@
|
||||
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'],
|
||||
py_modules=['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',
|
||||
],
|
||||
install_requires=[
|
||||
'setuptools >= 0.6b1',
|
||||
],
|
||||
)
|
268
python/voluptuous/tests.md
Normal file
268
python/voluptuous/tests.md
Normal file
@ -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 <class 'Structure'>
|
||||
>>> 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'
|
1954
python/voluptuous/voluptuous.py
Normal file
1954
python/voluptuous/voluptuous.py
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user