Import Upstream version 0.13.1
This commit is contained in:
commit
276de1bd05
|
@ -0,0 +1,2 @@
|
|||
[run]
|
||||
omit = *tests*
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
name: Contributions only
|
||||
about: Contributions only
|
||||
title: "(Contributions only)"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
# CONTRIBUTIONS ONLY
|
||||
|
||||
**What does this mean?** I do not have time to fix issues myself. The only way fixes or new features will be added is by people submitting PRs.
|
||||
|
||||
**Current status.** Voluptuous is largely feature stable. There hasn't been a need to add new features in a while, but there are some bugs that should be fixed.
|
||||
|
||||
**Why?** I no longer use Voluptuous personally (in fact I no longer regularly write Python code). Rather than leave the project in a limbo of people filing issues and wondering why they're note being worked on, I believe this notice will more clearly set expectations.
|
|
@ -0,0 +1,42 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Tox ${{ matrix.session }} session on Python ${{ matrix.python-version }}
|
||||
runs-on: "ubuntu-latest"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { python-version: "3.10", session: "flake8" }
|
||||
- { python-version: "3.10", session: "py310" }
|
||||
- { python-version: "3.9", session: "py39" }
|
||||
- { python-version: "3.8", session: "py38" }
|
||||
- { python-version: "3.7", session: "py37" }
|
||||
- { python-version: "3.6", session: "py36" }
|
||||
- { python-version: "2.7", session: "py27" }
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install tox-setuptools-version
|
||||
if: ${{ matrix.session != 'py27' }}
|
||||
run: |
|
||||
pip install tox-setuptools-version
|
||||
|
||||
- name: Run tox
|
||||
run: |
|
||||
pip install tox
|
||||
tox -e ${{ matrix.session }}
|
|
@ -0,0 +1,21 @@
|
|||
*.gem
|
||||
*.swp
|
||||
*.pyc
|
||||
*#*
|
||||
build
|
||||
dist
|
||||
.svn/*
|
||||
.DS_Store
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info
|
||||
.coverage
|
||||
.tox
|
||||
MANIFEST
|
||||
.idea
|
||||
venv
|
||||
docs/_build
|
||||
.vscode/
|
||||
.venv
|
||||
.pytest_cache
|
||||
htmlcov
|
|
@ -0,0 +1,20 @@
|
|||
language: python
|
||||
sudo: true
|
||||
dist: xenial
|
||||
python:
|
||||
- '2.7'
|
||||
- '3.6'
|
||||
- '3.7'
|
||||
- '3.8'
|
||||
- '3.9'
|
||||
install:
|
||||
- pip install coveralls
|
||||
- pip install coverage
|
||||
script: nosetests --with-coverage --cover-package=voluptuous
|
||||
after_success:
|
||||
- coveralls
|
||||
#- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh $USERNAME $PASSWORD; fi # Fix this later
|
||||
env:
|
||||
global:
|
||||
- secure: UKVFCaFRRECYeNaLJr4POqt6zENBjyUe79U/5b9pEGBFWzXWoJ+EElOFOJdkquL6u3AwL6Bw93GqRIYHKcRW70doCYiEI7p2CuXey2mjoC7bLKdk4Fcrj0MTbiS6WJxEDfcsP/Tj3tv4kPqA4nYYm9DQoNfUX3skns442h0zals=
|
||||
- secure: EK2dbVB4T7qNFWCSu3tL+l2YnpcrCvPk9E3W05rGZnkT38Do21kVDncf8XRh/5Nn4J6zGmdoHw6NFqeQtF6/+3GNIqEW4PzA5x5pUx1rI6drB0hTaEURG3VYUmLOoQ/thziaEmnez8Qt1hUtn/0Jhl6eUYOtmSTSkDeLz7zehm0=
|
|
@ -0,0 +1,204 @@
|
|||
# Changelog
|
||||
|
||||
## [0.13.1]
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- [#439](https://github.com/alecthomas/voluptuous/pull/454): Ignore `Enum` if it is unavailable
|
||||
- [#456](https://github.com/alecthomas/voluptuous/pull/456): Fix email regex match for Python 2.7
|
||||
|
||||
**New**:
|
||||
|
||||
- [#457](https://github.com/alecthomas/voluptuous/pull/457): Enable github actions
|
||||
- [#462](https://github.com/alecthomas/voluptuous/pull/462): Convert codebase to adhere to `flake8` W504 (PEP 8)
|
||||
- [#459](https://github.com/alecthomas/voluptuous/pull/459): Enable `flake8` in github actions
|
||||
- [#464](https://github.com/alecthomas/voluptuous/pull/464): `pytest` migration + enable Python 3.10
|
||||
|
||||
## [0.13.0]
|
||||
|
||||
**Changes**:
|
||||
|
||||
- [#450](https://github.com/alecthomas/voluptuous/pull/450): Display valid `Enum` values in `Coerce`
|
||||
|
||||
## [0.12.2]
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- [#439](https://github.com/alecthomas/voluptuous/issues/439): Revert Breaking `Maybe` change in 0.12.1
|
||||
- [#447](https://github.com/alecthomas/voluptuous/issues/447): Fix Email Regex to not match on extra characters
|
||||
|
||||
## [0.12.1]
|
||||
|
||||
**Changes**:
|
||||
- [#435](https://github.com/alecthomas/voluptuous/pull/435): Extended a few tests (`Required` and `In`)
|
||||
- [#425](https://github.com/alecthomas/voluptuous/pull/425): Improve error message for `In` and `NotIn`
|
||||
- [#436](https://github.com/alecthomas/voluptuous/pull/436): Add sorted() for `In` and `NotIn` + fix tests
|
||||
- [#437](https://github.com/alecthomas/voluptuous/pull/437): Grouped `Maybe` tests plus added another `Range` test
|
||||
- [#438](https://github.com/alecthomas/voluptuous/pull/438): Extend tests for `Schema` with empty list or dict
|
||||
|
||||
**New**:
|
||||
- [#433](https://github.com/alecthomas/voluptuous/pull/433): Add Python 3.9 support
|
||||
|
||||
**Fixes**:
|
||||
- [#431](https://github.com/alecthomas/voluptuous/pull/431): Fixed typos + made spelling more consistent
|
||||
- [#411](https://github.com/alecthomas/voluptuous/pull/411): Ensure `Maybe` propagates error information
|
||||
- [#434](https://github.com/alecthomas/voluptuous/pull/434): Remove value enumeration when validating empty list
|
||||
|
||||
## [0.12.0]
|
||||
|
||||
**Changes**:
|
||||
- n/a
|
||||
|
||||
**New**:
|
||||
- [#368](https://github.com/alecthomas/voluptuous/pull/368): Allow a discriminant field in validators
|
||||
|
||||
**Fixes**:
|
||||
- [#420](https://github.com/alecthomas/voluptuous/pull/420): Fixed issue with 'required' not being set properly and added test
|
||||
- [#414](https://github.com/alecthomas/voluptuous/pull/414): Handle incomparable values in Range
|
||||
- [#427](https://github.com/alecthomas/voluptuous/pull/427): Added additional tests for Range, Clamp and Length + catch TypeError exceptions
|
||||
|
||||
## [0.11.7]
|
||||
|
||||
**Changes**:
|
||||
|
||||
- [#378](https://github.com/alecthomas/voluptuous/pull/378): Allow `extend()` of a `Schema` to return a subclass of a `Schema` as well.
|
||||
|
||||
**New**:
|
||||
|
||||
- [#364](https://github.com/alecthomas/voluptuous/pull/364): Accept `description` for `Inclusive` instances.
|
||||
- [#373](https://github.com/alecthomas/voluptuous/pull/373): Accept `msg` for `Maybe` instances.
|
||||
- [#382](https://github.com/alecthomas/voluptuous/pull/382): Added support for default values in `Inclusive` instances.
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- [#371](https://github.com/alecthomas/voluptuous/pull/371): Fixed `DeprecationWarning` related to `collections.Mapping`.
|
||||
- [#377](https://github.com/alecthomas/voluptuous/pull/377): Preserve Unicode strings when passed to utility functions (e.g., `Lower()`, `Upper()`).
|
||||
- [#380](https://github.com/alecthomas/voluptuous/pull/380): Fixed regression with `Any` and `required` flag.
|
||||
|
||||
## [0.11.5]
|
||||
|
||||
- Fixed issue with opening README file in `setup.py`.
|
||||
|
||||
## [0.11.4]
|
||||
|
||||
- Removed use of `pypandoc` as Markdown is now supported by `setup()`.
|
||||
|
||||
## [0.11.3] and [0.11.2]
|
||||
|
||||
**Changes**:
|
||||
|
||||
- [#349](https://github.com/alecthomas/voluptuous/pull/349): Support Python 3.7.
|
||||
- [#343](https://github.com/alecthomas/voluptuous/pull/343): Drop support for Python 3.3.
|
||||
|
||||
**New**:
|
||||
|
||||
- [#342](https://github.com/alecthomas/voluptuous/pull/342): Add support for sets and frozensets.
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- [#332](https://github.com/alecthomas/voluptuous/pull/332): Fix Python 3.x compatibility for setup.py when `pypandoc` is installed.
|
||||
- [#348](https://github.com/alecthomas/voluptuous/pull/348): Include path in `AnyInvalid` errors.
|
||||
- [#351](https://github.com/alecthomas/voluptuous/pull/351): Fix `Date` behaviour when a custom format is specified.
|
||||
|
||||
## [0.11.1] and [0.11.0]
|
||||
|
||||
**Changes**:
|
||||
|
||||
- [#293](https://github.com/alecthomas/voluptuous/pull/293): Support Python 3.6.
|
||||
- [#294](https://github.com/alecthomas/voluptuous/pull/294): Drop support for Python 2.6, 3.1 and 3.2.
|
||||
- [#318](https://github.com/alecthomas/voluptuous/pull/318): Allow to use nested schema and allow any validator to be compiled.
|
||||
- [#324](https://github.com/alecthomas/voluptuous/pull/324):
|
||||
Default values MUST now pass validation just as any regular value. This is a backward incompatible change if a schema uses default values that don't pass validation against the specified schema.
|
||||
- [#328](https://github.com/alecthomas/voluptuous/pull/328):
|
||||
Modify `__lt__` in Marker class to allow comparison with non Marker objects, such as str and int.
|
||||
|
||||
**New**:
|
||||
|
||||
- [#307](https://github.com/alecthomas/voluptuous/pull/307): Add description field to `Marker` instances.
|
||||
- [#311](https://github.com/alecthomas/voluptuous/pull/311): Add `Schema.infer` method for basic schema inference.
|
||||
- [#314](https://github.com/alecthomas/voluptuous/pull/314): Add `SomeOf` validator.
|
||||
|
||||
**Fixes**:
|
||||
|
||||
- [#279](https://github.com/alecthomas/voluptuous/pull/279):
|
||||
Treat Python 2 old-style classes like types when validating.
|
||||
- [#280](https://github.com/alecthomas/voluptuous/pull/280): Make
|
||||
`IsDir()`, `IsFile()` and `PathExists()` consistent between different Python versions.
|
||||
- [#290](https://github.com/alecthomas/voluptuous/pull/290): Use absolute imports to avoid import conflicts.
|
||||
- [#291](https://github.com/alecthomas/voluptuous/pull/291): Fix `Coerce` validator to catch `decimal.InvalidOperation`.
|
||||
- [#298](https://github.com/alecthomas/voluptuous/pull/298): Make `Schema([])` usage consistent with `Schema({})`.
|
||||
- [#303](https://github.com/alecthomas/voluptuous/pull/303): Allow partial validation when using validate decorator.
|
||||
- [#316](https://github.com/alecthomas/voluptuous/pull/316): Make `Schema.__eq__` deterministic.
|
||||
- [#319](https://github.com/alecthomas/voluptuous/pull/319): Replace implementation of `Maybe(s)` with `Any(None, s)` to allow it to be compiled.
|
||||
|
||||
## [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.
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
|||
include *.md
|
||||
include COPYING
|
||||
include voluptuous/tests/*.py
|
||||
include voluptuous/tests/*.md
|
|
@ -0,0 +1,708 @@
|
|||
|
||||
# CONTRIBUTIONS ONLY
|
||||
|
||||
**What does this mean?** I do not have time to fix issues myself. The only way fixes or new features will be added is by people submitting PRs.
|
||||
|
||||
**Current status:** Voluptuous is largely feature stable. There hasn't been a need to add new features in a while, but there are some bugs that should be fixed.
|
||||
|
||||
**Why?** I no longer use Voluptuous personally (in fact I no longer regularly write Python code). Rather than leave the project in a limbo of people filing issues and wondering why they're not being worked on, I believe this notice will more clearly set expectations.
|
||||
|
||||
# Voluptuous is a Python data validation library
|
||||
|
||||
[![image](https://img.shields.io/pypi/v/voluptuous.svg)](https://python.org/pypi/voluptuous)
|
||||
[![image](https://img.shields.io/pypi/l/voluptuous.svg)](https://python.org/pypi/voluptuous)
|
||||
[![image](https://img.shields.io/pypi/pyversions/voluptuous.svg)](https://python.org/pypi/voluptuous)
|
||||
[![Build Status](https://travis-ci.org/alecthomas/voluptuous.svg)](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.svg)](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
|
||||
[<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.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is provided [here](http://alecthomas.github.io/voluptuous/).
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](https://github.com/alecthomas/voluptuous/blob/master/CHANGELOG.md).
|
||||
|
||||
## 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.
|
||||
|
||||
## Show me an example
|
||||
|
||||
Twitter's [user search API](https://dev.twitter.com/rest/reference/get/users/search) accepts
|
||||
query URLs like:
|
||||
|
||||
```bash
|
||||
$ curl 'https://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
|
||||
```
|
||||
|
||||
### URLs
|
||||
|
||||
URLs 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 @ data[1]"
|
||||
True
|
||||
>>> schema([])
|
||||
[]
|
||||
>>> schema = Schema(list)
|
||||
>>> schema([])
|
||||
[]
|
||||
>>> schema([1, 2])
|
||||
[1, 2]
|
||||
```
|
||||
|
||||
### Sets and frozensets
|
||||
|
||||
Sets and frozensets are treated as a set of valid values. Each element
|
||||
in the schema set is compared to each value in the input data:
|
||||
|
||||
```pycon
|
||||
>>> schema = Schema({42})
|
||||
>>> schema({42}) == {42}
|
||||
True
|
||||
>>> try:
|
||||
... schema({43})
|
||||
... raise AssertionError('MultipleInvalid not raised')
|
||||
... except MultipleInvalid as e:
|
||||
... exc = e
|
||||
>>> str(exc) == "invalid value in set"
|
||||
True
|
||||
>>> schema = Schema({int})
|
||||
>>> schema({1, 2, 3}) == {1, 2, 3}
|
||||
True
|
||||
>>> schema = Schema({int, str})
|
||||
>>> schema({1, 2, 'abc'}) == {1, 2, 'abc'}
|
||||
True
|
||||
>>> schema = Schema(frozenset([int]))
|
||||
>>> try:
|
||||
... schema({3})
|
||||
... raise AssertionError('Invalid not raised')
|
||||
... except Invalid as e:
|
||||
... exc = e
|
||||
>>> str(exc) == 'expected a frozenset'
|
||||
True
|
||||
```
|
||||
|
||||
However, an empty set (`set()`) is treated as is. If you want to specify a set
|
||||
that can contain anything, specify it as `set`:
|
||||
|
||||
```pycon
|
||||
>>> schema = Schema(set())
|
||||
>>> try:
|
||||
... schema({1})
|
||||
... raise AssertionError('MultipleInvalid not raised')
|
||||
... except MultipleInvalid as e:
|
||||
... exc = e
|
||||
>>> str(exc) == "invalid value in set"
|
||||
True
|
||||
>>> schema(set()) == set()
|
||||
True
|
||||
>>> schema = Schema(set)
|
||||
>>> schema({1, 2}) == {1, 2}
|
||||
True
|
||||
```
|
||||
|
||||
### Validation functions
|
||||
|
||||
Validators are simple callables that raise an `Invalid` exception when
|
||||
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 / nested schema
|
||||
|
||||
You can use `voluptuous.Self` to define a nested schema:
|
||||
|
||||
```pycon
|
||||
>>> from voluptuous import Schema, Self
|
||||
>>> recursive = Schema({"more": Self, "value": int})
|
||||
>>> recursive({"more": {"value": 42}, "value": 41}) == {'more': {'value': 42}, 'value': 41}
|
||||
True
|
||||
```
|
||||
|
||||
### 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]
|
||||
```
|
||||
|
||||
## Multi-field validation
|
||||
|
||||
Validation rules that involve multiple fields can be implemented as
|
||||
custom validators. It's recommended to use `All()` to do a two-pass
|
||||
validation - the first pass checking the basic structure of the data,
|
||||
and only after that, the second pass applying your cross-field
|
||||
validator:
|
||||
|
||||
```python
|
||||
def passwords_must_match(passwords):
|
||||
if passwords['password'] != passwords['password_again']:
|
||||
raise Invalid('passwords must match')
|
||||
return passwords
|
||||
|
||||
schema = Schema(All(
|
||||
# First "pass" for field types
|
||||
{'password': str, 'password_again': str},
|
||||
# Follow up the first "pass" with your multi-field rules
|
||||
passwords_must_match
|
||||
))
|
||||
|
||||
# valid
|
||||
schema({'password': '123', 'password_again': '123'})
|
||||
|
||||
# raises MultipleInvalid: passwords must match
|
||||
schema({'password': '123', 'password_again': 'and now for something completely different'})
|
||||
|
||||
```
|
||||
|
||||
With this structure, your multi-field validator will run with
|
||||
pre-validated data from the first "pass" and so will not have to do
|
||||
its own type checking on its inputs.
|
||||
|
||||
The flipside is that if the first "pass" of validation fails, your
|
||||
cross-field validator will not run:
|
||||
|
||||
```python
|
||||
# raises Invalid because password_again is not a string
|
||||
# passwords_must_match() will not run because first-pass validation already failed
|
||||
schema({'password': '123', 'password_again': 1337})
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
Voluptuous is using `pytest`:
|
||||
|
||||
```bash
|
||||
$ pip install pytest
|
||||
$ pytest
|
||||
```
|
||||
|
||||
To also include a coverage report:
|
||||
|
||||
```bash
|
||||
$ pip install pytest pytest-cov coverage>=3.0
|
||||
$ pytest --cov=voluptuous voluptuous/tests/
|
||||
```
|
||||
|
||||
## 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).
|
||||
|
||||
[pytest-voluptuous](https://github.com/F-Secure/pytest-voluptuous) is a
|
||||
[pytest](https://github.com/pytest-dev/pytest) plugin that helps in
|
||||
using voluptuous validators in `assert`s.
|
||||
|
||||
I greatly prefer the light-weight style promoted by these libraries to
|
||||
the complexity of libraries like FormEncode.
|
|
@ -0,0 +1,4 @@
|
|||
[pytest]
|
||||
python_files = tests.py
|
||||
testpaths = voluptuous/tests
|
||||
addopts = --doctest-glob=*.md -v
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env python
|
||||
from setuptools import setup
|
||||
|
||||
import sys
|
||||
import io
|
||||
sys.path.insert(0, '.')
|
||||
version = __import__('voluptuous').__version__
|
||||
|
||||
|
||||
with io.open('README.md', encoding='utf-8') 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,
|
||||
long_description_content_type='text/markdown',
|
||||
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.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
]
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
[tox]
|
||||
envlist = flake8,py27,py36,py37,py38,py39,py310
|
||||
|
||||
[flake8]
|
||||
; E501: line too long (X > 79 characters)
|
||||
; W503 line break before binary operator
|
||||
ignore = E501,W503
|
||||
exclude = .tox,.venv,build,*.egg
|
||||
|
||||
[testenv]
|
||||
distribute = True
|
||||
sitepackages = False
|
||||
deps =
|
||||
pytest
|
||||
pytest-cov
|
||||
coverage>=3.0
|
||||
commands =
|
||||
pytest \
|
||||
--cov=voluptuous \
|
||||
voluptuous/tests/
|
||||
|
||||
[testenv:flake8]
|
||||
deps = flake8
|
||||
commands = flake8 --doctests setup.py voluptuous
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Merge pushes to development branch to stable branch
|
||||
if [ ! -n $2 ] ; then
|
||||
echo "Usage: merge.sh <username> <password>"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
GIT_USER="$1"
|
||||
GIT_PASS="$2"
|
||||
|
||||
# Specify the development branch and stable branch names
|
||||
FROM_BRANCH="master"
|
||||
TO_BRANCH="gh-pages"
|
||||
|
||||
# Needed for setting identity
|
||||
git config --global user.email "tusharmakkar08@gmail.com"
|
||||
git config --global user.name "Tushar Makkar"
|
||||
git config --global push.default "simple"
|
||||
|
||||
# Get the current branch
|
||||
export PAGER=cat
|
||||
CURRENT_BRANCH=$(git log -n 1 --pretty=%d HEAD | cut -d"," -f3 | cut -d" " -f2 | cut -d")" -f1)
|
||||
echo "current branch is '$CURRENT_BRANCH'"
|
||||
|
||||
# Create the URL to push merge to
|
||||
URL=$(git remote -v | head -n1 | cut -f2 | cut -d" " -f1)
|
||||
echo "Repo url is $URL"
|
||||
PUSH_URL="https://$GIT_USER:$GIT_PASS@${URL:8}"
|
||||
|
||||
git remote set-url origin ${PUSH_URL}
|
||||
|
||||
echo "Checking out $FROM_BRANCH..." && \
|
||||
git fetch origin ${FROM_BRANCH}:${FROM_BRANCH} && \
|
||||
git checkout ${FROM_BRANCH}
|
||||
|
||||
|
||||
echo "Checking out $TO_BRANCH..." && \
|
||||
# Checkout the latest stable
|
||||
git fetch origin ${TO_BRANCH}:${TO_BRANCH} && \
|
||||
git checkout ${TO_BRANCH} && \
|
||||
|
||||
# Merge the dev into latest stable
|
||||
echo "Merging changes..." && \
|
||||
git merge ${FROM_BRANCH} && \
|
||||
|
||||
# Push changes back to remote vcs
|
||||
echo "Pushing changes..." && \
|
||||
git push origin gh-pages &> /dev/null && \
|
||||
echo "Merge complete!" || \
|
||||
echo "Error Occurred. Merge failed"
|
||||
|
||||
export PYTHONPATH=${PYTHONPATH}:$(pwd):$(pwd)/voluptuous
|
||||
|
||||
pip install -r requirements.txt && sphinx-apidoc -o docs -f voluptuous &&
|
||||
cd docs && make html ||
|
||||
echo "Sphinx not able to generate HTML"
|
||||
|
||||
git status && git add . &&
|
||||
git commit -m "Auto updating documentation from $CURRENT_BRANCH" &&
|
||||
git push origin gh-pages &> /dev/null && echo "Documentation pushed"
|
|
@ -0,0 +1,9 @@
|
|||
# flake8: noqa
|
||||
|
||||
from voluptuous.schema_builder import *
|
||||
from voluptuous.validators import *
|
||||
from voluptuous.util import *
|
||||
from voluptuous.error import *
|
||||
|
||||
__version__ = '0.13.1'
|
||||
__author__ = 'alecthomas'
|
|
@ -0,0 +1,199 @@
|
|||
|
||||
class Error(Exception):
|
||||
"""Base validation exception."""
|
||||
|
||||
|
||||
class SchemaError(Error):
|
||||
"""An error was encountered in the schema."""
|
||||
|
||||
|
||||
class Invalid(Error):
|
||||
"""The data was invalid.
|
||||
|
||||
:attr msg: The error message.
|
||||
:attr path: The path to the error, as a list of keys in the source data.
|
||||
:attr error_message: The actual error message that was raised, as a
|
||||
string.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, message, path=None, error_message=None, error_type=None):
|
||||
Error.__init__(self, message)
|
||||
self.path = path or []
|
||||
self.error_message = error_message or message
|
||||
self.error_type = error_type
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
return self.args[0]
|
||||
|
||||
def __str__(self):
|
||||
path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \
|
||||
if self.path else ''
|
||||
output = Exception.__str__(self)
|
||||
if self.error_type:
|
||||
output += ' for ' + self.error_type
|
||||
return output + path
|
||||
|
||||
def prepend(self, path):
|
||||
self.path = path + self.path
|
||||
|
||||
|
||||
class MultipleInvalid(Invalid):
|
||||
def __init__(self, errors=None):
|
||||
self.errors = errors[:] if errors else []
|
||||
|
||||
def __repr__(self):
|
||||
return 'MultipleInvalid(%r)' % self.errors
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
return self.errors[0].msg
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
return self.errors[0].path
|
||||
|
||||
@property
|
||||
def error_message(self):
|
||||
return self.errors[0].error_message
|
||||
|
||||
def add(self, error):
|
||||
self.errors.append(error)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.errors[0])
|
||||
|
||||
def prepend(self, path):
|
||||
for error in self.errors:
|
||||
error.prepend(path)
|
||||
|
||||
|
||||
class RequiredFieldInvalid(Invalid):
|
||||
"""Required field was missing."""
|
||||
|
||||
|
||||
class ObjectInvalid(Invalid):
|
||||
"""The value we found was not an object."""
|
||||
|
||||
|
||||
class DictInvalid(Invalid):
|
||||
"""The value found was not a dict."""
|
||||
|
||||
|
||||
class ExclusiveInvalid(Invalid):
|
||||
"""More than one value found in exclusion group."""
|
||||
|
||||
|
||||
class InclusiveInvalid(Invalid):
|
||||
"""Not all values found in inclusion group."""
|
||||
|
||||
|
||||
class SequenceTypeInvalid(Invalid):
|
||||
"""The type found is not a sequence type."""
|
||||
|
||||
|
||||
class TypeInvalid(Invalid):
|
||||
"""The value was not of required type."""
|
||||
|
||||
|
||||
class ValueInvalid(Invalid):
|
||||
"""The value was found invalid by evaluation function."""
|
||||
|
||||
|
||||
class ContainsInvalid(Invalid):
|
||||
"""List does not contain item"""
|
||||
|
||||
|
||||
class ScalarInvalid(Invalid):
|
||||
"""Scalars did not match."""
|
||||
|
||||
|
||||
class CoerceInvalid(Invalid):
|
||||
"""Impossible to coerce value to type."""
|
||||
|
||||
|
||||
class AnyInvalid(Invalid):
|
||||
"""The value did not pass any validator."""
|
||||
|
||||
|
||||
class AllInvalid(Invalid):
|
||||
"""The value did not pass all validators."""
|
||||
|
||||
|
||||
class MatchInvalid(Invalid):
|
||||
"""The value does not match the given regular expression."""
|
||||
|
||||
|
||||
class RangeInvalid(Invalid):
|
||||
"""The value is not in given range."""
|
||||
|
||||
|
||||
class TrueInvalid(Invalid):
|
||||
"""The value is not True."""
|
||||
|
||||
|
||||
class FalseInvalid(Invalid):
|
||||
"""The value is not False."""
|
||||
|
||||
|
||||
class BooleanInvalid(Invalid):
|
||||
"""The value is not a boolean."""
|
||||
|
||||
|
||||
class UrlInvalid(Invalid):
|
||||
"""The value is not a URL."""
|
||||
|
||||
|
||||
class EmailInvalid(Invalid):
|
||||
"""The value is not an email address."""
|
||||
|
||||
|
||||
class FileInvalid(Invalid):
|
||||
"""The value is not a file."""
|
||||
|
||||
|
||||
class DirInvalid(Invalid):
|
||||
"""The value is not a directory."""
|
||||
|
||||
|
||||
class PathInvalid(Invalid):
|
||||
"""The value is not a path."""
|
||||
|
||||
|
||||
class LiteralInvalid(Invalid):
|
||||
"""The literal values do not match."""
|
||||
|
||||
|
||||
class LengthInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class DatetimeInvalid(Invalid):
|
||||
"""The value is not a formatted datetime string."""
|
||||
|
||||
|
||||
class DateInvalid(Invalid):
|
||||
"""The value is not a formatted date string."""
|
||||
|
||||
|
||||
class InInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class NotInInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class ExactSequenceInvalid(Invalid):
|
||||
pass
|
||||
|
||||
|
||||
class NotEnoughValid(Invalid):
|
||||
"""The value did not pass enough validations."""
|
||||
pass
|
||||
|
||||
|
||||
class TooManyValid(Invalid):
|
||||
"""The value passed more than expected validations."""
|
||||
pass
|
|
@ -0,0 +1,40 @@
|
|||
from voluptuous import Invalid, MultipleInvalid
|
||||
from voluptuous.error import Error
|
||||
|
||||
|
||||
MAX_VALIDATION_ERROR_ITEM_LENGTH = 500
|
||||
|
||||
|
||||
def _nested_getitem(data, path):
|
||||
for item_index in path:
|
||||
try:
|
||||
data = data[item_index]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
# The index is not present in the dictionary, list or other
|
||||
# indexable or data is not subscriptable
|
||||
return None
|
||||
return data
|
||||
|
||||
|
||||
def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH):
|
||||
""" Provide a more helpful + complete validation error message than that provided automatically
|
||||
Invalid and MultipleInvalid do not include the offending value in error messages,
|
||||
and MultipleInvalid.__str__ only provides the first error.
|
||||
"""
|
||||
if isinstance(validation_error, MultipleInvalid):
|
||||
return '\n'.join(sorted(
|
||||
humanize_error(data, sub_error, max_sub_error_length)
|
||||
for sub_error in validation_error.errors
|
||||
))
|
||||
else:
|
||||
offending_item_summary = repr(_nested_getitem(data, validation_error.path))
|
||||
if len(offending_item_summary) > max_sub_error_length:
|
||||
offending_item_summary = offending_item_summary[:max_sub_error_length - 3] + '...'
|
||||
return '%s. Got %s' % (validation_error, offending_item_summary)
|
||||
|
||||
|
||||
def validate_with_humanized_errors(data, schema, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH):
|
||||
try:
|
||||
return schema(data)
|
||||
except (Invalid, MultipleInvalid) as e:
|
||||
raise Error(humanize_error(data, e, max_sub_error_length))
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
__author__ = 'tusharmakkar08'
|
|
@ -0,0 +1,273 @@
|
|||
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'
|
||||
|
||||
Ensure that Optional('Classification') < 'Name' will return True instead of throwing an AttributeError
|
||||
|
||||
>>> Optional('Classification') < 'Name'
|
||||
True
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,162 @@
|
|||
import sys
|
||||
|
||||
from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid
|
||||
from voluptuous.schema_builder import Schema, default_factory, raises
|
||||
from voluptuous import validators
|
||||
|
||||
__author__ = 'tusharmakkar08'
|
||||
|
||||
|
||||
def _fix_str(v):
|
||||
if sys.version_info[0] == 2 and isinstance(v, unicode): # noqa: F821
|
||||
s = v
|
||||
else:
|
||||
s = str(v)
|
||||
return s
|
||||
|
||||
|
||||
def Lower(v):
|
||||
"""Transform a string to lower case.
|
||||
|
||||
>>> s = Schema(Lower)
|
||||
>>> s('HI')
|
||||
'hi'
|
||||
"""
|
||||
return _fix_str(v).lower()
|
||||
|
||||
|
||||
def Upper(v):
|
||||
"""Transform a string to upper case.
|
||||
|
||||
>>> s = Schema(Upper)
|
||||
>>> s('hi')
|
||||
'HI'
|
||||
"""
|
||||
return _fix_str(v).upper()
|
||||
|
||||
|
||||
def Capitalize(v):
|
||||
"""Capitalise a string.
|
||||
|
||||
>>> s = Schema(Capitalize)
|
||||
>>> s('hello world')
|
||||
'Hello world'
|
||||
"""
|
||||
return _fix_str(v).capitalize()
|
||||
|
||||
|
||||
def Title(v):
|
||||
"""Title case a string.
|
||||
|
||||
>>> s = Schema(Title)
|
||||
>>> s('hello world')
|
||||
'Hello World'
|
||||
"""
|
||||
return _fix_str(v).title()
|
||||
|
||||
|
||||
def Strip(v):
|
||||
"""Strip whitespace from a string.
|
||||
|
||||
>>> s = Schema(Strip)
|
||||
>>> s(' hello world ')
|
||||
'hello world'
|
||||
"""
|
||||
return _fix_str(v).strip()
|
||||
|
||||
|
||||
class DefaultTo(object):
|
||||
"""Sets a value to default_value if none provided.
|
||||
|
||||
>>> s = Schema(DefaultTo(42))
|
||||
>>> s(None)
|
||||
42
|
||||
>>> s = Schema(DefaultTo(list))
|
||||
>>> s(None)
|
||||
[]
|
||||
"""
|
||||
|
||||
def __init__(self, default_value, msg=None):
|
||||
self.default_value = default_factory(default_value)
|
||||
self.msg = msg
|
||||
|
||||
def __call__(self, v):
|
||||
if v is None:
|
||||
v = self.default_value()
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
return 'DefaultTo(%s)' % (self.default_value(),)
|
||||
|
||||
|
||||
class SetTo(object):
|
||||
"""Set a value, ignoring any previous value.
|
||||
|
||||
>>> s = Schema(validators.Any(int, SetTo(42)))
|
||||
>>> s(2)
|
||||
2
|
||||
>>> s("foo")
|
||||
42
|
||||
"""
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = default_factory(value)
|
||||
|
||||
def __call__(self, v):
|
||||
return self.value()
|
||||
|
||||
def __repr__(self):
|
||||
return 'SetTo(%s)' % (self.value(),)
|
||||
|
||||
|
||||
class Set(object):
|
||||
"""Convert a list into a set.
|
||||
|
||||
>>> s = Schema(Set())
|
||||
>>> s([]) == set([])
|
||||
True
|
||||
>>> s([1, 2]) == set([1, 2])
|
||||
True
|
||||
>>> with raises(Invalid, regex="^cannot be presented as set: "):
|
||||
... s([set([1, 2]), set([3, 4])])
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None):
|
||||
self.msg = msg
|
||||
|
||||
def __call__(self, v):
|
||||
try:
|
||||
set_v = set(v)
|
||||
except Exception as e:
|
||||
raise TypeInvalid(
|
||||
self.msg or 'cannot be presented as set: {0}'.format(e))
|
||||
return set_v
|
||||
|
||||
def __repr__(self):
|
||||
return 'Set()'
|
||||
|
||||
|
||||
class Literal(object):
|
||||
def __init__(self, lit):
|
||||
self.lit = lit
|
||||
|
||||
def __call__(self, value, msg=None):
|
||||
if self.lit != value:
|
||||
raise LiteralInvalid(
|
||||
msg or '%s not match for %s' % (value, self.lit)
|
||||
)
|
||||
else:
|
||||
return self.lit
|
||||
|
||||
def __str__(self):
|
||||
return str(self.lit)
|
||||
|
||||
def __repr__(self):
|
||||
return repr(self.lit)
|
||||
|
||||
|
||||
def u(x):
|
||||
if sys.version_info < (3,):
|
||||
return unicode(x) # noqa: F821
|
||||
else:
|
||||
return x
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue