Import Upstream version 0.14.2
This commit is contained in:
parent
d61a34eba1
commit
cf3d051f0b
|
@ -1,2 +0,0 @@
|
|||
[run]
|
||||
omit = *tests*
|
|
@ -1,16 +0,0 @@
|
|||
---
|
||||
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.
|
|
@ -1,42 +0,0 @@
|
|||
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 }}
|
|
@ -1,21 +0,0 @@
|
|||
*.gem
|
||||
*.swp
|
||||
*.pyc
|
||||
*#*
|
||||
build
|
||||
dist
|
||||
.svn/*
|
||||
.DS_Store
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info
|
||||
.coverage
|
||||
.tox
|
||||
MANIFEST
|
||||
.idea
|
||||
venv
|
||||
docs/_build
|
||||
.vscode/
|
||||
.venv
|
||||
.pytest_cache
|
||||
htmlcov
|
20
.travis.yml
20
.travis.yml
|
@ -1,20 +0,0 @@
|
|||
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=
|
41
CHANGELOG.md
41
CHANGELOG.md
|
@ -1,5 +1,46 @@
|
|||
# Changelog
|
||||
|
||||
## [0.14.2]
|
||||
|
||||
**New**:
|
||||
* [#507](https://github.com/alecthomas/voluptuous/pull/507): docs: document description field of Marker
|
||||
|
||||
**Fixes**:
|
||||
* [#506](https://github.com/alecthomas/voluptuous/pull/506): fix: allow unsortable containers in In and NotIn validators (fixes [#451](https://github.com/alecthomas/voluptuous/issues/451)) (bug introduced in 0.12.1)
|
||||
* [#488](https://github.com/alecthomas/voluptuous/pull/488): fix(typing): fix type hint for Coerce type param (bug introduced in 0.14.0)
|
||||
* [#497](https://github.com/alecthomas/voluptuous/pull/497): fix(typing): allow path to be a list of strings, integers or any other hashables (bug introduced in 0.14.0)
|
||||
|
||||
**Changes**:
|
||||
* [#499](https://github.com/alecthomas/voluptuous/pull/499): support: drop support for python 3.7
|
||||
* [#501](https://github.com/alecthomas/voluptuous/pull/501): support: run tests on python 3.11
|
||||
* [#502](https://github.com/alecthomas/voluptuous/pull/502): support: run tests on python 3.12
|
||||
* [#495](https://github.com/alecthomas/voluptuous/pull/495): refactor: drop duplicated type checks in Schema._compile
|
||||
* [#500](https://github.com/alecthomas/voluptuous/pull/500): refactor: fix few tests, use pytest.raises, extend raises helper
|
||||
* [#503](https://github.com/alecthomas/voluptuous/pull/503): refactor: Add linters configuration, reformat whole code
|
||||
|
||||
## [0.14.1]
|
||||
|
||||
**Changes**:
|
||||
* [#487](https://github.com/alecthomas/voluptuous/pull/487): Add pytest.ini and tox.ini to sdist
|
||||
* [#494](https://github.com/alecthomas/voluptuous/pull/494): Add `python_requires` so package installers know requirement is >= 3.7
|
||||
|
||||
## [0.14.0]
|
||||
|
||||
**Fixes**:
|
||||
* [#470](https://github.com/alecthomas/voluptuous/pull/470): Fix a few code comment typos
|
||||
* [#472](https://github.com/alecthomas/voluptuous/pull/472): Change to SPDX conform license string
|
||||
|
||||
|
||||
**New**:
|
||||
* [#475](https://github.com/alecthomas/voluptuous/pull/475): Add typing information
|
||||
* [#478](https://github.com/alecthomas/voluptuous/pull/478): Fix new type hint of schemas, for example for `Required('key')`
|
||||
* [#486](https://github.com/alecthomas/voluptuous/pull/486): Fix new type hints and enable `mypy`
|
||||
* [#479](https://github.com/alecthomas/voluptuous/pull/479): Allow error reporting on keys
|
||||
|
||||
**Changes**:
|
||||
* [#476](https://github.com/alecthomas/voluptuous/pull/476): Set static PyPI project description
|
||||
* [#482](https://github.com/alecthomas/voluptuous/pull/482): Remove Travis build status badge
|
||||
|
||||
## [0.13.1]
|
||||
|
||||
**Fixes**:
|
||||
|
|
|
@ -2,3 +2,5 @@ include *.md
|
|||
include COPYING
|
||||
include voluptuous/tests/*.py
|
||||
include voluptuous/tests/*.md
|
||||
include pyproject.toml
|
||||
include tox.ini
|
||||
|
|
|
@ -0,0 +1,744 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: voluptuous
|
||||
Version: 0.14.2
|
||||
Summary: Python data validation library
|
||||
Home-page: https://github.com/alecthomas/voluptuous
|
||||
Download-URL: https://pypi.python.org/pypi/voluptuous
|
||||
Author: Alec Thomas
|
||||
Author-email: alec@swapoff.org
|
||||
License: BSD-3-Clause
|
||||
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 :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: COPYING
|
||||
|
||||
|
||||
# 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)
|
||||
[![Test status](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml/badge.svg)](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml)
|
||||
[![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/).
|
||||
|
||||
## Contribution to Documentation
|
||||
|
||||
Documentation is built using `Sphinx`. You can install it by
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
For building `sphinx-apidoc` from scratch you need to set PYTHONPATH to `voluptuous/voluptuous` repository.
|
||||
|
||||
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.
|
||||
|
16
README.md
16
README.md
|
@ -12,8 +12,9 @@
|
|||
[![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)
|
||||
[![Test status](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml/badge.svg)](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml)
|
||||
[![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,
|
||||
|
@ -40,6 +41,16 @@ To file a bug, create a [new issue](https://github.com/alecthomas/voluptuous/iss
|
|||
|
||||
The documentation is provided [here](http://alecthomas.github.io/voluptuous/).
|
||||
|
||||
## Contribution to Documentation
|
||||
|
||||
Documentation is built using `Sphinx`. You can install it by
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
For building `sphinx-apidoc` from scratch you need to set PYTHONPATH to `voluptuous/voluptuous` repository.
|
||||
|
||||
The documentation is provided [here.](http://alecthomas.github.io/voluptuous/)
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](https://github.com/alecthomas/voluptuous/blob/master/CHANGELOG.md).
|
||||
|
@ -706,3 +717,4 @@ using voluptuous validators in `assert`s.
|
|||
|
||||
I greatly prefer the light-weight style promoted by these libraries to
|
||||
the complexity of libraries like FormEncode.
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
voluptuous for Debian
|
||||
|
||||
Please edit this to provide information specific to
|
||||
this voluptuous Debian package.
|
||||
|
||||
(Automatically generated by debmake Version 4.3.1)
|
||||
|
||||
-- sufang <sufang@kylinos.cn> Mon, 05 Sep 2022 16:17:16 +0800
|
|
@ -1,5 +0,0 @@
|
|||
voluptuous (0.13.1-ok1) yangtze; urgency=medium
|
||||
|
||||
* Build for openkylin.
|
||||
|
||||
-- sufang <sufang@kylinos.cn> Mon, 05 Sep 2022 16:17:16 +0800
|
|
@ -1 +0,0 @@
|
|||
11
|
|
@ -1,31 +0,0 @@
|
|||
Source: voluptuous
|
||||
Section: python
|
||||
Priority: optional
|
||||
Maintainer: OpenKylin Developers <packaging@lists.openkylin.top>
|
||||
Build-Depends: debhelper (>=11~),
|
||||
dh-python,
|
||||
python3-all,
|
||||
python3-nose,
|
||||
python3-setuptools,
|
||||
Standards-Version: 4.5.1
|
||||
Vcs-Git: https://gitee.com/openkylin/voluptuous.git
|
||||
Vcs-Browser: https://gitee.com/openkylin/voluptuous
|
||||
Homepage: https://github.com/alecthomas/voluptuous
|
||||
|
||||
Package: python3-voluptuous
|
||||
Architecture: all
|
||||
Depends:
|
||||
${misc:Depends},
|
||||
${python3:Depends},
|
||||
Description: Python 3 library to validate data
|
||||
Voluptuous is primarily intended to validate data coming in to Python
|
||||
as JSON, YAML, etc. The aim is to offer a light-weight style with three
|
||||
main goals:
|
||||
1. simplicity
|
||||
2. support for complex data structures
|
||||
3. provide useful error messages
|
||||
.
|
||||
It has been inspired by Validino and to a lesser extent by jsonvalidator and
|
||||
json_schema.
|
||||
.
|
||||
This package provides the Python 3.x module.
|
|
@ -1,61 +0,0 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: voluptuous
|
||||
Source: <url://example.com>
|
||||
#
|
||||
# Please double check copyright with the licensecheck(1) command.
|
||||
|
||||
Files: .coveragerc
|
||||
.github/ISSUE_TEMPLATE/contributions-only.md
|
||||
.github/workflows/tests.yml
|
||||
.gitignore
|
||||
.travis.yml
|
||||
CHANGELOG.md
|
||||
MANIFEST.in
|
||||
README.md
|
||||
pytest.ini
|
||||
setup.py
|
||||
tox.ini
|
||||
update_documentation.sh
|
||||
voluptuous/__init__.py
|
||||
voluptuous/error.py
|
||||
voluptuous/humanize.py
|
||||
voluptuous/schema_builder.py
|
||||
voluptuous/tests/__init__.py
|
||||
voluptuous/tests/tests.md
|
||||
voluptuous/tests/tests.py
|
||||
voluptuous/util.py
|
||||
voluptuous/validators.py
|
||||
Copyright: __NO_COPYRIGHT_NOR_LICENSE__
|
||||
License: __NO_COPYRIGHT_NOR_LICENSE__
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
# Files marked as NO_LICENSE_TEXT_FOUND may be covered by the following
|
||||
# license/copyright files.
|
||||
|
||||
#----------------------------------------------------------------------------
|
||||
# License file: COPYING
|
||||
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.
|
|
@ -1 +0,0 @@
|
|||
README.md
|
|
@ -1 +0,0 @@
|
|||
# You must remove unused comment lines for the released package.
|
|
@ -1,12 +0,0 @@
|
|||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
#export PYBUILD_VERBOSE=1
|
||||
export PYBUILD_NAME=voluptuous
|
||||
export PYBUILD_TEST_NOSE=1
|
||||
export PYBUILD_TEST_ARGS=-vv
|
||||
export SETUPTOOLS_USE_DISTUTILS=stdlib
|
||||
|
||||
%:
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
|
@ -1 +0,0 @@
|
|||
3.0 (native)
|
|
@ -1,4 +0,0 @@
|
|||
Bug-Database: https://github.com/alecthomas/voluptuous/issues
|
||||
Bug-Submit: https://github.com/alecthomas/voluptuous/issues/new
|
||||
Repository: https://github.com/alecthomas/voluptuous.git
|
||||
Repository-Browse: https://github.com/alecthomas/voluptuous
|
|
@ -1,3 +0,0 @@
|
|||
version=3
|
||||
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
|
||||
https://pypi.debian.net/voluptuous/voluptuous-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
|
|
@ -0,0 +1,18 @@
|
|||
[tool.black]
|
||||
target-version = ["py38", "py39", "py310", "py311", "py312"]
|
||||
skip-string-normalization = true
|
||||
|
||||
[tool.isort]
|
||||
skip_gitignore = true
|
||||
profile = "black"
|
||||
multi_line_output = 5
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
|
||||
warn_unused_ignores = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
python_files = "tests.py"
|
||||
testpaths = "voluptuous/tests"
|
||||
addopts = "--doctest-glob=*.md -v"
|
|
@ -1,4 +0,0 @@
|
|||
[pytest]
|
||||
python_files = tests.py
|
||||
testpaths = voluptuous/tests
|
||||
addopts = --doctest-glob=*.md -v
|
24
setup.py
24
setup.py
|
@ -1,15 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
import io
|
||||
import sys
|
||||
|
||||
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(
|
||||
|
@ -17,26 +16,29 @@ setup(
|
|||
url='https://github.com/alecthomas/voluptuous',
|
||||
download_url='https://pypi.python.org/pypi/voluptuous',
|
||||
version=version,
|
||||
description=description,
|
||||
description='Python data validation library',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
license='BSD',
|
||||
license='BSD-3-Clause',
|
||||
platforms=['any'],
|
||||
packages=['voluptuous'],
|
||||
package_data={
|
||||
'voluptuous': ['py.typed'],
|
||||
},
|
||||
author='Alec Thomas',
|
||||
author_email='alec@swapoff.org',
|
||||
python_requires=">=3.8",
|
||||
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',
|
||||
]
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
],
|
||||
setup_requires=['wheel']
|
||||
)
|
||||
|
|
22
tox.ini
22
tox.ini
|
@ -1,10 +1,10 @@
|
|||
[tox]
|
||||
envlist = flake8,py27,py36,py37,py38,py39,py310
|
||||
envlist = flake8,black,py38,py39,py310,py311,py312
|
||||
|
||||
[flake8]
|
||||
; E501: line too long (X > 79 characters)
|
||||
; W503 line break before binary operator
|
||||
ignore = E501,W503
|
||||
; E203, E704: black-related ignores (see https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#flake8)
|
||||
extend-ignore = E203, E501, E704
|
||||
exclude = .tox,.venv,build,*.egg
|
||||
|
||||
[testenv]
|
||||
|
@ -22,3 +22,19 @@ commands =
|
|||
[testenv:flake8]
|
||||
deps = flake8
|
||||
commands = flake8 --doctests setup.py voluptuous
|
||||
|
||||
[testenv:mypy]
|
||||
deps =
|
||||
mypy
|
||||
pytest
|
||||
commands = mypy voluptuous
|
||||
|
||||
[testenv:black]
|
||||
deps =
|
||||
black
|
||||
commands = black --check .
|
||||
|
||||
[testenv:isort]
|
||||
deps =
|
||||
isort
|
||||
commands = isort --check .
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
#!/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,744 @@
|
|||
Metadata-Version: 2.1
|
||||
Name: voluptuous
|
||||
Version: 0.14.2
|
||||
Summary: Python data validation library
|
||||
Home-page: https://github.com/alecthomas/voluptuous
|
||||
Download-URL: https://pypi.python.org/pypi/voluptuous
|
||||
Author: Alec Thomas
|
||||
Author-email: alec@swapoff.org
|
||||
License: BSD-3-Clause
|
||||
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 :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: COPYING
|
||||
|
||||
|
||||
# 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)
|
||||
[![Test status](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml/badge.svg)](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml)
|
||||
[![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/).
|
||||
|
||||
## Contribution to Documentation
|
||||
|
||||
Documentation is built using `Sphinx`. You can install it by
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
For building `sphinx-apidoc` from scratch you need to set PYTHONPATH to `voluptuous/voluptuous` repository.
|
||||
|
||||
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,21 @@
|
|||
CHANGELOG.md
|
||||
COPYING
|
||||
MANIFEST.in
|
||||
README.md
|
||||
pyproject.toml
|
||||
setup.py
|
||||
tox.ini
|
||||
voluptuous/__init__.py
|
||||
voluptuous/error.py
|
||||
voluptuous/humanize.py
|
||||
voluptuous/py.typed
|
||||
voluptuous/schema_builder.py
|
||||
voluptuous/util.py
|
||||
voluptuous/validators.py
|
||||
voluptuous.egg-info/PKG-INFO
|
||||
voluptuous.egg-info/SOURCES.txt
|
||||
voluptuous.egg-info/dependency_links.txt
|
||||
voluptuous.egg-info/top_level.txt
|
||||
voluptuous/tests/__init__.py
|
||||
voluptuous/tests/tests.md
|
||||
voluptuous/tests/tests.py
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
voluptuous
|
|
@ -1,9 +1,12 @@
|
|||
# flake8: noqa
|
||||
|
||||
# fmt: off
|
||||
from voluptuous.schema_builder import *
|
||||
from voluptuous.validators import *
|
||||
from voluptuous.util import *
|
||||
from voluptuous.error import *
|
||||
from voluptuous.validators import *
|
||||
|
||||
__version__ = '0.13.1'
|
||||
from voluptuous.error import * # isort: skip
|
||||
|
||||
# fmt: on
|
||||
|
||||
__version__ = '0.14.2'
|
||||
__author__ = 'alecthomas'
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
# fmt: off
|
||||
import typing
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base validation exception."""
|
||||
|
@ -17,54 +22,67 @@ class Invalid(Error):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, message, path=None, error_message=None, error_type=None):
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
path: typing.Optional[typing.List[typing.Hashable]] = None,
|
||||
error_message: typing.Optional[str] = None,
|
||||
error_type: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
Error.__init__(self, message)
|
||||
self.path = path or []
|
||||
self.error_message = error_message or message
|
||||
self._path = path or []
|
||||
self._error_message = error_message or message
|
||||
self.error_type = error_type
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
def msg(self) -> str:
|
||||
return self.args[0]
|
||||
|
||||
def __str__(self):
|
||||
path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \
|
||||
if self.path else ''
|
||||
@property
|
||||
def path(self) -> typing.List[typing.Hashable]:
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def error_message(self) -> str:
|
||||
return self._error_message
|
||||
|
||||
def __str__(self) -> str:
|
||||
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
|
||||
def prepend(self, path: typing.List[typing.Hashable]) -> None:
|
||||
self._path = path + self.path
|
||||
|
||||
|
||||
class MultipleInvalid(Invalid):
|
||||
def __init__(self, errors=None):
|
||||
def __init__(self, errors: typing.Optional[typing.List[Invalid]] = None) -> None:
|
||||
self.errors = errors[:] if errors else []
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return 'MultipleInvalid(%r)' % self.errors
|
||||
|
||||
@property
|
||||
def msg(self):
|
||||
def msg(self) -> str:
|
||||
return self.errors[0].msg
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
def path(self) -> typing.List[typing.Hashable]:
|
||||
return self.errors[0].path
|
||||
|
||||
@property
|
||||
def error_message(self):
|
||||
def error_message(self) -> str:
|
||||
return self.errors[0].error_message
|
||||
|
||||
def add(self, error):
|
||||
def add(self, error: Invalid) -> None:
|
||||
self.errors.append(error)
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return str(self.errors[0])
|
||||
|
||||
def prepend(self, path):
|
||||
def prepend(self, path: typing.List[typing.Hashable]) -> None:
|
||||
for error in self.errors:
|
||||
error.prepend(path)
|
||||
|
||||
|
@ -191,9 +209,11 @@ class ExactSequenceInvalid(Invalid):
|
|||
|
||||
class NotEnoughValid(Invalid):
|
||||
"""The value did not pass enough validations."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TooManyValid(Invalid):
|
||||
"""The value passed more than expected validations."""
|
||||
|
||||
pass
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
# fmt: off
|
||||
import typing
|
||||
|
||||
from voluptuous import Invalid, MultipleInvalid
|
||||
from voluptuous.error import Error
|
||||
from voluptuous.schema_builder import Schema
|
||||
|
||||
# fmt: on
|
||||
|
||||
MAX_VALIDATION_ERROR_ITEM_LENGTH = 500
|
||||
|
||||
|
||||
def _nested_getitem(data, path):
|
||||
def _nested_getitem(
|
||||
data: typing.Any, path: typing.List[typing.Hashable]
|
||||
) -> typing.Optional[typing.Any]:
|
||||
for item_index in path:
|
||||
try:
|
||||
data = data[item_index]
|
||||
|
@ -16,24 +23,34 @@ def _nested_getitem(data, path):
|
|||
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
|
||||
def humanize_error(
|
||||
data,
|
||||
validation_error: Invalid,
|
||||
max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH,
|
||||
) -> str:
|
||||
"""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
|
||||
))
|
||||
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] + '...'
|
||||
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):
|
||||
def validate_with_humanized_errors(
|
||||
data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH
|
||||
) -> typing.Any:
|
||||
try:
|
||||
return schema(data)
|
||||
except (Invalid, MultipleInvalid) as e:
|
||||
|
|
|
@ -1,31 +1,20 @@
|
|||
# fmt: off
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import inspect
|
||||
import re
|
||||
from functools import wraps
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
|
||||
from voluptuous import error as er
|
||||
from voluptuous.error import Error
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
long = int
|
||||
unicode = str
|
||||
basestring = str
|
||||
ifilter = filter
|
||||
|
||||
def iteritems(d):
|
||||
return d.items()
|
||||
else:
|
||||
from itertools import ifilter
|
||||
|
||||
def iteritems(d):
|
||||
return d.iteritems()
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
_Mapping = collections.abc.Mapping
|
||||
else:
|
||||
_Mapping = collections.Mapping
|
||||
# fmt: on
|
||||
|
||||
"""Schema validation for Python data structures.
|
||||
|
||||
|
@ -113,9 +102,6 @@ def _isnamedtuple(obj):
|
|||
return isinstance(obj, tuple) and hasattr(obj, '_fields')
|
||||
|
||||
|
||||
primitive_types = (str, unicode, bool, int, float)
|
||||
|
||||
|
||||
class Undefined(object):
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
|
@ -127,18 +113,23 @@ class Undefined(object):
|
|||
UNDEFINED = Undefined()
|
||||
|
||||
|
||||
def Self():
|
||||
def Self() -> None:
|
||||
raise er.SchemaError('"Self" should never be called')
|
||||
|
||||
|
||||
def default_factory(value):
|
||||
DefaultFactory = typing.Union[Undefined, typing.Callable[[], typing.Any]]
|
||||
|
||||
|
||||
def default_factory(value) -> DefaultFactory:
|
||||
if value is UNDEFINED or callable(value):
|
||||
return value
|
||||
return lambda: value
|
||||
|
||||
|
||||
@contextmanager
|
||||
def raises(exc, msg=None, regex=None):
|
||||
def raises(
|
||||
exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None
|
||||
) -> Generator[None, None, None]:
|
||||
try:
|
||||
yield
|
||||
except exc as e:
|
||||
|
@ -146,9 +137,11 @@ def raises(exc, msg=None, regex=None):
|
|||
assert str(e) == msg, '%r != %r' % (str(e), msg)
|
||||
if regex is not None:
|
||||
assert re.search(regex, str(e)), '%r does not match %r' % (str(e), regex)
|
||||
else:
|
||||
raise AssertionError(f"Did not raise exception {exc.__name__}")
|
||||
|
||||
|
||||
def Extra(_):
|
||||
def Extra(_) -> None:
|
||||
"""Allow keys in the data that are not present in the schema."""
|
||||
raise er.SchemaError('"Extra" should never be called')
|
||||
|
||||
|
@ -157,6 +150,18 @@ def Extra(_):
|
|||
# deprecated object, so we just leave an alias here instead.
|
||||
extra = Extra
|
||||
|
||||
primitive_types = (bool, bytes, int, str, float, complex)
|
||||
|
||||
# fmt: off
|
||||
Schemable = typing.Union[
|
||||
'Schema', 'Object',
|
||||
collections.abc.Mapping,
|
||||
list, tuple, frozenset, set,
|
||||
bool, bytes, int, str, float, complex,
|
||||
type, object, dict, None, typing.Callable
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
class Schema(object):
|
||||
"""A validation schema.
|
||||
|
@ -172,9 +177,9 @@ class Schema(object):
|
|||
|
||||
For Example:
|
||||
|
||||
>>> v = Schema({Required('a'): unicode})
|
||||
>>> v1 = Schema({Required('a'): unicode})
|
||||
>>> v2 = Schema({Required('b'): unicode})
|
||||
>>> v = Schema({Required('a'): str})
|
||||
>>> v1 = Schema({Required('a'): str})
|
||||
>>> v2 = Schema({Required('b'): str})
|
||||
>>> assert v == v1
|
||||
>>> assert v != v2
|
||||
|
||||
|
@ -186,7 +191,9 @@ class Schema(object):
|
|||
PREVENT_EXTRA: 'PREVENT_EXTRA',
|
||||
}
|
||||
|
||||
def __init__(self, schema, required=False, extra=PREVENT_EXTRA):
|
||||
def __init__(
|
||||
self, schema: Schemable, required: bool = False, extra: int = PREVENT_EXTRA
|
||||
) -> None:
|
||||
"""Create a new Schema.
|
||||
|
||||
:param schema: Validation schema. See :module:`voluptuous` for details.
|
||||
|
@ -207,7 +214,7 @@ class Schema(object):
|
|||
self._compiled = self._compile(schema)
|
||||
|
||||
@classmethod
|
||||
def infer(cls, data, **kwargs):
|
||||
def infer(cls, data, **kwargs) -> Schema:
|
||||
"""Create a Schema from concrete data (e.g. an API response).
|
||||
|
||||
For example, this will take a dict like:
|
||||
|
@ -234,18 +241,17 @@ class Schema(object):
|
|||
|
||||
Note: only very basic inference is supported.
|
||||
"""
|
||||
|
||||
def value_to_schema_type(value):
|
||||
if isinstance(value, dict):
|
||||
if len(value) == 0:
|
||||
return dict
|
||||
return {k: value_to_schema_type(v)
|
||||
for k, v in iteritems(value)}
|
||||
return {k: value_to_schema_type(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
if len(value) == 0:
|
||||
return list
|
||||
else:
|
||||
return [value_to_schema_type(v)
|
||||
for v in value]
|
||||
return [value_to_schema_type(v) for v in value]
|
||||
return type(value)
|
||||
|
||||
return cls(value_to_schema_type(data), **kwargs)
|
||||
|
@ -263,8 +269,11 @@ class Schema(object):
|
|||
|
||||
def __repr__(self):
|
||||
return "<Schema(%s, extra=%s, required=%s) object at 0x%x>" % (
|
||||
self.schema, self._extra_to_name.get(self.extra, '??'),
|
||||
self.required, id(self))
|
||||
self.schema,
|
||||
self._extra_to_name.get(self.extra, '??'),
|
||||
self.required,
|
||||
id(self),
|
||||
)
|
||||
|
||||
def __call__(self, data):
|
||||
"""Validate data against this schema."""
|
||||
|
@ -285,7 +294,7 @@ class Schema(object):
|
|||
return schema.__voluptuous_compile__(self)
|
||||
if isinstance(schema, Object):
|
||||
return self._compile_object(schema)
|
||||
if isinstance(schema, _Mapping):
|
||||
if isinstance(schema, collections.abc.Mapping):
|
||||
return self._compile_dict(schema)
|
||||
elif isinstance(schema, list):
|
||||
return self._compile_list(schema)
|
||||
|
@ -296,30 +305,34 @@ class Schema(object):
|
|||
type_ = type(schema)
|
||||
if inspect.isclass(schema):
|
||||
type_ = schema
|
||||
if type_ in (bool, bytes, int, long, str, unicode, float, complex, object,
|
||||
list, dict, type(None)) or callable(schema):
|
||||
if type_ in (*primitive_types, object, type(None)) or callable(schema):
|
||||
return _compile_scalar(schema)
|
||||
raise er.SchemaError('unsupported schema data type %r' %
|
||||
type(schema).__name__)
|
||||
raise er.SchemaError('unsupported schema data type %r' % type(schema).__name__)
|
||||
|
||||
def _compile_mapping(self, schema, invalid_msg=None):
|
||||
"""Create validator for given mapping."""
|
||||
invalid_msg = invalid_msg or 'mapping value'
|
||||
|
||||
# Keys that may be required
|
||||
all_required_keys = set(key for key in schema
|
||||
if key is not Extra
|
||||
and ((self.required
|
||||
and not isinstance(key, (Optional, Remove)))
|
||||
or isinstance(key, Required)))
|
||||
all_required_keys = set(
|
||||
key
|
||||
for key in schema
|
||||
if key is not Extra
|
||||
and (
|
||||
(self.required and not isinstance(key, (Optional, Remove)))
|
||||
or isinstance(key, Required)
|
||||
)
|
||||
)
|
||||
|
||||
# Keys that may have defaults
|
||||
all_default_keys = set(key for key in schema
|
||||
if isinstance(key, Required)
|
||||
or isinstance(key, Optional))
|
||||
all_default_keys = set(
|
||||
key
|
||||
for key in schema
|
||||
if isinstance(key, Required) or isinstance(key, Optional)
|
||||
)
|
||||
|
||||
_compiled_schema = {}
|
||||
for skey, svalue in iteritems(schema):
|
||||
for skey, svalue in schema.items():
|
||||
new_key = self._compile(skey)
|
||||
new_value = self._compile(svalue)
|
||||
_compiled_schema[skey] = (new_key, new_value)
|
||||
|
@ -335,7 +348,9 @@ class Schema(object):
|
|||
if type(skey) in primitive_types:
|
||||
candidates_by_key.setdefault(skey, []).append((skey, (ckey, cvalue)))
|
||||
elif isinstance(skey, Marker) and type(skey.schema) in primitive_types:
|
||||
candidates_by_key.setdefault(skey.schema, []).append((skey, (ckey, cvalue)))
|
||||
candidates_by_key.setdefault(skey.schema, []).append(
|
||||
(skey, (ckey, cvalue))
|
||||
)
|
||||
else:
|
||||
# These are wildcards such as 'int', 'str', 'Remove' and others which should be applied to all keys
|
||||
additional_candidates.append((skey, (ckey, cvalue)))
|
||||
|
@ -352,23 +367,27 @@ class Schema(object):
|
|||
|
||||
# Insert default values for non-existing keys.
|
||||
for key in all_default_keys:
|
||||
if not isinstance(key.default, Undefined) and \
|
||||
key.schema not in key_value_map:
|
||||
if (
|
||||
not isinstance(key.default, Undefined)
|
||||
and key.schema not in key_value_map
|
||||
):
|
||||
# A default value has been specified for this missing
|
||||
# key, insert it.
|
||||
key_value_map[key.schema] = key.default()
|
||||
|
||||
error = None
|
||||
errors = []
|
||||
for key, value in key_value_map.items():
|
||||
key_path = path + [key]
|
||||
remove_key = False
|
||||
|
||||
# Optimization. Validate against the matching key first, then fallback to the rest
|
||||
relevant_candidates = itertools.chain(candidates_by_key.get(key, []), additional_candidates)
|
||||
relevant_candidates = itertools.chain(
|
||||
candidates_by_key.get(key, []), additional_candidates
|
||||
)
|
||||
|
||||
# compare each given key/value against all compiled key/values
|
||||
# schema key, (compiled key, compiled value)
|
||||
error = None
|
||||
for skey, (ckey, cvalue) in relevant_candidates:
|
||||
try:
|
||||
new_key = ckey(key_path, key)
|
||||
|
@ -416,7 +435,9 @@ class Schema(object):
|
|||
|
||||
break
|
||||
else:
|
||||
if remove_key:
|
||||
if error:
|
||||
errors.append(error)
|
||||
elif remove_key:
|
||||
# remove key
|
||||
continue
|
||||
elif self.extra == ALLOW_EXTRA:
|
||||
|
@ -427,7 +448,11 @@ class Schema(object):
|
|||
|
||||
# for any required keys left that weren't found and don't have defaults:
|
||||
for key in required_keys:
|
||||
msg = key.msg if hasattr(key, 'msg') and key.msg else 'required key not provided'
|
||||
msg = (
|
||||
key.msg
|
||||
if hasattr(key, 'msg') and key.msg
|
||||
else 'required key not provided'
|
||||
)
|
||||
errors.append(er.RequiredFieldInvalid(msg, path + [key]))
|
||||
if errors:
|
||||
raise er.MultipleInvalid(errors)
|
||||
|
@ -454,14 +479,13 @@ class Schema(object):
|
|||
... validate(Structure(one='three'))
|
||||
|
||||
"""
|
||||
base_validate = self._compile_mapping(
|
||||
schema, invalid_msg='object value')
|
||||
base_validate = self._compile_mapping(schema, invalid_msg='object value')
|
||||
|
||||
def validate_object(path, data):
|
||||
if schema.cls is not UNDEFINED and not isinstance(data, schema.cls):
|
||||
raise er.ObjectInvalid('expected a {0!r}'.format(schema.cls), path)
|
||||
iterable = _iterate_object(data)
|
||||
iterable = ifilter(lambda item: item[1] is not None, iterable)
|
||||
iterable = filter(lambda item: item[1] is not None, iterable)
|
||||
out = base_validate(path, iterable, {})
|
||||
return type(data)(**out)
|
||||
|
||||
|
@ -543,8 +567,7 @@ class Schema(object):
|
|||
"expected str for dictionary value @ data['adict']['strfield']"]
|
||||
|
||||
"""
|
||||
base_validate = self._compile_mapping(
|
||||
schema, invalid_msg='dictionary value')
|
||||
base_validate = self._compile_mapping(schema, invalid_msg='dictionary value')
|
||||
|
||||
groups_of_exclusion = {}
|
||||
groups_of_inclusion = {}
|
||||
|
@ -566,8 +589,12 @@ class Schema(object):
|
|||
for exclusive in group:
|
||||
if exclusive.schema in data:
|
||||
if exists:
|
||||
msg = exclusive.msg if hasattr(exclusive, 'msg') and exclusive.msg else \
|
||||
"two or more values in the same group of exclusion '%s'" % label
|
||||
msg = (
|
||||
exclusive.msg
|
||||
if hasattr(exclusive, 'msg') and exclusive.msg
|
||||
else "two or more values in the same group of exclusion '%s'"
|
||||
% label
|
||||
)
|
||||
next_path = path + [VirtualPathComponent(label)]
|
||||
errors.append(er.ExclusiveInvalid(msg, next_path))
|
||||
break
|
||||
|
@ -579,7 +606,10 @@ class Schema(object):
|
|||
for label, group in groups_of_inclusion.items():
|
||||
included = [node.schema in data for node in group]
|
||||
if any(included) and not all(included):
|
||||
msg = "some but not all values in the same group of inclusion '%s'" % label
|
||||
msg = (
|
||||
"some but not all values in the same group of inclusion '%s'"
|
||||
% label
|
||||
)
|
||||
for g in group:
|
||||
if hasattr(g, 'msg') and g.msg:
|
||||
msg = g.msg
|
||||
|
@ -592,7 +622,7 @@ class Schema(object):
|
|||
raise er.MultipleInvalid(errors)
|
||||
|
||||
out = data.__class__()
|
||||
return base_validate(path, iteritems(data), out)
|
||||
return base_validate(path, data.items(), out)
|
||||
|
||||
return validate_dict
|
||||
|
||||
|
@ -619,9 +649,9 @@ class Schema(object):
|
|||
# Empty seq schema, reject any data.
|
||||
if not schema:
|
||||
if data:
|
||||
raise er.MultipleInvalid([
|
||||
er.ValueInvalid('not a valid value', path if path else data)
|
||||
])
|
||||
raise er.MultipleInvalid(
|
||||
[er.ValueInvalid('not a valid value', path if path else data)]
|
||||
)
|
||||
return data
|
||||
|
||||
out = []
|
||||
|
@ -723,7 +753,12 @@ class Schema(object):
|
|||
|
||||
return validate_set
|
||||
|
||||
def extend(self, schema, required=None, extra=None):
|
||||
def extend(
|
||||
self,
|
||||
schema: Schemable,
|
||||
required: typing.Optional[bool] = None,
|
||||
extra: typing.Optional[int] = None,
|
||||
) -> Schema:
|
||||
"""Create a new `Schema` by merging this and the provided `schema`.
|
||||
|
||||
Neither this `Schema` nor the provided `schema` are modified. The
|
||||
|
@ -737,13 +772,15 @@ class Schema(object):
|
|||
:param extra: if set, overrides `extra` of this `Schema`
|
||||
"""
|
||||
|
||||
assert type(self.schema) == dict and type(schema) == dict, 'Both schemas must be dictionary-based'
|
||||
assert isinstance(self.schema, dict) and isinstance(
|
||||
schema, dict
|
||||
), 'Both schemas must be dictionary-based'
|
||||
|
||||
result = self.schema.copy()
|
||||
|
||||
# returns the key that may have been passed as an argument to Marker constructor
|
||||
def key_literal(key):
|
||||
return (key.schema if isinstance(key, Marker) else key)
|
||||
return key.schema if isinstance(key, Marker) else key
|
||||
|
||||
# build a map that takes the key literals to the needed objects
|
||||
# literal -> Required|Optional|literal
|
||||
|
@ -751,18 +788,16 @@ class Schema(object):
|
|||
|
||||
# for each item in the extension schema, replace duplicates
|
||||
# or add new keys
|
||||
for key, value in iteritems(schema):
|
||||
|
||||
for key, value in schema.items():
|
||||
# if the key is already in the dictionary, we need to replace it
|
||||
# transform key to literal before checking presence
|
||||
if key_literal(key) in result_key_map:
|
||||
|
||||
result_key = result_key_map[key_literal(key)]
|
||||
result_value = result[result_key]
|
||||
|
||||
# if both are dictionaries, we need to extend recursively
|
||||
# create the new extended sub schema, then remove the old key and add the new one
|
||||
if type(result_value) == dict and type(value) == dict:
|
||||
if isinstance(result_value, dict) and isinstance(value, dict):
|
||||
new_value = Schema(result_value).extend(value).schema
|
||||
del result[result_key]
|
||||
result[key] = new_value
|
||||
|
@ -778,8 +813,8 @@ class Schema(object):
|
|||
|
||||
# recompile and send old object
|
||||
result_cls = type(self)
|
||||
result_required = (required if required is not None else self.required)
|
||||
result_extra = (extra if extra is not None else self.extra)
|
||||
result_required = required if required is not None else self.required
|
||||
result_extra = extra if extra is not None else self.extra
|
||||
return result_cls(result, required=result_required, extra=result_extra)
|
||||
|
||||
|
||||
|
@ -803,6 +838,7 @@ def _compile_scalar(schema):
|
|||
... _compile_scalar(lambda v: float(v))([], 'a')
|
||||
"""
|
||||
if inspect.isclass(schema):
|
||||
|
||||
def validate_instance(path, data):
|
||||
if isinstance(data, schema):
|
||||
return data
|
||||
|
@ -813,6 +849,7 @@ def _compile_scalar(schema):
|
|||
return validate_instance
|
||||
|
||||
if callable(schema):
|
||||
|
||||
def validate_callable(path, data):
|
||||
try:
|
||||
return schema(data)
|
||||
|
@ -855,18 +892,20 @@ def _compile_itemsort():
|
|||
# Remove markers should match first (since invalid values will not
|
||||
# raise an Error, instead the validator will check if other schemas match
|
||||
# the same value).
|
||||
priority = [(1, is_remove), # Remove highest priority after values
|
||||
(2, is_marker), # then other Markers
|
||||
(4, is_type), # types/classes lowest before Extra
|
||||
(3, is_callable), # callables after markers
|
||||
(5, is_extra)] # Extra lowest priority
|
||||
priority = [
|
||||
(1, is_remove), # Remove highest priority after values
|
||||
(2, is_marker), # then other Markers
|
||||
(4, is_type), # types/classes lowest before Extra
|
||||
(3, is_callable), # callables after markers
|
||||
(5, is_extra), # Extra lowest priority
|
||||
]
|
||||
|
||||
def item_priority(item_):
|
||||
key_ = item_[0]
|
||||
for i, check_ in priority:
|
||||
if check_(key_):
|
||||
return i
|
||||
# values have hightest priorities
|
||||
# values have highest priorities
|
||||
return 0
|
||||
|
||||
return item_priority
|
||||
|
@ -880,7 +919,7 @@ def _iterate_mapping_candidates(schema):
|
|||
# Without this, Extra might appear first in the iterator, and fail to
|
||||
# validate a key even though it's a Required that has its own validation,
|
||||
# generating a false positive.
|
||||
return sorted(iteritems(schema), key=_sort_item)
|
||||
return sorted(schema.items(), key=_sort_item)
|
||||
|
||||
|
||||
def _iterate_object(obj):
|
||||
|
@ -895,7 +934,7 @@ def _iterate_object(obj):
|
|||
# maybe we have named tuple here?
|
||||
if hasattr(obj, '_asdict'):
|
||||
d = obj._asdict()
|
||||
for item in iteritems(d):
|
||||
for item in d.items():
|
||||
yield item
|
||||
try:
|
||||
slots = obj.__slots__
|
||||
|
@ -936,10 +975,16 @@ class Msg(object):
|
|||
... assert isinstance(e.errors[0], er.RangeInvalid)
|
||||
"""
|
||||
|
||||
def __init__(self, schema, msg, cls=None):
|
||||
def __init__(
|
||||
self,
|
||||
schema: Schemable,
|
||||
msg: str,
|
||||
cls: typing.Optional[typing.Type[Error]] = None,
|
||||
) -> None:
|
||||
if cls and not issubclass(cls, er.Invalid):
|
||||
raise er.SchemaError("Msg can only use subclases of"
|
||||
" Invalid as custom class")
|
||||
raise er.SchemaError(
|
||||
"Msg can only use subclases of Invalid as custom class"
|
||||
)
|
||||
self._schema = schema
|
||||
self.schema = Schema(schema)
|
||||
self.msg = msg
|
||||
|
@ -961,7 +1006,7 @@ class Msg(object):
|
|||
class Object(dict):
|
||||
"""Indicate that we should work with attributes, not keys."""
|
||||
|
||||
def __init__(self, schema, cls=UNDEFINED):
|
||||
def __init__(self, schema: typing.Any, cls: object = UNDEFINED) -> None:
|
||||
self.cls = cls
|
||||
super(Object, self).__init__(schema)
|
||||
|
||||
|
@ -975,9 +1020,18 @@ class VirtualPathComponent(str):
|
|||
|
||||
|
||||
class Marker(object):
|
||||
"""Mark nodes for special treatment."""
|
||||
"""Mark nodes for special treatment.
|
||||
|
||||
def __init__(self, schema_, msg=None, description=None):
|
||||
`description` is an optional field, unused by Voluptuous itself, but can be
|
||||
introspected by any external tool, for example to generate schema documentation.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
schema_: Schemable,
|
||||
msg: typing.Optional[str] = None,
|
||||
description: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
self.schema = schema_
|
||||
self._schema = Schema(schema_)
|
||||
self.msg = msg
|
||||
|
@ -1009,7 +1063,7 @@ class Marker(object):
|
|||
return self.schema == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not(self.schema == other)
|
||||
return not (self.schema == other)
|
||||
|
||||
|
||||
class Optional(Marker):
|
||||
|
@ -1035,9 +1089,14 @@ class Optional(Marker):
|
|||
{'key2': 'value'}
|
||||
"""
|
||||
|
||||
def __init__(self, schema, msg=None, default=UNDEFINED, description=None):
|
||||
super(Optional, self).__init__(schema, msg=msg,
|
||||
description=description)
|
||||
def __init__(
|
||||
self,
|
||||
schema: Schemable,
|
||||
msg: typing.Optional[str] = None,
|
||||
default=UNDEFINED,
|
||||
description: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
super(Optional, self).__init__(schema, msg=msg, description=description)
|
||||
self.default = default_factory(default)
|
||||
|
||||
|
||||
|
@ -1060,15 +1119,15 @@ class Exclusive(Optional):
|
|||
>>> msg = 'Please, use only one type of authentication at the same time.'
|
||||
>>> schema = Schema({
|
||||
... Exclusive('classic', 'auth', msg=msg):{
|
||||
... Required('email'): basestring,
|
||||
... Required('password'): basestring
|
||||
... Required('email'): str,
|
||||
... Required('password'): str
|
||||
... },
|
||||
... Exclusive('internal', 'auth', msg=msg):{
|
||||
... Required('secret_key'): basestring
|
||||
... Required('secret_key'): str
|
||||
... },
|
||||
... Exclusive('social', 'auth', msg=msg):{
|
||||
... Required('social_network'): basestring,
|
||||
... Required('token'): basestring
|
||||
... Required('social_network'): str,
|
||||
... Required('token'): str
|
||||
... }
|
||||
... })
|
||||
|
||||
|
@ -1077,14 +1136,19 @@ class Exclusive(Optional):
|
|||
... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}})
|
||||
"""
|
||||
|
||||
def __init__(self, schema, group_of_exclusion, msg=None, description=None):
|
||||
super(Exclusive, self).__init__(schema, msg=msg,
|
||||
description=description)
|
||||
def __init__(
|
||||
self,
|
||||
schema: Schemable,
|
||||
group_of_exclusion: str,
|
||||
msg: typing.Optional[str] = None,
|
||||
description: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
super(Exclusive, self).__init__(schema, msg=msg, description=description)
|
||||
self.group_of_exclusion = group_of_exclusion
|
||||
|
||||
|
||||
class Inclusive(Optional):
|
||||
""" Mark a node in the schema as inclusive.
|
||||
"""Mark a node in the schema as inclusive.
|
||||
|
||||
Inclusive keys inherited from Optional:
|
||||
|
||||
|
@ -1125,11 +1189,17 @@ class Inclusive(Optional):
|
|||
True
|
||||
"""
|
||||
|
||||
def __init__(self, schema, group_of_inclusion,
|
||||
msg=None, description=None, default=UNDEFINED):
|
||||
super(Inclusive, self).__init__(schema, msg=msg,
|
||||
default=default,
|
||||
description=description)
|
||||
def __init__(
|
||||
self,
|
||||
schema: Schemable,
|
||||
group_of_inclusion: str,
|
||||
msg: typing.Optional[str] = None,
|
||||
description: typing.Optional[str] = None,
|
||||
default=UNDEFINED,
|
||||
) -> None:
|
||||
super(Inclusive, self).__init__(
|
||||
schema, msg=msg, default=default, description=description
|
||||
)
|
||||
self.group_of_inclusion = group_of_inclusion
|
||||
|
||||
|
||||
|
@ -1148,9 +1218,14 @@ class Required(Marker):
|
|||
{'key': []}
|
||||
"""
|
||||
|
||||
def __init__(self, schema, msg=None, default=UNDEFINED, description=None):
|
||||
super(Required, self).__init__(schema, msg=msg,
|
||||
description=description)
|
||||
def __init__(
|
||||
self,
|
||||
schema: Schemable,
|
||||
msg: typing.Optional[str] = None,
|
||||
default=UNDEFINED,
|
||||
description: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
super(Required, self).__init__(schema, msg=msg, description=description)
|
||||
self.default = default_factory(default)
|
||||
|
||||
|
||||
|
@ -1169,8 +1244,8 @@ class Remove(Marker):
|
|||
[1, 2, 3, 5, '7']
|
||||
"""
|
||||
|
||||
def __call__(self, v):
|
||||
super(Remove, self).__call__(v)
|
||||
def __call__(self, schema: Schemable):
|
||||
super(Remove, self).__call__(schema)
|
||||
return self.__class__
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -1180,7 +1255,10 @@ class Remove(Marker):
|
|||
return object.__hash__(self)
|
||||
|
||||
|
||||
def message(default=None, cls=None):
|
||||
def message(
|
||||
default: typing.Optional[str] = None,
|
||||
cls: typing.Optional[typing.Type[Error]] = None,
|
||||
) -> typing.Callable:
|
||||
"""Convenience decorator to allow functions to provide a message.
|
||||
|
||||
Set a default message:
|
||||
|
@ -1209,7 +1287,9 @@ def message(default=None, cls=None):
|
|||
... assert isinstance(e.errors[0], IntegerInvalid)
|
||||
"""
|
||||
if cls and not issubclass(cls, er.Invalid):
|
||||
raise er.SchemaError("message can only use subclases of Invalid as custom class")
|
||||
raise er.SchemaError(
|
||||
"message can only use subclases of Invalid as custom class"
|
||||
)
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
|
@ -1219,7 +1299,9 @@ def message(default=None, cls=None):
|
|||
try:
|
||||
return f(*args, **kwargs)
|
||||
except ValueError:
|
||||
raise (clsoverride or cls or er.ValueInvalid)(msg or default or 'invalid value')
|
||||
raise (clsoverride or cls or er.ValueInvalid)(
|
||||
msg or default or 'invalid value'
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
@ -1238,9 +1320,11 @@ def _args_to_dict(func, args):
|
|||
arg_names = func.func_code.co_varnames[:arg_count]
|
||||
|
||||
arg_value_list = list(args)
|
||||
arguments = dict((arg_name, arg_value_list[i])
|
||||
for i, arg_name in enumerate(arg_names)
|
||||
if i < len(arg_value_list))
|
||||
arguments = dict(
|
||||
(arg_name, arg_value_list[i])
|
||||
for i, arg_name in enumerate(arg_names)
|
||||
if i < len(arg_value_list)
|
||||
)
|
||||
return arguments
|
||||
|
||||
|
||||
|
@ -1251,7 +1335,7 @@ def _merge_args_with_kwargs(args_dict, kwargs_dict):
|
|||
return ret
|
||||
|
||||
|
||||
def validate(*a, **kw):
|
||||
def validate(*a, **kw) -> typing.Callable:
|
||||
"""Decorator for validating arguments of a function against a given schema.
|
||||
|
||||
Set restrictions for arguments:
|
||||
|
@ -1270,7 +1354,6 @@ def validate(*a, **kw):
|
|||
RETURNS_KEY = '__return__'
|
||||
|
||||
def validate_schema_decorator(func):
|
||||
|
||||
returns_defined = False
|
||||
returns = None
|
||||
|
||||
|
@ -1282,8 +1365,11 @@ def validate(*a, **kw):
|
|||
returns = schema_arguments[RETURNS_KEY]
|
||||
del schema_arguments[RETURNS_KEY]
|
||||
|
||||
input_schema = (Schema(schema_arguments, extra=ALLOW_EXTRA)
|
||||
if len(schema_arguments) != 0 else lambda x: x)
|
||||
input_schema = (
|
||||
Schema(schema_arguments, extra=ALLOW_EXTRA)
|
||||
if len(schema_arguments) != 0
|
||||
else lambda x: x
|
||||
)
|
||||
output_schema = Schema(returns) if returns_defined else lambda x: x
|
||||
|
||||
@wraps(func)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,68 +1,65 @@
|
|||
import sys
|
||||
# F401: "imported but unused"
|
||||
# fmt: off
|
||||
import typing
|
||||
|
||||
from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid
|
||||
from voluptuous.schema_builder import Schema, default_factory, raises
|
||||
from voluptuous import validators
|
||||
from voluptuous import validators # noqa: F401
|
||||
from voluptuous.error import Invalid, LiteralInvalid, TypeInvalid # noqa: F401
|
||||
from voluptuous.schema_builder import DefaultFactory # noqa: F401
|
||||
from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401
|
||||
|
||||
# fmt: on
|
||||
|
||||
__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):
|
||||
def Lower(v: str) -> str:
|
||||
"""Transform a string to lower case.
|
||||
|
||||
>>> s = Schema(Lower)
|
||||
>>> s('HI')
|
||||
'hi'
|
||||
"""
|
||||
return _fix_str(v).lower()
|
||||
return str(v).lower()
|
||||
|
||||
|
||||
def Upper(v):
|
||||
def Upper(v: str) -> str:
|
||||
"""Transform a string to upper case.
|
||||
|
||||
>>> s = Schema(Upper)
|
||||
>>> s('hi')
|
||||
'HI'
|
||||
"""
|
||||
return _fix_str(v).upper()
|
||||
return str(v).upper()
|
||||
|
||||
|
||||
def Capitalize(v):
|
||||
def Capitalize(v: str) -> str:
|
||||
"""Capitalise a string.
|
||||
|
||||
>>> s = Schema(Capitalize)
|
||||
>>> s('hello world')
|
||||
'Hello world'
|
||||
"""
|
||||
return _fix_str(v).capitalize()
|
||||
return str(v).capitalize()
|
||||
|
||||
|
||||
def Title(v):
|
||||
def Title(v: str) -> str:
|
||||
"""Title case a string.
|
||||
|
||||
>>> s = Schema(Title)
|
||||
>>> s('hello world')
|
||||
'Hello World'
|
||||
"""
|
||||
return _fix_str(v).title()
|
||||
return str(v).title()
|
||||
|
||||
|
||||
def Strip(v):
|
||||
def Strip(v: str) -> str:
|
||||
"""Strip whitespace from a string.
|
||||
|
||||
>>> s = Schema(Strip)
|
||||
>>> s(' hello world ')
|
||||
'hello world'
|
||||
"""
|
||||
return _fix_str(v).strip()
|
||||
return str(v).strip()
|
||||
|
||||
|
||||
class DefaultTo(object):
|
||||
|
@ -76,7 +73,7 @@ class DefaultTo(object):
|
|||
[]
|
||||
"""
|
||||
|
||||
def __init__(self, default_value, msg=None):
|
||||
def __init__(self, default_value, msg: typing.Optional[str] = None) -> None:
|
||||
self.default_value = default_factory(default_value)
|
||||
self.msg = msg
|
||||
|
||||
|
@ -99,7 +96,7 @@ class SetTo(object):
|
|||
42
|
||||
"""
|
||||
|
||||
def __init__(self, value):
|
||||
def __init__(self, value) -> None:
|
||||
self.value = default_factory(value)
|
||||
|
||||
def __call__(self, v):
|
||||
|
@ -121,15 +118,14 @@ class Set(object):
|
|||
... s([set([1, 2]), set([3, 4])])
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None):
|
||||
def __init__(self, msg: typing.Optional[str] = None) -> 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))
|
||||
raise TypeInvalid(self.msg or 'cannot be presented as set: {0}'.format(e))
|
||||
return set_v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -137,14 +133,12 @@ class Set(object):
|
|||
|
||||
|
||||
class Literal(object):
|
||||
def __init__(self, lit):
|
||||
def __init__(self, lit) -> None:
|
||||
self.lit = lit
|
||||
|
||||
def __call__(self, value, msg=None):
|
||||
def __call__(self, value, msg: typing.Optional[str] = None):
|
||||
if self.lit != value:
|
||||
raise LiteralInvalid(
|
||||
msg or '%s not match for %s' % (value, self.lit)
|
||||
)
|
||||
raise LiteralInvalid(msg or '%s not match for %s' % (value, self.lit))
|
||||
else:
|
||||
return self.lit
|
||||
|
||||
|
@ -153,10 +147,3 @@ class Literal(object):
|
|||
|
||||
def __repr__(self):
|
||||
return repr(self.lit)
|
||||
|
||||
|
||||
def u(x):
|
||||
if sys.version_info < (3,):
|
||||
return unicode(x) # noqa: F821
|
||||
else:
|
||||
return x
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
# fmt: off
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import sys
|
||||
from functools import wraps
|
||||
import typing
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from functools import wraps
|
||||
|
||||
from voluptuous.error import (
|
||||
AllInvalid, AnyInvalid, BooleanInvalid, CoerceInvalid, ContainsInvalid, DateInvalid,
|
||||
DatetimeInvalid, DirInvalid, EmailInvalid, ExactSequenceInvalid, FalseInvalid,
|
||||
FileInvalid, InInvalid, Invalid, LengthInvalid, MatchInvalid, MultipleInvalid,
|
||||
NotEnoughValid, NotInInvalid, PathInvalid, RangeInvalid, TooManyValid, TrueInvalid,
|
||||
TypeInvalid, UrlInvalid,
|
||||
)
|
||||
|
||||
# F401: flake8 complains about 'raises' not being used, but it is used in doctests
|
||||
from voluptuous.schema_builder import Schema, Schemable, message, raises # noqa: F401
|
||||
|
||||
# fmt: on
|
||||
|
||||
|
||||
Enum: typing.Union[type, None]
|
||||
try:
|
||||
from enum import Enum
|
||||
except ImportError:
|
||||
Enum = None
|
||||
|
||||
from voluptuous.schema_builder import Schema, raises, message
|
||||
from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid,
|
||||
AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid,
|
||||
RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid,
|
||||
DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid,
|
||||
TooManyValid)
|
||||
|
||||
if sys.version_info >= (3,):
|
||||
import urllib.parse as urlparse
|
||||
|
@ -24,6 +36,7 @@ else:
|
|||
import urlparse
|
||||
|
||||
# Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py
|
||||
# fmt: off
|
||||
USER_REGEX = re.compile(
|
||||
# start anchor, because fullmatch is not available in python 2.7
|
||||
"(?:"
|
||||
|
@ -35,35 +48,38 @@ USER_REGEX = re.compile(
|
|||
r"""\\[\001-\011\013\014\016-\177])*"$)"""
|
||||
# end anchor, because fullmatch is not available in python 2.7
|
||||
r")\Z",
|
||||
re.IGNORECASE
|
||||
re.IGNORECASE,
|
||||
)
|
||||
DOMAIN_REGEX = re.compile(
|
||||
# start anchor, because fullmatch is not available in python 2.7
|
||||
"(?:"
|
||||
# domain
|
||||
r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+'
|
||||
# tld
|
||||
r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)'
|
||||
# literal form, ipv4 address (SMTP 4.1.3)
|
||||
r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)'
|
||||
r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$'
|
||||
# end anchor, because fullmatch is not available in python 2.7
|
||||
r")\Z",
|
||||
re.IGNORECASE)
|
||||
re.IGNORECASE,
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
__author__ = 'tusharmakkar08'
|
||||
|
||||
|
||||
def truth(f):
|
||||
def truth(f: typing.Callable) -> typing.Callable:
|
||||
"""Convenience decorator to convert truth functions into validators.
|
||||
|
||||
>>> @truth
|
||||
... def isdir(v):
|
||||
... return os.path.isdir(v)
|
||||
>>> validate = Schema(isdir)
|
||||
>>> validate('/')
|
||||
'/'
|
||||
>>> with raises(MultipleInvalid, 'not a valid value'):
|
||||
... validate('/notavaliddir')
|
||||
>>> @truth
|
||||
... def isdir(v):
|
||||
... return os.path.isdir(v)
|
||||
>>> validate = Schema(isdir)
|
||||
>>> validate('/')
|
||||
'/'
|
||||
>>> with raises(MultipleInvalid, 'not a valid value'):
|
||||
... validate('/notavaliddir')
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
|
@ -97,7 +113,11 @@ class Coerce(object):
|
|||
... validate('foo')
|
||||
"""
|
||||
|
||||
def __init__(self, type, msg=None):
|
||||
def __init__(
|
||||
self,
|
||||
type: typing.Union[type, typing.Callable],
|
||||
msg: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
self.type = type
|
||||
self.msg = msg
|
||||
self.type_name = type.__name__
|
||||
|
@ -203,13 +223,15 @@ class _WithSubValidators(object):
|
|||
sub-validators are compiled by the parent `Schema`.
|
||||
"""
|
||||
|
||||
def __init__(self, *validators, **kwargs):
|
||||
def __init__(
|
||||
self, *validators, msg=None, required=False, discriminant=None, **kwargs
|
||||
) -> None:
|
||||
self.validators = validators
|
||||
self.msg = kwargs.pop('msg', None)
|
||||
self.required = kwargs.pop('required', False)
|
||||
self.discriminant = kwargs.pop('discriminant', None)
|
||||
self.msg = msg
|
||||
self.required = required
|
||||
self.discriminant = discriminant
|
||||
|
||||
def __voluptuous_compile__(self, schema):
|
||||
def __voluptuous_compile__(self, schema: Schema) -> typing.Callable:
|
||||
self._compiled = []
|
||||
old_required = schema.required
|
||||
self.schema = schema
|
||||
|
@ -219,7 +241,7 @@ class _WithSubValidators(object):
|
|||
schema.required = old_required
|
||||
return self._run
|
||||
|
||||
def _run(self, path, value):
|
||||
def _run(self, path: typing.List[typing.Hashable], value):
|
||||
if self.discriminant is not None:
|
||||
self._compiled = [
|
||||
self.schema._compile(v)
|
||||
|
@ -235,9 +257,17 @@ class _WithSubValidators(object):
|
|||
return '%s(%s, msg=%r)' % (
|
||||
self.__class__.__name__,
|
||||
", ".join(repr(v) for v in self.validators),
|
||||
self.msg
|
||||
self.msg,
|
||||
)
|
||||
|
||||
def _exec(
|
||||
self,
|
||||
funcs: typing.Iterable,
|
||||
v,
|
||||
path: typing.Optional[typing.List[typing.Hashable]] = None,
|
||||
):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Any(_WithSubValidators):
|
||||
"""Use the first validated value.
|
||||
|
@ -277,10 +307,8 @@ class Any(_WithSubValidators):
|
|||
error = e
|
||||
else:
|
||||
if error:
|
||||
raise error if self.msg is None else AnyInvalid(
|
||||
self.msg, path=path)
|
||||
raise AnyInvalid(self.msg or 'no valid value found',
|
||||
path=path)
|
||||
raise error if self.msg is None else AnyInvalid(self.msg, path=path)
|
||||
raise AnyInvalid(self.msg or 'no valid value found', path=path)
|
||||
|
||||
|
||||
# Convenience alias
|
||||
|
@ -321,10 +349,8 @@ class Union(_WithSubValidators):
|
|||
error = e
|
||||
else:
|
||||
if error:
|
||||
raise error if self.msg is None else AnyInvalid(
|
||||
self.msg, path=path)
|
||||
raise AnyInvalid(self.msg or 'no valid value found',
|
||||
path=path)
|
||||
raise error if self.msg is None else AnyInvalid(self.msg, path=path)
|
||||
raise AnyInvalid(self.msg or 'no valid value found', path=path)
|
||||
|
||||
|
||||
# Convenience alias
|
||||
|
@ -379,7 +405,9 @@ class Match(object):
|
|||
'0x123ef4'
|
||||
"""
|
||||
|
||||
def __init__(self, pattern, msg=None):
|
||||
def __init__(
|
||||
self, pattern: typing.Union[re.Pattern, str], msg: typing.Optional[str] = None
|
||||
) -> None:
|
||||
if isinstance(pattern, basestring):
|
||||
pattern = re.compile(pattern)
|
||||
self.pattern = pattern
|
||||
|
@ -391,7 +419,10 @@ class Match(object):
|
|||
except TypeError:
|
||||
raise MatchInvalid("expected string or buffer")
|
||||
if not match:
|
||||
raise MatchInvalid(self.msg or 'does not match regular expression {}'.format(self.pattern.pattern))
|
||||
raise MatchInvalid(
|
||||
self.msg
|
||||
or 'does not match regular expression {}'.format(self.pattern.pattern)
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -407,7 +438,12 @@ class Replace(object):
|
|||
'I say goodbye'
|
||||
"""
|
||||
|
||||
def __init__(self, pattern, substitution, msg=None):
|
||||
def __init__(
|
||||
self,
|
||||
pattern: typing.Union[re.Pattern, str],
|
||||
substitution: str,
|
||||
msg: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
if isinstance(pattern, basestring):
|
||||
pattern = re.compile(pattern)
|
||||
self.pattern = pattern
|
||||
|
@ -418,12 +454,14 @@ class Replace(object):
|
|||
return self.pattern.sub(self.substitution, v)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Replace(%r, %r, msg=%r)' % (self.pattern.pattern,
|
||||
self.substitution,
|
||||
self.msg)
|
||||
return 'Replace(%r, %r, msg=%r)' % (
|
||||
self.pattern.pattern,
|
||||
self.substitution,
|
||||
self.msg,
|
||||
)
|
||||
|
||||
|
||||
def _url_validation(v):
|
||||
def _url_validation(v: str) -> urlparse.ParseResult:
|
||||
parsed = urlparse.urlparse(v)
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
raise UrlInvalid("must have a URL scheme and host")
|
||||
|
@ -556,7 +594,7 @@ def PathExists(v):
|
|||
raise PathInvalid("Not a Path")
|
||||
|
||||
|
||||
def Maybe(validator, msg=None):
|
||||
def Maybe(validator: typing.Callable, msg: typing.Optional[str] = None):
|
||||
"""Validate that the object matches given validator or is None.
|
||||
|
||||
:raises Invalid: If the value does not match the given validator and is not
|
||||
|
@ -572,6 +610,9 @@ def Maybe(validator, msg=None):
|
|||
return Any(None, validator, msg=msg)
|
||||
|
||||
|
||||
NullableNumber = typing.Union[int, float, None]
|
||||
|
||||
|
||||
class Range(object):
|
||||
"""Limit a value to a range.
|
||||
|
||||
|
@ -593,8 +634,14 @@ class Range(object):
|
|||
... Schema(Range(max=10, max_included=False))(20)
|
||||
"""
|
||||
|
||||
def __init__(self, min=None, max=None, min_included=True,
|
||||
max_included=True, msg=None):
|
||||
def __init__(
|
||||
self,
|
||||
min: NullableNumber = None,
|
||||
max: NullableNumber = None,
|
||||
min_included: bool = True,
|
||||
max_included: bool = True,
|
||||
msg: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.min_included = min_included
|
||||
|
@ -606,33 +653,40 @@ class Range(object):
|
|||
if self.min_included:
|
||||
if self.min is not None and not v >= self.min:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'value must be at least %s' % self.min)
|
||||
self.msg or 'value must be at least %s' % self.min
|
||||
)
|
||||
else:
|
||||
if self.min is not None and not v > self.min:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'value must be higher than %s' % self.min)
|
||||
self.msg or 'value must be higher than %s' % self.min
|
||||
)
|
||||
if self.max_included:
|
||||
if self.max is not None and not v <= self.max:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'value must be at most %s' % self.max)
|
||||
self.msg or 'value must be at most %s' % self.max
|
||||
)
|
||||
else:
|
||||
if self.max is not None and not v < self.max:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'value must be lower than %s' % self.max)
|
||||
self.msg or 'value must be lower than %s' % self.max
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
# Objects that lack a partial ordering, e.g. None or strings will raise TypeError
|
||||
except TypeError:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'invalid value or type (must have a partial ordering)')
|
||||
self.msg or 'invalid value or type (must have a partial ordering)'
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return ('Range(min=%r, max=%r, min_included=%r,'
|
||||
' max_included=%r, msg=%r)' % (self.min, self.max,
|
||||
self.min_included,
|
||||
self.max_included,
|
||||
self.msg))
|
||||
return 'Range(min=%r, max=%r, min_included=%r, max_included=%r, msg=%r)' % (
|
||||
self.min,
|
||||
self.max,
|
||||
self.min_included,
|
||||
self.max_included,
|
||||
self.msg,
|
||||
)
|
||||
|
||||
|
||||
class Clamp(object):
|
||||
|
@ -649,7 +703,12 @@ class Clamp(object):
|
|||
0
|
||||
"""
|
||||
|
||||
def __init__(self, min=None, max=None, msg=None):
|
||||
def __init__(
|
||||
self,
|
||||
min: NullableNumber = None,
|
||||
max: NullableNumber = None,
|
||||
msg: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.msg = msg
|
||||
|
@ -665,7 +724,8 @@ class Clamp(object):
|
|||
# Objects that lack a partial ordering, e.g. None or strings will raise TypeError
|
||||
except TypeError:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'invalid value or type (must have a partial ordering)')
|
||||
self.msg or 'invalid value or type (must have a partial ordering)'
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Clamp(min=%s, max=%s)' % (self.min, self.max)
|
||||
|
@ -674,7 +734,12 @@ class Clamp(object):
|
|||
class Length(object):
|
||||
"""The length of a value must be in a certain range."""
|
||||
|
||||
def __init__(self, min=None, max=None, msg=None):
|
||||
def __init__(
|
||||
self,
|
||||
min: NullableNumber = None,
|
||||
max: NullableNumber = None,
|
||||
msg: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.msg = msg
|
||||
|
@ -683,16 +748,17 @@ class Length(object):
|
|||
try:
|
||||
if self.min is not None and len(v) < self.min:
|
||||
raise LengthInvalid(
|
||||
self.msg or 'length of value must be at least %s' % self.min)
|
||||
self.msg or 'length of value must be at least %s' % self.min
|
||||
)
|
||||
if self.max is not None and len(v) > self.max:
|
||||
raise LengthInvalid(
|
||||
self.msg or 'length of value must be at most %s' % self.max)
|
||||
self.msg or 'length of value must be at most %s' % self.max
|
||||
)
|
||||
return v
|
||||
|
||||
# Objects that havbe no length e.g. None or strings will raise TypeError
|
||||
# Objects that have no length e.g. None or strings will raise TypeError
|
||||
except TypeError:
|
||||
raise RangeInvalid(
|
||||
self.msg or 'invalid value or type')
|
||||
raise RangeInvalid(self.msg or 'invalid value or type')
|
||||
|
||||
def __repr__(self):
|
||||
return 'Length(min=%s, max=%s)' % (self.min, self.max)
|
||||
|
@ -703,7 +769,9 @@ class Datetime(object):
|
|||
|
||||
DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
|
||||
|
||||
def __init__(self, format=None, msg=None):
|
||||
def __init__(
|
||||
self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None
|
||||
) -> None:
|
||||
self.format = format or self.DEFAULT_FORMAT
|
||||
self.msg = msg
|
||||
|
||||
|
@ -712,8 +780,8 @@ class Datetime(object):
|
|||
datetime.datetime.strptime(v, self.format)
|
||||
except (TypeError, ValueError):
|
||||
raise DatetimeInvalid(
|
||||
self.msg or 'value does not match'
|
||||
' expected format %s' % self.format)
|
||||
self.msg or 'value does not match expected format %s' % self.format
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -730,8 +798,8 @@ class Date(Datetime):
|
|||
datetime.datetime.strptime(v, self.format)
|
||||
except (TypeError, ValueError):
|
||||
raise DateInvalid(
|
||||
self.msg or 'value does not match'
|
||||
' expected format %s' % self.format)
|
||||
self.msg or 'value does not match expected format %s' % self.format
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -741,7 +809,9 @@ class Date(Datetime):
|
|||
class In(object):
|
||||
"""Validate that a value is in a collection."""
|
||||
|
||||
def __init__(self, container, msg=None):
|
||||
def __init__(
|
||||
self, container: typing.Iterable, msg: typing.Optional[str] = None
|
||||
) -> None:
|
||||
self.container = container
|
||||
self.msg = msg
|
||||
|
||||
|
@ -751,8 +821,15 @@ class In(object):
|
|||
except TypeError:
|
||||
check = True
|
||||
if check:
|
||||
raise InInvalid(self.msg
|
||||
or 'value must be one of {}'.format(sorted(self.container)))
|
||||
try:
|
||||
raise InInvalid(
|
||||
self.msg or f'value must be one of {sorted(self.container)}'
|
||||
)
|
||||
except TypeError:
|
||||
raise InInvalid(
|
||||
self.msg
|
||||
or f'value must be one of {sorted(self.container, key=str)}'
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -762,7 +839,9 @@ class In(object):
|
|||
class NotIn(object):
|
||||
"""Validate that a value is not in a collection."""
|
||||
|
||||
def __init__(self, container, msg=None):
|
||||
def __init__(
|
||||
self, container: typing.Iterable, msg: typing.Optional[str] = None
|
||||
) -> None:
|
||||
self.container = container
|
||||
self.msg = msg
|
||||
|
||||
|
@ -772,8 +851,15 @@ class NotIn(object):
|
|||
except TypeError:
|
||||
check = True
|
||||
if check:
|
||||
raise NotInInvalid(self.msg
|
||||
or 'value must not be one of {}'.format(sorted(self.container)))
|
||||
try:
|
||||
raise NotInInvalid(
|
||||
self.msg or f'value must not be one of {sorted(self.container)}'
|
||||
)
|
||||
except TypeError:
|
||||
raise NotInInvalid(
|
||||
self.msg
|
||||
or f'value must not be one of {sorted(self.container, key=str)}'
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -790,7 +876,7 @@ class Contains(object):
|
|||
... s([3, 2])
|
||||
"""
|
||||
|
||||
def __init__(self, item, msg=None):
|
||||
def __init__(self, item, msg: typing.Optional[str] = None) -> None:
|
||||
self.item = item
|
||||
self.msg = msg
|
||||
|
||||
|
@ -823,9 +909,14 @@ class ExactSequence(object):
|
|||
('hourly_report', 10, [], [])
|
||||
"""
|
||||
|
||||
def __init__(self, validators, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
validators: typing.Iterable[Schemable],
|
||||
msg: typing.Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.validators = validators
|
||||
self.msg = kwargs.pop('msg', None)
|
||||
self.msg = msg
|
||||
self._schemas = [Schema(val, **kwargs) for val in validators]
|
||||
|
||||
def __call__(self, v):
|
||||
|
@ -838,14 +929,13 @@ class ExactSequence(object):
|
|||
return v
|
||||
|
||||
def __repr__(self):
|
||||
return 'ExactSequence([%s])' % (", ".join(repr(v)
|
||||
for v in self.validators))
|
||||
return 'ExactSequence([%s])' % ", ".join(repr(v) for v in self.validators)
|
||||
|
||||
|
||||
class Unique(object):
|
||||
"""Ensure an iterable does not contain duplicate items.
|
||||
|
||||
Only iterables convertable to a set are supported (native types and
|
||||
Only iterables convertible to a set are supported (native types and
|
||||
objects with correct __eq__).
|
||||
|
||||
JSON does not support set, so they need to be presented as arrays.
|
||||
|
@ -868,20 +958,18 @@ class Unique(object):
|
|||
... s('aabbc')
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None):
|
||||
def __init__(self, msg: typing.Optional[str] = None) -> None:
|
||||
self.msg = msg
|
||||
|
||||
def __call__(self, v):
|
||||
try:
|
||||
set_v = set(v)
|
||||
except TypeError as e:
|
||||
raise TypeInvalid(
|
||||
self.msg or 'contains unhashable elements: {0}'.format(e))
|
||||
raise TypeInvalid(self.msg or 'contains unhashable elements: {0}'.format(e))
|
||||
if len(set_v) != len(v):
|
||||
seen = set()
|
||||
dupes = list(set(x for x in v if x in seen or seen.add(x)))
|
||||
raise Invalid(
|
||||
self.msg or 'contains duplicate items: {0}'.format(dupes))
|
||||
raise Invalid(self.msg or 'contains duplicate items: {0}'.format(dupes))
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -904,13 +992,16 @@ class Equal(object):
|
|||
... s('foo')
|
||||
"""
|
||||
|
||||
def __init__(self, target, msg=None):
|
||||
def __init__(self, target, msg: typing.Optional[str] = None) -> None:
|
||||
self.target = target
|
||||
self.msg = msg
|
||||
|
||||
def __call__(self, v):
|
||||
if v != self.target:
|
||||
raise Invalid(self.msg or 'Values are not equal: value:{} != target:{}'.format(v, self.target))
|
||||
raise Invalid(
|
||||
self.msg
|
||||
or 'Values are not equal: value:{} != target:{}'.format(v, self.target)
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -932,7 +1023,12 @@ class Unordered(object):
|
|||
[1, 'foo']
|
||||
"""
|
||||
|
||||
def __init__(self, validators, msg=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
validators: typing.Iterable[Schemable],
|
||||
msg: typing.Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.validators = validators
|
||||
self.msg = msg
|
||||
self._schemas = [Schema(val, **kwargs) for val in validators]
|
||||
|
@ -942,7 +1038,12 @@ class Unordered(object):
|
|||
raise Invalid(self.msg or 'Value {} is not sequence!'.format(v))
|
||||
|
||||
if len(v) != len(self._schemas):
|
||||
raise Invalid(self.msg or 'List lengths differ, value:{} != target:{}'.format(len(v), len(self._schemas)))
|
||||
raise Invalid(
|
||||
self.msg
|
||||
or 'List lengths differ, value:{} != target:{}'.format(
|
||||
len(v), len(self._schemas)
|
||||
)
|
||||
)
|
||||
|
||||
consumed = set()
|
||||
missing = []
|
||||
|
@ -964,10 +1065,24 @@ class Unordered(object):
|
|||
|
||||
if len(missing) == 1:
|
||||
el = missing[0]
|
||||
raise Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format(el[0], el[1]))
|
||||
raise Invalid(
|
||||
self.msg
|
||||
or 'Element #{} ({}) is not valid against any validator'.format(
|
||||
el[0], el[1]
|
||||
)
|
||||
)
|
||||
elif missing:
|
||||
raise MultipleInvalid([Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format(
|
||||
el[0], el[1])) for el in missing])
|
||||
raise MultipleInvalid(
|
||||
[
|
||||
Invalid(
|
||||
self.msg
|
||||
or 'Element #{} ({}) is not valid against any validator'.format(
|
||||
el[0], el[1]
|
||||
)
|
||||
)
|
||||
for el in missing
|
||||
]
|
||||
)
|
||||
return v
|
||||
|
||||
def __repr__(self):
|
||||
|
@ -989,7 +1104,13 @@ class Number(object):
|
|||
Decimal('1234.01')
|
||||
"""
|
||||
|
||||
def __init__(self, precision=None, scale=None, msg=None, yield_decimal=False):
|
||||
def __init__(
|
||||
self,
|
||||
precision: typing.Optional[int] = None,
|
||||
scale: typing.Optional[int] = None,
|
||||
msg: typing.Optional[str] = None,
|
||||
yield_decimal: bool = False,
|
||||
) -> None:
|
||||
self.precision = precision
|
||||
self.scale = scale
|
||||
self.msg = msg
|
||||
|
@ -1002,13 +1123,22 @@ class Number(object):
|
|||
"""
|
||||
precision, scale, decimal_num = self._get_precision_scale(v)
|
||||
|
||||
if self.precision is not None and self.scale is not None and precision != self.precision\
|
||||
and scale != self.scale:
|
||||
raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" % (self.precision,
|
||||
self.scale))
|
||||
if (
|
||||
self.precision is not None
|
||||
and self.scale is not None
|
||||
and precision != self.precision
|
||||
and scale != self.scale
|
||||
):
|
||||
raise Invalid(
|
||||
self.msg
|
||||
or "Precision must be equal to %s, and Scale must be equal to %s"
|
||||
% (self.precision, self.scale)
|
||||
)
|
||||
else:
|
||||
if self.precision is not None and precision != self.precision:
|
||||
raise Invalid(self.msg or "Precision must be equal to %s" % self.precision)
|
||||
raise Invalid(
|
||||
self.msg or "Precision must be equal to %s" % self.precision
|
||||
)
|
||||
|
||||
if self.scale is not None and scale != self.scale:
|
||||
raise Invalid(self.msg or "Scale must be equal to %s" % self.scale)
|
||||
|
@ -1019,9 +1149,13 @@ class Number(object):
|
|||
return v
|
||||
|
||||
def __repr__(self):
|
||||
return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg))
|
||||
return 'Number(precision=%s, scale=%s, msg=%s)' % (
|
||||
self.precision,
|
||||
self.scale,
|
||||
self.msg,
|
||||
)
|
||||
|
||||
def _get_precision_scale(self, number):
|
||||
def _get_precision_scale(self, number) -> typing.Tuple[int, int, Decimal]:
|
||||
"""
|
||||
:param number:
|
||||
:return: tuple(precision, scale, decimal_number)
|
||||
|
@ -1031,7 +1165,13 @@ class Number(object):
|
|||
except InvalidOperation:
|
||||
raise Invalid(self.msg or 'Value must be a number enclosed with string')
|
||||
|
||||
return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num)
|
||||
exp = decimal_num.as_tuple().exponent
|
||||
if isinstance(exp, int):
|
||||
return (len(decimal_num.as_tuple().digits), -exp, decimal_num)
|
||||
else:
|
||||
# TODO: handle infinity and NaN
|
||||
# raise Invalid(self.msg or 'Value has no precision')
|
||||
raise TypeError("infinity and NaN have no precision")
|
||||
|
||||
|
||||
class SomeOf(_WithSubValidators):
|
||||
|
@ -1058,9 +1198,17 @@ class SomeOf(_WithSubValidators):
|
|||
... validate(6.2)
|
||||
"""
|
||||
|
||||
def __init__(self, validators, min_valid=None, max_valid=None, **kwargs):
|
||||
assert min_valid is not None or max_valid is not None, \
|
||||
'when using "%s" you should specify at least one of min_valid and max_valid' % (type(self).__name__,)
|
||||
def __init__(
|
||||
self,
|
||||
validators: typing.List[Schemable],
|
||||
min_valid: typing.Optional[int] = None,
|
||||
max_valid: typing.Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
assert min_valid is not None or max_valid is not None, (
|
||||
'when using "%s" you should specify at least one of min_valid and max_valid'
|
||||
% (type(self).__name__,)
|
||||
)
|
||||
self.min_valid = min_valid or 0
|
||||
self.max_valid = max_valid or len(validators)
|
||||
super(SomeOf, self).__init__(*validators, **kwargs)
|
||||
|
@ -1091,4 +1239,8 @@ class SomeOf(_WithSubValidators):
|
|||
|
||||
def __repr__(self):
|
||||
return 'SomeOf(min_valid=%s, validators=[%s], max_valid=%s, msg=%r)' % (
|
||||
self.min_valid, ", ".join(repr(v) for v in self.validators), self.max_valid, self.msg)
|
||||
self.min_valid,
|
||||
", ".join(repr(v) for v in self.validators),
|
||||
self.max_valid,
|
||||
self.msg,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue