././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706959351.2507324 voluptuous-0.14.2/0000755000175000017500000000000014557420767013460 5ustar00philipphilip././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/CHANGELOG.md0000644000175000017500000002723614557417777015312 0ustar00philipphilip# 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**: - [#439](https://github.com/alecthomas/voluptuous/pull/454): Ignore `Enum` if it is unavailable - [#456](https://github.com/alecthomas/voluptuous/pull/456): Fix email regex match for Python 2.7 **New**: - [#457](https://github.com/alecthomas/voluptuous/pull/457): Enable github actions - [#462](https://github.com/alecthomas/voluptuous/pull/462): Convert codebase to adhere to `flake8` W504 (PEP 8) - [#459](https://github.com/alecthomas/voluptuous/pull/459): Enable `flake8` in github actions - [#464](https://github.com/alecthomas/voluptuous/pull/464): `pytest` migration + enable Python 3.10 ## [0.13.0] **Changes**: - [#450](https://github.com/alecthomas/voluptuous/pull/450): Display valid `Enum` values in `Coerce` ## [0.12.2] **Fixes**: - [#439](https://github.com/alecthomas/voluptuous/issues/439): Revert Breaking `Maybe` change in 0.12.1 - [#447](https://github.com/alecthomas/voluptuous/issues/447): Fix Email Regex to not match on extra characters ## [0.12.1] **Changes**: - [#435](https://github.com/alecthomas/voluptuous/pull/435): Extended a few tests (`Required` and `In`) - [#425](https://github.com/alecthomas/voluptuous/pull/425): Improve error message for `In` and `NotIn` - [#436](https://github.com/alecthomas/voluptuous/pull/436): Add sorted() for `In` and `NotIn` + fix tests - [#437](https://github.com/alecthomas/voluptuous/pull/437): Grouped `Maybe` tests plus added another `Range` test - [#438](https://github.com/alecthomas/voluptuous/pull/438): Extend tests for `Schema` with empty list or dict **New**: - [#433](https://github.com/alecthomas/voluptuous/pull/433): Add Python 3.9 support **Fixes**: - [#431](https://github.com/alecthomas/voluptuous/pull/431): Fixed typos + made spelling more consistent - [#411](https://github.com/alecthomas/voluptuous/pull/411): Ensure `Maybe` propagates error information - [#434](https://github.com/alecthomas/voluptuous/pull/434): Remove value enumeration when validating empty list ## [0.12.0] **Changes**: - n/a **New**: - [#368](https://github.com/alecthomas/voluptuous/pull/368): Allow a discriminant field in validators **Fixes**: - [#420](https://github.com/alecthomas/voluptuous/pull/420): Fixed issue with 'required' not being set properly and added test - [#414](https://github.com/alecthomas/voluptuous/pull/414): Handle incomparable values in Range - [#427](https://github.com/alecthomas/voluptuous/pull/427): Added additional tests for Range, Clamp and Length + catch TypeError exceptions ## [0.11.7] **Changes**: - [#378](https://github.com/alecthomas/voluptuous/pull/378): Allow `extend()` of a `Schema` to return a subclass of a `Schema` as well. **New**: - [#364](https://github.com/alecthomas/voluptuous/pull/364): Accept `description` for `Inclusive` instances. - [#373](https://github.com/alecthomas/voluptuous/pull/373): Accept `msg` for `Maybe` instances. - [#382](https://github.com/alecthomas/voluptuous/pull/382): Added support for default values in `Inclusive` instances. **Fixes**: - [#371](https://github.com/alecthomas/voluptuous/pull/371): Fixed `DeprecationWarning` related to `collections.Mapping`. - [#377](https://github.com/alecthomas/voluptuous/pull/377): Preserve Unicode strings when passed to utility functions (e.g., `Lower()`, `Upper()`). - [#380](https://github.com/alecthomas/voluptuous/pull/380): Fixed regression with `Any` and `required` flag. ## [0.11.5] - Fixed issue with opening README file in `setup.py`. ## [0.11.4] - Removed use of `pypandoc` as Markdown is now supported by `setup()`. ## [0.11.3] and [0.11.2] **Changes**: - [#349](https://github.com/alecthomas/voluptuous/pull/349): Support Python 3.7. - [#343](https://github.com/alecthomas/voluptuous/pull/343): Drop support for Python 3.3. **New**: - [#342](https://github.com/alecthomas/voluptuous/pull/342): Add support for sets and frozensets. **Fixes**: - [#332](https://github.com/alecthomas/voluptuous/pull/332): Fix Python 3.x compatibility for setup.py when `pypandoc` is installed. - [#348](https://github.com/alecthomas/voluptuous/pull/348): Include path in `AnyInvalid` errors. - [#351](https://github.com/alecthomas/voluptuous/pull/351): Fix `Date` behaviour when a custom format is specified. ## [0.11.1] and [0.11.0] **Changes**: - [#293](https://github.com/alecthomas/voluptuous/pull/293): Support Python 3.6. - [#294](https://github.com/alecthomas/voluptuous/pull/294): Drop support for Python 2.6, 3.1 and 3.2. - [#318](https://github.com/alecthomas/voluptuous/pull/318): Allow to use nested schema and allow any validator to be compiled. - [#324](https://github.com/alecthomas/voluptuous/pull/324): Default values MUST now pass validation just as any regular value. This is a backward incompatible change if a schema uses default values that don't pass validation against the specified schema. - [#328](https://github.com/alecthomas/voluptuous/pull/328): Modify `__lt__` in Marker class to allow comparison with non Marker objects, such as str and int. **New**: - [#307](https://github.com/alecthomas/voluptuous/pull/307): Add description field to `Marker` instances. - [#311](https://github.com/alecthomas/voluptuous/pull/311): Add `Schema.infer` method for basic schema inference. - [#314](https://github.com/alecthomas/voluptuous/pull/314): Add `SomeOf` validator. **Fixes**: - [#279](https://github.com/alecthomas/voluptuous/pull/279): Treat Python 2 old-style classes like types when validating. - [#280](https://github.com/alecthomas/voluptuous/pull/280): Make `IsDir()`, `IsFile()` and `PathExists()` consistent between different Python versions. - [#290](https://github.com/alecthomas/voluptuous/pull/290): Use absolute imports to avoid import conflicts. - [#291](https://github.com/alecthomas/voluptuous/pull/291): Fix `Coerce` validator to catch `decimal.InvalidOperation`. - [#298](https://github.com/alecthomas/voluptuous/pull/298): Make `Schema([])` usage consistent with `Schema({})`. - [#303](https://github.com/alecthomas/voluptuous/pull/303): Allow partial validation when using validate decorator. - [#316](https://github.com/alecthomas/voluptuous/pull/316): Make `Schema.__eq__` deterministic. - [#319](https://github.com/alecthomas/voluptuous/pull/319): Replace implementation of `Maybe(s)` with `Any(None, s)` to allow it to be compiled. ## [0.10.5] - [#278](https://github.com/alecthomas/voluptuous/pull/278): Unicode translation to python 2 issue fixed. ## [0.10.2] **Changes**: - [#195](https://github.com/alecthomas/voluptuous/pull/195): `Range` raises `RangeInvalid` when testing `math.nan`. - [#215](https://github.com/alecthomas/voluptuous/pull/215): `{}` and `[]` now always evaluate as is, instead of as any dict or any list. To specify a free-form list, use `list` instead of `[]`. To specify a free-form dict, use `dict` instead of `Schema({}, extra=ALLOW_EXTRA)`. - [#224](https://github.com/alecthomas/voluptuous/pull/224): Change the encoding of keys in error messages from Unicode to UTF-8. **New**: - [#185](https://github.com/alecthomas/voluptuous/pull/185): Add argument validation decorator. - [#199](https://github.com/alecthomas/voluptuous/pull/199): Add `Unordered`. - [#200](https://github.com/alecthomas/voluptuous/pull/200): Add `Equal`. - [#207](https://github.com/alecthomas/voluptuous/pull/207): Add `Number`. - [#210](https://github.com/alecthomas/voluptuous/pull/210): Add `Schema` equality check. - [#212](https://github.com/alecthomas/voluptuous/pull/212): Add `coveralls`. - [#227](https://github.com/alecthomas/voluptuous/pull/227): Improve `Marker` management in `Schema`. - [#232](https://github.com/alecthomas/voluptuous/pull/232): Add `Maybe`. - [#234](https://github.com/alecthomas/voluptuous/pull/234): Add `Date`. - [#236](https://github.com/alecthomas/voluptuous/pull/236), [#237](https://github.com/alecthomas/voluptuous/pull/237), and [#238](https://github.com/alecthomas/voluptuous/pull/238): Add script for updating `gh-pages`. - [#256](https://github.com/alecthomas/voluptuous/pull/256): Add support for `OrderedDict` validation. - [#258](https://github.com/alecthomas/voluptuous/pull/258): Add `Contains`. **Fixes**: - [#197](https://github.com/alecthomas/voluptuous/pull/197): `ExactSequence` checks sequences are the same length. - [#201](https://github.com/alecthomas/voluptuous/pull/201): Empty lists are evaluated as is. - [#205](https://github.com/alecthomas/voluptuous/pull/205): Filepath validators correctly handle `None`. - [#206](https://github.com/alecthomas/voluptuous/pull/206): Handle non-subscriptable types in `humanize_error`. - [#231](https://github.com/alecthomas/voluptuous/pull/231): Validate `namedtuple` as a `tuple`. - [#235](https://github.com/alecthomas/voluptuous/pull/235): Update docstring. - [#249](https://github.com/alecthomas/voluptuous/pull/249): Update documentation. - [#262](https://github.com/alecthomas/voluptuous/pull/262): Fix a performance issue of exponential complexity where all of the dict keys were matched against all keys in the schema. This resulted in O(n*m) complexity where n is the number of keys in the dict being validated and m is the number of keys in the schema. The fix ensures that each key in the dict is matched against the relevant schema keys only. It now works in O(n). - [#266](https://github.com/alecthomas/voluptuous/pull/266): Remove setuptools as a dependency. ## 0.9.3 (2016-08-03) Changelog not kept for 0.9.3 and earlier releases. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600547846.0 voluptuous-0.14.2/COPYING0000644000175000017500000000271613731466006014506 0ustar00philipphilipCopyright (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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/MANIFEST.in0000644000175000017500000000020014557417777015215 0ustar00philipphilipinclude *.md include COPYING include voluptuous/tests/*.py include voluptuous/tests/*.md include pyproject.toml include tox.ini ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706959351.2507324 voluptuous-0.14.2/PKG-INFO0000644000175000017500000005032314557420767014560 0ustar00philipphilipMetadata-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 [](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 ''.format(self) ... >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) >>> schema(Structure(q='one')) ``` ### Allow None values To allow value to be None as well, use Any: ```pycon >>> from voluptuous import Any >>> schema = Schema(Any(None, int)) >>> schema(None) >>> schema(5) 5 ``` ## Error reporting Validators must throw an `Invalid` exception if invalid data is passed to them. All other exceptions are treated as errors in the validator and will not be caught. Each `Invalid` exception has an associated `path` attribute representing the path in the data structure to our currently validating value, as well as an `error_message` attribute that contains the message of the original exception. This is especially useful when you want to catch `Invalid` exceptions and give some feedback to the user, for instance in the context of an HTTP API. ```pycon >>> def validate_email(email): ... """Validate email.""" ... if not "@" in email: ... raise Invalid("This email is invalid.") ... return email >>> schema = Schema({"email": validate_email}) >>> exc = None >>> try: ... schema({"email": "whatever"}) ... except MultipleInvalid as e: ... exc = e >>> str(exc) "This email is invalid. for dictionary value @ data['email']" >>> exc.path ['email'] >>> exc.msg 'This email is invalid.' >>> exc.error_message 'This email is invalid.' ``` The `path` attribute is used during error reporting, but also during matching to determine whether an error should be reported to the user or if the next match should be attempted. This is determined by comparing the depth of the path where the check is, to the depth of the path where the error occurred. If the error is more than one level deeper, it is reported. The upshot of this is that *matching is depth-first and fail-fast*. To illustrate this, here is an example schema: ```pycon >>> schema = Schema([[2, 3], 6]) ``` Each value in the top-level list is matched depth-first in-order. Given input data of `[[6]]`, the inner list will match the first element of the schema, but the literal `6` will not match any of the elements of that list. This error will be reported back to the user immediately. No backtracking is attempted: ```pycon >>> try: ... schema([[6]]) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) == "not a valid value @ data[0][0]" True ``` If we pass the data `[6]`, the `6` is not a list type and so will not recurse into the first element of the schema. Matching will continue on to the second element in the schema, and succeed: ```pycon >>> schema([6]) [6] ``` ## 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1687512869.0 voluptuous-0.14.2/README.md0000644000175000017500000004655214445263445014745 0ustar00philipphilip # 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 [](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 ''.format(self) ... >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) >>> schema(Structure(q='one')) ``` ### Allow None values To allow value to be None as well, use Any: ```pycon >>> from voluptuous import Any >>> schema = Schema(Any(None, int)) >>> schema(None) >>> schema(5) 5 ``` ## Error reporting Validators must throw an `Invalid` exception if invalid data is passed to them. All other exceptions are treated as errors in the validator and will not be caught. Each `Invalid` exception has an associated `path` attribute representing the path in the data structure to our currently validating value, as well as an `error_message` attribute that contains the message of the original exception. This is especially useful when you want to catch `Invalid` exceptions and give some feedback to the user, for instance in the context of an HTTP API. ```pycon >>> def validate_email(email): ... """Validate email.""" ... if not "@" in email: ... raise Invalid("This email is invalid.") ... return email >>> schema = Schema({"email": validate_email}) >>> exc = None >>> try: ... schema({"email": "whatever"}) ... except MultipleInvalid as e: ... exc = e >>> str(exc) "This email is invalid. for dictionary value @ data['email']" >>> exc.path ['email'] >>> exc.msg 'This email is invalid.' >>> exc.error_message 'This email is invalid.' ``` The `path` attribute is used during error reporting, but also during matching to determine whether an error should be reported to the user or if the next match should be attempted. This is determined by comparing the depth of the path where the check is, to the depth of the path where the error occurred. If the error is more than one level deeper, it is reported. The upshot of this is that *matching is depth-first and fail-fast*. To illustrate this, here is an example schema: ```pycon >>> schema = Schema([[2, 3], 6]) ``` Each value in the top-level list is matched depth-first in-order. Given input data of `[[6]]`, the inner list will match the first element of the schema, but the literal `6` will not match any of the elements of that list. This error will be reported back to the user immediately. No backtracking is attempted: ```pycon >>> try: ... schema([[6]]) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) == "not a valid value @ data[0][0]" True ``` If we pass the data `[6]`, the `6` is not a list type and so will not recurse into the first element of the schema. Matching will continue on to the second element in the schema, and succeed: ```pycon >>> schema([6]) [6] ``` ## 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/pyproject.toml0000644000175000017500000000055614557417777016411 0ustar00philipphilip[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" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706959351.2507324 voluptuous-0.14.2/setup.cfg0000644000175000017500000000004614557420767015301 0ustar00philipphilip[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706959205.0 voluptuous-0.14.2/setup.py0000644000175000017500000000241314557420545015164 0ustar00philipphilipimport io import sys from setuptools import setup sys.path.insert(0, '.') version = __import__('voluptuous').__version__ with io.open('README.md', encoding='utf-8') as f: long_description = f.read() setup( name='voluptuous', url='https://github.com/alecthomas/voluptuous', download_url='https://pypi.python.org/pypi/voluptuous', version=version, description='Python data validation library', long_description=long_description, long_description_content_type='text/markdown', 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 :: 3', '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'] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/tox.ini0000644000175000017500000000136214557417777015004 0ustar00philipphilip[tox] envlist = flake8,black,py38,py39,py310,py311,py312 [flake8] ; E501: line too long (X > 79 characters) ; 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] distribute = True sitepackages = False deps = pytest pytest-cov coverage>=3.0 commands = pytest \ --cov=voluptuous \ voluptuous/tests/ [testenv:flake8] deps = flake8 commands = flake8 --doctests setup.py voluptuous [testenv:mypy] deps = mypy pytest commands = mypy voluptuous [testenv:black] deps = black commands = black --check . [testenv:isort] deps = isort commands = isort --check . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706959351.2467322 voluptuous-0.14.2/voluptuous/0000755000175000017500000000000014557420767015725 5ustar00philipphilip././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/__init__.py0000644000175000017500000000036014557417777020044 0ustar00philipphilip# flake8: noqa # fmt: off from voluptuous.schema_builder import * from voluptuous.util import * from voluptuous.validators import * from voluptuous.error import * # isort: skip # fmt: on __version__ = '0.14.2' __author__ = 'alecthomas' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/error.py0000644000175000017500000001077614557417777017452 0ustar00philipphilip# fmt: off import typing # fmt: on class Error(Exception): """Base validation exception.""" class SchemaError(Error): """An error was encountered in the schema.""" class Invalid(Error): """The data was invalid. :attr msg: The error message. :attr path: The path to the error, as a list of keys in the source data. :attr error_message: The actual error message that was raised, as a string. """ def __init__( self, message: 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.error_type = error_type @property def msg(self) -> str: return self.args[0] @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: typing.List[typing.Hashable]) -> None: self._path = path + self.path class MultipleInvalid(Invalid): def __init__(self, errors: typing.Optional[typing.List[Invalid]] = None) -> None: self.errors = errors[:] if errors else [] def __repr__(self) -> str: return 'MultipleInvalid(%r)' % self.errors @property def msg(self) -> str: return self.errors[0].msg @property def path(self) -> typing.List[typing.Hashable]: return self.errors[0].path @property def error_message(self) -> str: return self.errors[0].error_message def add(self, error: Invalid) -> None: self.errors.append(error) def __str__(self) -> str: return str(self.errors[0]) def prepend(self, path: typing.List[typing.Hashable]) -> None: for error in self.errors: error.prepend(path) class RequiredFieldInvalid(Invalid): """Required field was missing.""" class ObjectInvalid(Invalid): """The value we found was not an object.""" class DictInvalid(Invalid): """The value found was not a dict.""" class ExclusiveInvalid(Invalid): """More than one value found in exclusion group.""" class InclusiveInvalid(Invalid): """Not all values found in inclusion group.""" class SequenceTypeInvalid(Invalid): """The type found is not a sequence type.""" class TypeInvalid(Invalid): """The value was not of required type.""" class ValueInvalid(Invalid): """The value was found invalid by evaluation function.""" class ContainsInvalid(Invalid): """List does not contain item""" class ScalarInvalid(Invalid): """Scalars did not match.""" class CoerceInvalid(Invalid): """Impossible to coerce value to type.""" class AnyInvalid(Invalid): """The value did not pass any validator.""" class AllInvalid(Invalid): """The value did not pass all validators.""" class MatchInvalid(Invalid): """The value does not match the given regular expression.""" class RangeInvalid(Invalid): """The value is not in given range.""" class TrueInvalid(Invalid): """The value is not True.""" class FalseInvalid(Invalid): """The value is not False.""" class BooleanInvalid(Invalid): """The value is not a boolean.""" class UrlInvalid(Invalid): """The value is not a URL.""" class EmailInvalid(Invalid): """The value is not an email address.""" class FileInvalid(Invalid): """The value is not a file.""" class DirInvalid(Invalid): """The value is not a directory.""" class PathInvalid(Invalid): """The value is not a path.""" class LiteralInvalid(Invalid): """The literal values do not match.""" class LengthInvalid(Invalid): pass class DatetimeInvalid(Invalid): """The value is not a formatted datetime string.""" class DateInvalid(Invalid): """The value is not a formatted date string.""" class InInvalid(Invalid): pass class NotInInvalid(Invalid): pass class ExactSequenceInvalid(Invalid): pass class NotEnoughValid(Invalid): """The value did not pass enough validations.""" pass class TooManyValid(Invalid): """The value passed more than expected validations.""" pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/humanize.py0000644000175000017500000000356114557417777020133 0ustar00philipphilip# 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: typing.Any, path: typing.List[typing.Hashable] ) -> typing.Optional[typing.Any]: for item_index in path: try: data = data[item_index] except (KeyError, IndexError, TypeError): # The index is not present in the dictionary, list or other # indexable or data is not subscriptable return None return data def humanize_error( data, validation_error: 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 ) ) else: offending_item_summary = repr(_nested_getitem(data, validation_error.path)) if len(offending_item_summary) > max_sub_error_length: offending_item_summary = ( offending_item_summary[: max_sub_error_length - 3] + '...' ) return '%s. Got %s' % (validation_error, offending_item_summary) def validate_with_humanized_errors( data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH ) -> typing.Any: try: return schema(data) except (Invalid, MultipleInvalid) as e: raise Error(humanize_error(data, e, max_sub_error_length)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1686692291.0 voluptuous-0.14.2/voluptuous/py.typed0000644000175000017500000000000014442160703017372 0ustar00philipphilip././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/schema_builder.py0000644000175000017500000013040114557417777021253 0ustar00philipphilip# fmt: off from __future__ import annotations import collections import inspect 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 # fmt: on """Schema validation for Python data structures. Given eg. a nested data structure like this: { 'exclude': ['Users', 'Uptime'], 'include': [], 'set': { 'snmp_community': 'public', 'snmp_timeout': 15, 'snmp_version': '2c', }, 'targets': { 'localhost': { 'exclude': ['Uptime'], 'features': { 'Uptime': { 'retries': 3, }, 'Users': { 'snmp_community': 'monkey', 'snmp_port': 15, }, }, 'include': ['Users'], 'set': { 'snmp_community': 'monkeys', }, }, }, } A schema like this: >>> settings = { ... 'snmp_community': str, ... 'retries': int, ... 'snmp_version': All(Coerce(str), Any('3', '2c', '1')), ... } >>> features = ['Ping', 'Uptime', 'Http'] >>> schema = Schema({ ... 'exclude': features, ... 'include': features, ... 'set': settings, ... 'targets': { ... 'exclude': features, ... 'include': features, ... 'features': { ... str: settings, ... }, ... }, ... }) Validate like so: >>> schema({ ... 'set': { ... 'snmp_community': 'public', ... 'snmp_version': '2c', ... }, ... 'targets': { ... 'exclude': ['Ping'], ... 'features': { ... 'Uptime': {'retries': 3}, ... 'Users': {'snmp_community': 'monkey'}, ... }, ... }, ... }) == { ... 'set': {'snmp_version': '2c', 'snmp_community': 'public'}, ... 'targets': { ... 'exclude': ['Ping'], ... 'features': {'Uptime': {'retries': 3}, ... 'Users': {'snmp_community': 'monkey'}}}} True """ # options for extra keys PREVENT_EXTRA = 0 # any extra key not in schema will raise an error ALLOW_EXTRA = 1 # extra keys not in schema will be included in output REMOVE_EXTRA = 2 # extra keys not in schema will be excluded from output def _isnamedtuple(obj): return isinstance(obj, tuple) and hasattr(obj, '_fields') class Undefined(object): def __nonzero__(self): return False def __repr__(self): return '...' UNDEFINED = Undefined() def Self() -> None: raise er.SchemaError('"Self" should never be called') 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: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None ) -> Generator[None, None, None]: try: yield except exc as e: if msg is not 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(_) -> None: """Allow keys in the data that are not present in the schema.""" raise er.SchemaError('"Extra" should never be called') # As extra() is never called there's no way to catch references to the # 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. The schema is a Python tree-like structure where nodes are pattern matched against corresponding trees of values. Nodes can be values, in which case a direct comparison is used, types, in which case an isinstance() check is performed, or callables, which will validate and optionally convert the value. We can equate schemas also. For Example: >>> v = Schema({Required('a'): str}) >>> v1 = Schema({Required('a'): str}) >>> v2 = Schema({Required('b'): str}) >>> assert v == v1 >>> assert v != v2 """ _extra_to_name = { REMOVE_EXTRA: 'REMOVE_EXTRA', ALLOW_EXTRA: 'ALLOW_EXTRA', PREVENT_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. :param required: Keys defined in the schema must be in the data. :param extra: Specify how extra keys in the data are treated: - :const:`~voluptuous.PREVENT_EXTRA`: to disallow any undefined extra keys (raise ``Invalid``). - :const:`~voluptuous.ALLOW_EXTRA`: to include undefined extra keys in the output. - :const:`~voluptuous.REMOVE_EXTRA`: to exclude undefined extra keys from the output. - Any value other than the above defaults to :const:`~voluptuous.PREVENT_EXTRA` """ self.schema = schema self.required = required self.extra = int(extra) # ensure the value is an integer self._compiled = self._compile(schema) @classmethod 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: { 'foo': 1, 'bar': { 'a': True, 'b': False }, 'baz': ['purple', 'monkey', 'dishwasher'] } And return a Schema: { 'foo': int, 'bar': { 'a': bool, 'b': bool }, 'baz': [str] } 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 value.items()} if isinstance(value, list): if len(value) == 0: return list else: return [value_to_schema_type(v) for v in value] return type(value) return cls(value_to_schema_type(data), **kwargs) def __eq__(self, other): if not isinstance(other, Schema): return False return other.schema == self.schema def __ne__(self, other): return not (self == other) def __str__(self): return str(self.schema) def __repr__(self): return "" % ( self.schema, self._extra_to_name.get(self.extra, '??'), self.required, id(self), ) def __call__(self, data): """Validate data against this schema.""" try: return self._compiled([], data) except er.MultipleInvalid: raise except er.Invalid as e: raise er.MultipleInvalid([e]) # return self.validate([], self.schema, data) def _compile(self, schema): if schema is Extra: return lambda _, v: v if schema is Self: return lambda p, v: self._compiled(p, v) elif hasattr(schema, "__voluptuous_compile__"): return schema.__voluptuous_compile__(self) if isinstance(schema, Object): return self._compile_object(schema) if isinstance(schema, collections.abc.Mapping): return self._compile_dict(schema) elif isinstance(schema, list): return self._compile_list(schema) elif isinstance(schema, tuple): return self._compile_tuple(schema) elif isinstance(schema, (frozenset, set)): return self._compile_set(schema) type_ = type(schema) if inspect.isclass(schema): type_ = 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__) 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) ) ) # Keys that may have defaults all_default_keys = set( key for key in schema if isinstance(key, Required) or isinstance(key, Optional) ) _compiled_schema = {} for skey, svalue in schema.items(): new_key = self._compile(skey) new_value = self._compile(svalue) _compiled_schema[skey] = (new_key, new_value) candidates = list(_iterate_mapping_candidates(_compiled_schema)) # After we have the list of candidates in the correct order, we want to apply some optimization so that each # key in the data being validated will be matched against the relevant schema keys only. # No point in matching against different keys additional_candidates = [] candidates_by_key = {} for skey, (ckey, cvalue) in candidates: 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)) ) else: # These are wildcards such as 'int', 'str', 'Remove' and others which should be applied to all keys additional_candidates.append((skey, (ckey, cvalue))) def validate_mapping(path, iterable, out): required_keys = all_required_keys.copy() # Build a map of all provided key-value pairs. # The type(out) is used to retain ordering in case a ordered # map type is provided as input. key_value_map = type(out)() for key, value in iterable: key_value_map[key] = value # 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 ): # A default value has been specified for this missing # key, insert it. key_value_map[key.schema] = key.default() 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 ) # 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) except er.Invalid as e: if len(e.path) > len(key_path): raise if not error or len(e.path) > len(error.path): error = e continue # Backtracking is not performed once a key is selected, so if # the value is invalid we immediately throw an exception. exception_errors = [] # check if the key is marked for removal is_remove = new_key is Remove try: cval = cvalue(key_path, value) # include if it's not marked for removal if not is_remove: out[new_key] = cval else: remove_key = True continue except er.MultipleInvalid as e: exception_errors.extend(e.errors) except er.Invalid as e: exception_errors.append(e) if exception_errors: if is_remove or remove_key: continue for err in exception_errors: if len(err.path) <= len(key_path): err.error_type = invalid_msg errors.append(err) # If there is a validation error for a required # key, this means that the key was provided. # Discard the required key so it does not # create an additional, noisy exception. required_keys.discard(skey) break # Key and value okay, mark as found in case it was # a Required() field. required_keys.discard(skey) break else: if error: errors.append(error) elif remove_key: # remove key continue elif self.extra == ALLOW_EXTRA: out[key] = value elif self.extra != REMOVE_EXTRA: errors.append(er.Invalid('extra keys not allowed', key_path)) # else REMOVE_EXTRA: ignore the key so it's removed from output # 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' ) errors.append(er.RequiredFieldInvalid(msg, path + [key])) if errors: raise er.MultipleInvalid(errors) return out return validate_mapping def _compile_object(self, schema): """Validate an object. Has the same behavior as dictionary validator but work with object attributes. For example: >>> class Structure(object): ... def __init__(self, one=None, three=None): ... self.one = one ... self.three = three ... >>> validate = Schema(Object({'one': 'two', 'three': 'four'}, cls=Structure)) >>> with raises(er.MultipleInvalid, "not a valid value for object value @ data['one']"): ... validate(Structure(one='three')) """ 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 = filter(lambda item: item[1] is not None, iterable) out = base_validate(path, iterable, {}) return type(data)(**out) return validate_object def _compile_dict(self, schema): """Validate a dictionary. A dictionary schema can contain a set of values, or at most one validator function/type. A dictionary schema will only validate a dictionary: >>> validate = Schema({}) >>> with raises(er.MultipleInvalid, 'expected a dictionary'): ... validate([]) An invalid dictionary value: >>> validate = Schema({'one': 'two', 'three': 'four'}) >>> with raises(er.MultipleInvalid, "not a valid value for dictionary value @ data['one']"): ... validate({'one': 'three'}) An invalid key: >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['two']"): ... validate({'two': 'three'}) Validation function, in this case the "int" type: >>> validate = Schema({'one': 'two', 'three': 'four', int: str}) Valid integer input: >>> validate({10: 'twenty'}) {10: 'twenty'} By default, a "type" in the schema (in this case "int") will be used purely to validate that the corresponding value is of that type. It will not Coerce the value: >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['10']"): ... validate({'10': 'twenty'}) Wrap them in the Coerce() function to achieve this: >>> from voluptuous import Coerce >>> validate = Schema({'one': 'two', 'three': 'four', ... Coerce(int): str}) >>> validate({'10': 'twenty'}) {10: 'twenty'} Custom message for required key >>> validate = Schema({Required('one', 'required'): 'two'}) >>> with raises(er.MultipleInvalid, "required @ data['one']"): ... validate({}) (This is to avoid unexpected surprises.) Multiple errors for nested field in a dict: >>> validate = Schema({ ... 'adict': { ... 'strfield': str, ... 'intfield': int ... } ... }) >>> try: ... validate({ ... 'adict': { ... 'strfield': 123, ... 'intfield': 'one' ... } ... }) ... except er.MultipleInvalid as e: ... print(sorted(str(i) for i in e.errors)) # doctest: +NORMALIZE_WHITESPACE ["expected int for dictionary value @ data['adict']['intfield']", "expected str for dictionary value @ data['adict']['strfield']"] """ base_validate = self._compile_mapping(schema, invalid_msg='dictionary value') groups_of_exclusion = {} groups_of_inclusion = {} for node in schema: if isinstance(node, Exclusive): g = groups_of_exclusion.setdefault(node.group_of_exclusion, []) g.append(node) elif isinstance(node, Inclusive): g = groups_of_inclusion.setdefault(node.group_of_inclusion, []) g.append(node) def validate_dict(path, data): if not isinstance(data, dict): raise er.DictInvalid('expected a dictionary', path) errors = [] for label, group in groups_of_exclusion.items(): exists = False 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 ) next_path = path + [VirtualPathComponent(label)] errors.append(er.ExclusiveInvalid(msg, next_path)) break exists = True if errors: raise er.MultipleInvalid(errors) 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 ) for g in group: if hasattr(g, 'msg') and g.msg: msg = g.msg break next_path = path + [VirtualPathComponent(label)] errors.append(er.InclusiveInvalid(msg, next_path)) break if errors: raise er.MultipleInvalid(errors) out = data.__class__() return base_validate(path, data.items(), out) return validate_dict def _compile_sequence(self, schema, seq_type): """Validate a sequence type. This is a sequence of valid values or validators tried in order. >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'): ... validator([3.5]) >>> validator([1]) [1] """ _compiled = [self._compile(s) for s in schema] seq_type_name = seq_type.__name__ def validate_sequence(path, data): if not isinstance(data, seq_type): raise er.SequenceTypeInvalid('expected a %s' % seq_type_name, path) # 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)] ) return data out = [] invalid = None errors = [] index_path = UNDEFINED for i, value in enumerate(data): index_path = path + [i] invalid = None for validate in _compiled: try: cval = validate(index_path, value) if cval is not Remove: # do not include Remove values out.append(cval) break except er.Invalid as e: if len(e.path) > len(index_path): raise invalid = e else: errors.append(invalid) if errors: raise er.MultipleInvalid(errors) if _isnamedtuple(data): return type(data)(*out) else: return type(data)(out) return validate_sequence def _compile_tuple(self, schema): """Validate a tuple. A tuple is a sequence of valid values or validators tried in order. >>> validator = Schema(('one', 'two', int)) >>> validator(('one',)) ('one',) >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'): ... validator((3.5,)) >>> validator((1,)) (1,) """ return self._compile_sequence(schema, tuple) def _compile_list(self, schema): """Validate a list. A list is a sequence of valid values or validators tried in order. >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'): ... validator([3.5]) >>> validator([1]) [1] """ return self._compile_sequence(schema, list) def _compile_set(self, schema): """Validate a set. A set is an unordered collection of unique elements. >>> validator = Schema({int}) >>> validator(set([42])) == set([42]) True >>> with raises(er.Invalid, 'expected a set'): ... validator(42) >>> with raises(er.MultipleInvalid, 'invalid value in set'): ... validator(set(['a'])) """ type_ = type(schema) type_name = type_.__name__ def validate_set(path, data): if not isinstance(data, type_): raise er.Invalid('expected a %s' % type_name, path) _compiled = [self._compile(s) for s in schema] errors = [] for value in data: for validate in _compiled: try: validate(path, value) break except er.Invalid: pass else: invalid = er.Invalid('invalid value in %s' % type_name, path) errors.append(invalid) if errors: raise er.MultipleInvalid(errors) return data return validate_set 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 resulting `Schema` inherits the `required` and `extra` parameters of this, unless overridden. Both schemas must be dictionary-based. :param schema: dictionary to extend this `Schema` with :param required: if set, overrides `required` of this `Schema` :param extra: if set, overrides `extra` of this `Schema` """ 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 # build a map that takes the key literals to the needed objects # literal -> Required|Optional|literal result_key_map = dict((key_literal(key), key) for key in result) # for each item in the extension schema, replace duplicates # or add new keys 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 isinstance(result_value, dict) and isinstance(value, dict): new_value = Schema(result_value).extend(value).schema del result[result_key] result[key] = new_value # one or the other or both are not sub-schemas, simple replacement is fine # remove old key and add new one else: del result[result_key] result[key] = value # key is new and can simply be added else: result[key] = value # 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 return result_cls(result, required=result_required, extra=result_extra) def _compile_scalar(schema): """A scalar value. The schema can either be a value or a type. >>> _compile_scalar(int)([], 1) 1 >>> with raises(er.Invalid, 'expected float'): ... _compile_scalar(float)([], '1') Callables have >>> _compile_scalar(lambda v: float(v))([], '1') 1.0 As a convenience, ValueError's are trapped: >>> with raises(er.Invalid, 'not a valid value'): ... _compile_scalar(lambda v: float(v))([], 'a') """ if inspect.isclass(schema): def validate_instance(path, data): if isinstance(data, schema): return data else: msg = 'expected %s' % schema.__name__ raise er.TypeInvalid(msg, path) return validate_instance if callable(schema): def validate_callable(path, data): try: return schema(data) except ValueError: raise er.ValueInvalid('not a valid value', path) except er.Invalid as e: e.prepend(path) raise return validate_callable def validate_value(path, data): if data != schema: raise er.ScalarInvalid('not a valid value', path) return data return validate_value def _compile_itemsort(): '''return sort function of mappings''' def is_extra(key_): return key_ is Extra def is_remove(key_): return isinstance(key_, Remove) def is_marker(key_): return isinstance(key_, Marker) def is_type(key_): return inspect.isclass(key_) def is_callable(key_): return callable(key_) # priority list for map sorting (in order of checking) # We want Extra to match last, because it's a catch-all. On the other hand, # 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 ] def item_priority(item_): key_ = item_[0] for i, check_ in priority: if check_(key_): return i # values have highest priorities return 0 return item_priority _sort_item = _compile_itemsort() def _iterate_mapping_candidates(schema): """Iterate over schema in a meaningful order.""" # 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(schema.items(), key=_sort_item) def _iterate_object(obj): """Return iterator over object attributes. Respect objects with defined __slots__. """ d = {} try: d = vars(obj) except TypeError: # maybe we have named tuple here? if hasattr(obj, '_asdict'): d = obj._asdict() for item in d.items(): yield item try: slots = obj.__slots__ except AttributeError: pass else: for key in slots: if key != '__dict__': yield (key, getattr(obj, key)) class Msg(object): """Report a user-friendly message if a schema fails to validate. >>> validate = Schema( ... Msg(['one', 'two', int], ... 'should be one of "one", "two" or an integer')) >>> with raises(er.MultipleInvalid, 'should be one of "one", "two" or an integer'): ... validate(['three']) Messages are only applied to invalid direct descendants of the schema: >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!')) >>> with raises(er.MultipleInvalid, 'expected int @ data[0][0]'): ... validate([['three']]) The type which is thrown can be overridden but needs to be a subclass of Invalid >>> with raises(er.SchemaError, 'Msg can only use subclases of Invalid as custom class'): ... validate = Schema(Msg([int], 'should be int', cls=KeyError)) If you do use a subclass of Invalid, that error will be thrown (wrapped in a MultipleInvalid) >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!', cls=er.RangeInvalid)) >>> try: ... validate(['three']) ... except er.MultipleInvalid as e: ... assert isinstance(e.errors[0], er.RangeInvalid) """ 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" ) self._schema = schema self.schema = Schema(schema) self.msg = msg self.cls = cls def __call__(self, v): try: return self.schema(v) except er.Invalid as e: if len(e.path) > 1: raise e else: raise (self.cls or er.Invalid)(self.msg) def __repr__(self): return 'Msg(%s, %s, cls=%s)' % (self._schema, self.msg, self.cls) class Object(dict): """Indicate that we should work with attributes, not keys.""" def __init__(self, schema: typing.Any, cls: object = UNDEFINED) -> None: self.cls = cls super(Object, self).__init__(schema) class VirtualPathComponent(str): def __str__(self): return '<' + self + '>' def __repr__(self): return self.__str__() class Marker(object): """Mark nodes for special treatment. `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 self.description = description def __call__(self, v): try: return self._schema(v) except er.Invalid as e: if not self.msg or len(e.path) > 1: raise raise er.Invalid(self.msg) def __str__(self): return str(self.schema) def __repr__(self): return repr(self.schema) def __lt__(self, other): if isinstance(other, Marker): return self.schema < other.schema return self.schema < other def __hash__(self): return hash(self.schema) def __eq__(self, other): return self.schema == other def __ne__(self, other): return not (self.schema == other) class Optional(Marker): """Mark a node in the schema as optional, and optionally provide a default >>> schema = Schema({Optional('key'): str}) >>> schema({}) {} >>> schema = Schema({Optional('key', default='value'): str}) >>> schema({}) {'key': 'value'} >>> schema = Schema({Optional('key', default=list): list}) >>> schema({}) {'key': []} If 'required' flag is set for an entire schema, optional keys aren't required >>> schema = Schema({ ... Optional('key'): str, ... 'key2': str ... }, required=True) >>> schema({'key2':'value'}) {'key2': 'value'} """ 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) class Exclusive(Optional): """Mark a node in the schema as exclusive. Exclusive keys inherited from Optional: >>> schema = Schema({Exclusive('alpha', 'angles'): int, Exclusive('beta', 'angles'): int}) >>> schema({'alpha': 30}) {'alpha': 30} Keys inside a same group of exclusion cannot be together, it only makes sense for dictionaries: >>> with raises(er.MultipleInvalid, "two or more values in the same group of exclusion 'angles' @ data[]"): ... schema({'alpha': 30, 'beta': 45}) For example, API can provides multiple types of authentication, but only one works in the same time: >>> msg = 'Please, use only one type of authentication at the same time.' >>> schema = Schema({ ... Exclusive('classic', 'auth', msg=msg):{ ... Required('email'): str, ... Required('password'): str ... }, ... Exclusive('internal', 'auth', msg=msg):{ ... Required('secret_key'): str ... }, ... Exclusive('social', 'auth', msg=msg):{ ... Required('social_network'): str, ... Required('token'): str ... } ... }) >>> with raises(er.MultipleInvalid, "Please, use only one type of authentication at the same time. @ data[]"): ... schema({'classic': {'email': 'foo@example.com', 'password': 'bar'}, ... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}}) """ 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. Inclusive keys inherited from Optional: >>> schema = Schema({ ... Inclusive('filename', 'file'): str, ... Inclusive('mimetype', 'file'): str ... }) >>> data = {'filename': 'dog.jpg', 'mimetype': 'image/jpeg'} >>> data == schema(data) True Keys inside a same group of inclusive must exist together, it only makes sense for dictionaries: >>> with raises(er.MultipleInvalid, "some but not all values in the same group of inclusion 'file' @ data[]"): ... schema({'filename': 'dog.jpg'}) If none of the keys in the group are present, it is accepted: >>> schema({}) {} For example, API can return 'height' and 'width' together, but not separately. >>> msg = "Height and width must exist together" >>> schema = Schema({ ... Inclusive('height', 'size', msg=msg): int, ... Inclusive('width', 'size', msg=msg): int ... }) >>> with raises(er.MultipleInvalid, msg + " @ data[]"): ... schema({'height': 100}) >>> with raises(er.MultipleInvalid, msg + " @ data[]"): ... schema({'width': 100}) >>> data = {'height': 100, 'width': 100} >>> data == schema(data) True """ 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 class Required(Marker): """Mark a node in the schema as being required, and optionally provide a default value. >>> schema = Schema({Required('key'): str}) >>> with raises(er.MultipleInvalid, "required key not provided @ data['key']"): ... schema({}) >>> schema = Schema({Required('key', default='value'): str}) >>> schema({}) {'key': 'value'} >>> schema = Schema({Required('key', default=list): list}) >>> schema({}) {'key': []} """ 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) class Remove(Marker): """Mark a node in the schema to be removed and excluded from the validated output. Keys that fail validation will not raise ``Invalid``. Instead, these keys will be treated as extras. >>> schema = Schema({str: int, Remove(int): str}) >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data[1]"): ... schema({'keep': 1, 1: 1.0}) >>> schema({1: 'red', 'red': 1, 2: 'green'}) {'red': 1} >>> schema = Schema([int, Remove(float), Extra]) >>> schema([1, 2, 3, 4.0, 5, 6.0, '7']) [1, 2, 3, 5, '7'] """ def __call__(self, schema: Schemable): super(Remove, self).__call__(schema) return self.__class__ def __repr__(self): return "Remove(%r)" % (self.schema,) def __hash__(self): return object.__hash__(self) 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: >>> @message('not an integer') ... def isint(v): ... return int(v) >>> validate = Schema(isint()) >>> with raises(er.MultipleInvalid, 'not an integer'): ... validate('a') The message can be overridden on a per validator basis: >>> validate = Schema(isint('bad')) >>> with raises(er.MultipleInvalid, 'bad'): ... validate('a') The class thrown too: >>> class IntegerInvalid(er.Invalid): pass >>> validate = Schema(isint('bad', clsoverride=IntegerInvalid)) >>> try: ... validate('a') ... except er.MultipleInvalid as e: ... 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" ) def decorator(f): @wraps(f) def check(msg=None, clsoverride=None): @wraps(f) def wrapper(*args, **kwargs): try: return f(*args, **kwargs) except ValueError: raise (clsoverride or cls or er.ValueInvalid)( msg or default or 'invalid value' ) return wrapper return check return decorator def _args_to_dict(func, args): """Returns argument names as values as key-value pairs.""" if sys.version_info >= (3, 0): arg_count = func.__code__.co_argcount arg_names = func.__code__.co_varnames[:arg_count] else: arg_count = func.func_code.co_argcount 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) ) return arguments def _merge_args_with_kwargs(args_dict, kwargs_dict): """Merge args with kwargs.""" ret = args_dict.copy() ret.update(kwargs_dict) return ret def validate(*a, **kw) -> typing.Callable: """Decorator for validating arguments of a function against a given schema. Set restrictions for arguments: >>> @validate(arg1=int, arg2=int) ... def foo(arg1, arg2): ... return arg1 * arg2 Set restriction for returned value: >>> @validate(arg=int, __return__=int) ... def bar(arg1): ... return arg1 * 2 """ RETURNS_KEY = '__return__' def validate_schema_decorator(func): returns_defined = False returns = None schema_args_dict = _args_to_dict(func, a) schema_arguments = _merge_args_with_kwargs(schema_args_dict, kw) if RETURNS_KEY in schema_arguments: returns_defined = True 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 ) output_schema = Schema(returns) if returns_defined else lambda x: x @wraps(func) def func_wrapper(*args, **kwargs): args_dict = _args_to_dict(func, args) arguments = _merge_args_with_kwargs(args_dict, kwargs) validated_arguments = input_schema(arguments) output = func(**validated_arguments) return output_schema(output) return func_wrapper return validate_schema_decorator ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706959351.2507324 voluptuous-0.14.2/voluptuous/tests/0000755000175000017500000000000014557420767017067 5ustar00philipphilip././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600547846.0 voluptuous-0.14.2/voluptuous/tests/__init__.py0000644000175000017500000000003613731466006021164 0ustar00philipphilip__author__ = 'tusharmakkar08' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1600547846.0 voluptuous-0.14.2/voluptuous/tests/tests.md0000644000175000017500000002032213731466006020537 0ustar00philipphilipError reporting should be accurate: >>> from voluptuous import * >>> schema = Schema(['one', {'two': 'three', 'four': ['five'], ... 'six': {'seven': 'eight'}}]) >>> schema(['one']) ['one'] >>> schema([{'two': 'three'}]) [{'two': 'three'}] It should show the exact index and container type, in this case a list value: >>> try: ... schema(['one', 'two']) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) == 'expected a dictionary @ data[1]' True It should also be accurate for nested values: >>> try: ... schema([{'two': 'nine'}]) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) "not a valid value for dictionary value @ data[0]['two']" >>> try: ... schema([{'four': ['nine']}]) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) "not a valid value @ data[0]['four'][0]" >>> try: ... schema([{'six': {'seven': 'nine'}}]) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) "not a valid value for dictionary value @ data[0]['six']['seven']" Errors should be reported depth-first: >>> validate = Schema({'one': {'two': 'three', 'four': 'five'}}) >>> try: ... validate({'one': {'four': 'six'}}) ... except Invalid as e: ... print(e) ... print(e.path) not a valid value for dictionary value @ data['one']['four'] ['one', 'four'] Voluptuous supports validation when extra fields are present in the data: >>> schema = Schema({'one': 1, Extra: object}) >>> schema({'two': 'two', 'one': 1}) == {'two': 'two', 'one': 1} True >>> schema = Schema({'one': 1}) >>> try: ... schema({'two': 2}) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) "extra keys not allowed @ data['two']" dict, list, and tuple should be available as type validators: >>> Schema(dict)({'a': 1, 'b': 2}) == {'a': 1, 'b': 2} True >>> Schema(list)([1,2,3]) [1, 2, 3] >>> Schema(tuple)((1,2,3)) (1, 2, 3) Validation should return instances of the right types when the types are subclasses of dict or list: >>> class Dict(dict): ... pass >>> >>> d = Schema(dict)(Dict(a=1, b=2)) >>> d == {'a': 1, 'b': 2} True >>> type(d) is Dict True >>> class List(list): ... pass >>> >>> l = Schema(list)(List([1,2,3])) >>> l [1, 2, 3] >>> type(l) is List True Multiple errors are reported: >>> schema = Schema({'one': 1, 'two': 2}) >>> try: ... schema({'one': 2, 'two': 3, 'three': 4}) ... except MultipleInvalid as e: ... errors = sorted(e.errors, key=lambda k: str(k)) ... print([str(i) for i in errors]) # doctest: +NORMALIZE_WHITESPACE ["extra keys not allowed @ data['three']", "not a valid value for dictionary value @ data['one']", "not a valid value for dictionary value @ data['two']"] >>> schema = Schema([[1], [2], [3]]) >>> try: ... schema([1, 2, 3]) ... except MultipleInvalid as e: ... print([str(i) for i in e.errors]) # doctest: +NORMALIZE_WHITESPACE ['expected a list @ data[0]', 'expected a list @ data[1]', 'expected a list @ data[2]'] Required fields in dictionary which are invalid should not have required : >>> from voluptuous import * >>> schema = Schema({'one': {'two': 3}}, required=True) >>> try: ... schema({'one': {'two': 2}}) ... except MultipleInvalid as e: ... errors = e.errors >>> 'required' in ' '.join([x.msg for x in errors]) False Multiple errors for nested fields in dicts and objects: > \>\>\> from collections import namedtuple \>\>\> validate = Schema({ > ... 'anobject': Object({ ... 'strfield': str, ... 'intfield': int ... > }) ... }) \>\>\> try: ... SomeObj = namedtuple('SomeObj', ('strfield', > 'intfield')) ... validate({'anobject': SomeObj(strfield=123, > intfield='one')}) ... except MultipleInvalid as e: ... > print(sorted(str(i) for i in e.errors)) \# doctest: > +NORMALIZE\_WHITESPACE ["expected int for object value @ > data['anobject']['intfield']", "expected str for object value @ > data['anobject']['strfield']"] Custom classes validate as schemas: >>> class Thing(object): ... pass >>> schema = Schema(Thing) >>> t = schema(Thing()) >>> type(t) is Thing True Classes with custom metaclasses should validate as schemas: >>> class MyMeta(type): ... pass >>> class Thing(object): ... __metaclass__ = MyMeta >>> schema = Schema(Thing) >>> t = schema(Thing()) >>> type(t) is Thing True Schemas built with All() should give the same error as the original validator (Issue \#26): >>> schema = Schema({ ... Required('items'): All([{ ... Required('foo'): str ... }]) ... }) >>> try: ... schema({'items': [{}]}) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) "required key not provided @ data['items'][0]['foo']" Validator should return same instance of the same type for object: >>> class Structure(object): ... def __init__(self, q=None): ... self.q = q ... def __repr__(self): ... return '{0.__name__}(q={1.q!r})'.format(type(self), self) ... >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) >>> type(schema(Structure(q='one'))) is Structure True Object validator should treat cls argument as optional. In this case it shouldn't check object type: >>> from collections import namedtuple >>> NamedTuple = namedtuple('NamedTuple', ('q',)) >>> schema = Schema(Object({'q': 'one'})) >>> named = NamedTuple(q='one') >>> schema(named) == named True >>> schema(named) NamedTuple(q='one') If cls argument passed to object validator we should check object type: >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) >>> schema(NamedTuple(q='one')) # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... MultipleInvalid: expected a >>> schema = Schema(Object({'q': 'one'}, cls=NamedTuple)) >>> schema(NamedTuple(q='one')) NamedTuple(q='one') Ensure that objects with \_\_slots\_\_ supported properly: >>> class SlotsStructure(Structure): ... __slots__ = ['q'] ... >>> schema = Schema(Object({'q': 'one'})) >>> schema(SlotsStructure(q='one')) SlotsStructure(q='one') >>> class DictStructure(object): ... __slots__ = ['q', '__dict__'] ... def __init__(self, q=None, page=None): ... self.q = q ... self.page = page ... def __repr__(self): ... return '{0.__name__}(q={1.q!r}, page={1.page!r})'.format(type(self), self) ... >>> structure = DictStructure(q='one') >>> structure.page = 1 >>> try: ... schema(structure) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) "extra keys not allowed @ data['page']" >>> schema = Schema(Object({'q': 'one', Extra: object})) >>> schema(structure) DictStructure(q='one', page=1) Ensure that objects can be used with other validators: >>> schema = Schema({'meta': Object({'q': 'one'})}) >>> schema({'meta': Structure(q='one')}) {'meta': Structure(q='one')} Ensure that subclasses of Invalid of are raised as is. >>> class SpecialInvalid(Invalid): ... pass ... >>> def custom_validator(value): ... raise SpecialInvalid('boom') ... >>> schema = Schema({'thing': custom_validator}) >>> try: ... schema({'thing': 'not an int'}) ... except MultipleInvalid as e: ... exc = e >>> exc.errors[0].__class__.__name__ 'SpecialInvalid' Ensure that Optional('Classification') < 'Name' will return True instead of throwing an AttributeError >>> Optional('Classification') < 'Name' True ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/tests/tests.py0000644000175000017500000014127214557417777020621 0ustar00philipphilip# fmt: off import collections import copy import os import sys from enum import Enum import pytest from voluptuous import ( ALLOW_EXTRA, PREVENT_EXTRA, All, AllInvalid, Any, Clamp, Coerce, Contains, ContainsInvalid, Date, Datetime, Email, EmailInvalid, Equal, ExactSequence, Exclusive, Extra, FqdnUrl, In, Inclusive, InInvalid, Invalid, IsDir, IsFile, Length, Literal, LiteralInvalid, Marker, Match, MatchInvalid, Maybe, MultipleInvalid, NotIn, NotInInvalid, Number, Object, Optional, PathExists, Range, Remove, Replace, Required, Schema, Self, SomeOf, TooManyValid, TypeInvalid, Union, Unordered, Url, UrlInvalid, raises, validate, ) from voluptuous.humanize import humanize_error from voluptuous.util import Capitalize, Lower, Strip, Title, Upper # fmt: on def test_new_required_test(): schema = Schema( { 'my_key': All(int, Range(1, 20)), }, required=True, ) assert schema.required def test_exact_sequence(): schema = Schema(ExactSequence([int, int])) with raises(Invalid): schema([1, 2, 3]) assert schema([1, 2]) == [1, 2] def test_required(): """Verify that Required works.""" schema = Schema({Required('q'): int}) schema({"q": 123}) with raises(Invalid, "required key not provided @ data['q']"): schema({}) def test_extra_with_required(): """Verify that Required does not break Extra.""" schema = Schema({Required('toaster'): str, Extra: object}) r = schema({'toaster': 'blue', 'another_valid_key': 'another_valid_value'}) assert r == {'toaster': 'blue', 'another_valid_key': 'another_valid_value'} def test_iterate_candidates(): """Verify that the order for iterating over mapping candidates is right.""" schema = { "toaster": str, Extra: object, } # toaster should be first. from voluptuous.schema_builder import _iterate_mapping_candidates assert _iterate_mapping_candidates(schema)[0][0] == 'toaster' def test_in(): """Verify that In works.""" schema = Schema({"color": In(frozenset(["red", "blue", "yellow"]))}) schema({"color": "blue"}) with pytest.raises( MultipleInvalid, match=r"value must be one of \['blue', 'red', 'yellow'\] for dictionary value @ data\['color'\]", ) as ctx: schema({"color": "orange"}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], InInvalid) def test_in_unsortable_container(): """Verify that In works with unsortable container.""" schema = Schema({"type": In((int, str, float))}) schema({"type": float}) with pytest.raises( MultipleInvalid, match=( r"value must be one of \[, , \] for dictionary value " r"@ data\['type'\]" ), ) as ctx: schema({"type": 42}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], InInvalid) def test_not_in(): """Verify that NotIn works.""" schema = Schema({"color": NotIn(frozenset(["red", "blue", "yellow"]))}) schema({"color": "orange"}) with pytest.raises( MultipleInvalid, match=( r"value must not be one of \['blue', 'red', 'yellow'\] for dictionary " r"value @ data\['color'\]" ), ) as ctx: schema({"color": "blue"}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], NotInInvalid) def test_not_in_unsortable_container(): """Verify that NotIn works with unsortable container.""" schema = Schema({"type": NotIn((int, str, float))}) schema({"type": 42}) with pytest.raises( MultipleInvalid, match=( r"value must not be one of \[, , " r"\] for dictionary value @ data\['type'\]" ), ) as ctx: schema({"type": str}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], NotInInvalid) def test_contains(): """Verify contains validation method.""" schema = Schema({'color': Contains('red')}) schema({'color': ['blue', 'red', 'yellow']}) with pytest.raises( MultipleInvalid, match=r"value is not allowed for dictionary value @ data\['color'\]", ) as ctx: schema({'color': ['blue', 'yellow']}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], ContainsInvalid) def test_remove(): """Verify that Remove works.""" # remove dict keys schema = Schema({"weight": int, Remove("color"): str, Remove("amount"): int}) out_ = schema({"weight": 10, "color": "red", "amount": 1}) assert "color" not in out_ and "amount" not in out_ # remove keys by type schema = Schema( { "weight": float, "amount": int, # remove str keys with int values Remove(str): int, # keep str keys with str values str: str, } ) out_ = schema({"weight": 73.4, "condition": "new", "amount": 5, "left": 2}) # amount should stay since it's defined # other string keys with int values will be removed assert "amount" in out_ and "left" not in out_ # string keys with string values will stay assert "condition" in out_ # remove value from list schema = Schema([Remove(1), int]) out_ = schema([1, 2, 3, 4, 1, 5, 6, 1, 1, 1]) assert out_ == [2, 3, 4, 5, 6] # remove values from list by type schema = Schema([1.0, Remove(float), int]) out_ = schema([1, 2, 1.0, 2.0, 3.0, 4]) assert out_ == [1, 2, 1.0, 4] def test_extra_empty_errors(): schema = Schema({'a': {Extra: object}}, required=True) schema({'a': {}}) def test_literal(): """Test with Literal""" schema = Schema([Literal({"a": 1}), Literal({"b": 1})]) schema([{"a": 1}]) schema([{"b": 1}]) schema([{"a": 1}, {"b": 1}]) with pytest.raises( MultipleInvalid, match=r"\{'c': 1\} not match for \{'b': 1\} @ data\[0\]" ) as ctx: schema([{"c": 1}]) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], LiteralInvalid) schema = Schema(Literal({"a": 1})) with pytest.raises( MultipleInvalid, match=r"\{'b': 1\} not match for \{'a': 1\}" ) as ctx: schema({"b": 1}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], LiteralInvalid) def test_class(): class C1: pass schema = Schema(C1) schema(C1()) with pytest.raises(MultipleInvalid, match=r"expected C1") as ctx: schema(None) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], TypeInvalid) def test_email_validation(): """Test with valid email address""" schema = Schema({"email": Email()}) out_ = schema({"email": "example@example.com"}) assert 'example@example.com"', out_.get("url") def test_email_validation_with_none(): """Test with invalid None email address""" schema = Schema({"email": Email()}) with pytest.raises( MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": None}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], EmailInvalid) def test_email_validation_with_empty_string(): """Test with empty string email address""" schema = Schema({"email": Email()}) with pytest.raises( MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": ''}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], EmailInvalid) def test_email_validation_without_host(): """Test with empty host name in email address""" schema = Schema({"email": Email()}) with pytest.raises( MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": 'a@.com'}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], EmailInvalid) @pytest.mark.parametrize( 'input_value', ['john@voluptuous.com>', 'john!@voluptuous.org!@($*!'] ) def test_email_validation_with_bad_data(input_value: str): """Test with bad data in email address""" schema = Schema({"email": Email()}) with pytest.raises( MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": input_value}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], EmailInvalid) def test_fqdn_url_validation(): """Test with valid fully qualified domain name URL""" schema = Schema({"url": FqdnUrl()}) out_ = schema({"url": "http://example.com/"}) assert 'http://example.com/', out_.get("url") @pytest.mark.parametrize( 'input_value', [ pytest.param("http://localhost/", id="without domain name"), pytest.param(None, id="None"), pytest.param("", id="empty string"), pytest.param("http://", id="empty host"), ], ) def test_fqdn_url_validation_with_bad_data(input_value): schema = Schema({"url": FqdnUrl()}) with pytest.raises( MultipleInvalid, match=r"expected a fully qualified domain name URL for dictionary value @ data\['url'\]", ) as ctx: schema({"url": input_value}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], UrlInvalid) def test_url_validation(): """Test with valid URL""" schema = Schema({"url": Url()}) out_ = schema({"url": "http://example.com/"}) assert 'http://example.com/', out_.get("url") @pytest.mark.parametrize( 'input_value', [ pytest.param(None, id="None"), pytest.param("", id="empty string"), pytest.param("http://", id="empty host"), ], ) def test_url_validation_with_bad_data(input_value): schema = Schema({"url": Url()}) with pytest.raises( MultipleInvalid, match=r"expected a URL for dictionary value @ data\['url'\]" ) as ctx: schema({"url": input_value}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], UrlInvalid) def test_copy_dict_undefined(): """Test with a copied dictionary""" fields = {Required("foo"): int} copied_fields = copy.deepcopy(fields) schema = Schema(copied_fields) # This used to raise a `TypeError` because the instance of `Undefined` # was a copy, so object comparison would not work correctly. try: schema({"foo": "bar"}) except Exception as e: assert isinstance(e, MultipleInvalid) def test_sorting(): """Expect alphabetic sorting""" foo = Required('foo') bar = Required('bar') items = [foo, bar] expected = [bar, foo] result = sorted(items) assert result == expected def test_schema_extend(): """Verify that Schema.extend copies schema keys from both.""" base = Schema({'a': int}, required=True) extension = {'b': str} extended = base.extend(extension) assert base.schema == {'a': int} assert extension == {'b': str} assert extended.schema == {'a': int, 'b': str} assert extended.required == base.required assert extended.extra == base.extra assert isinstance(extended, Schema) def test_schema_extend_overrides(): """Verify that Schema.extend can override required/extra parameters.""" base = Schema({'a': int}, required=True) extended = base.extend({'b': str}, required=False, extra=ALLOW_EXTRA) assert base.required is True assert base.extra == PREVENT_EXTRA assert extended.required is False assert extended.extra == ALLOW_EXTRA def test_schema_extend_key_swap(): """Verify that Schema.extend can replace keys, even when different markers are used""" base = Schema({Optional('a'): int}) extension = {Required('a'): int} extended = base.extend(extension) assert len(base.schema) == 1 assert isinstance(list(base.schema)[0], Optional) assert len(extended.schema) == 1 assert list(extended.schema)[0] def test_subschema_extension(): """Verify that Schema.extend adds and replaces keys in a subschema""" base = Schema({'a': {'b': int, 'c': float}}) extension = {'d': str, 'a': {'b': str, 'e': int}} extended = base.extend(extension) assert base.schema == {'a': {'b': int, 'c': float}} assert extension == {'d': str, 'a': {'b': str, 'e': int}} assert extended.schema == {'a': {'b': str, 'c': float, 'e': int}, 'd': str} def test_schema_extend_handles_schema_subclass(): """Verify that Schema.extend handles a subclass of Schema""" class S(Schema): pass base = S({Required('a'): int}) extension = {Optional('b'): str} extended = base.extend(extension) expected_schema = {Required('a'): int, Optional('b'): str} assert extended.schema == expected_schema assert isinstance(extended, S) def test_equality(): assert Schema('foo') == Schema('foo') assert Schema(['foo', 'bar', 'baz']) == Schema(['foo', 'bar', 'baz']) # Ensure two Schemas w/ two equivalent dicts initialized in a different # order are considered equal. dict_a = {} dict_a['foo'] = 1 dict_a['bar'] = 2 dict_a['baz'] = 3 dict_b = {} dict_b['baz'] = 3 dict_b['bar'] = 2 dict_b['foo'] = 1 assert Schema(dict_a) == Schema(dict_b) def test_equality_negative(): """Verify that Schema objects are not equal to string representations""" assert not Schema('foo') == 'foo' assert not Schema(['foo', 'bar']) == "['foo', 'bar']" assert not Schema(['foo', 'bar']) == Schema("['foo', 'bar']") assert not Schema({'foo': 1, 'bar': 2}) == "{'foo': 1, 'bar': 2}" assert not Schema({'foo': 1, 'bar': 2}) == Schema("{'foo': 1, 'bar': 2}") def test_inequality(): assert Schema('foo') != 'foo' assert Schema(['foo', 'bar']) != "['foo', 'bar']" assert Schema(['foo', 'bar']) != Schema("['foo', 'bar']") assert Schema({'foo': 1, 'bar': 2}) != "{'foo': 1, 'bar': 2}" assert Schema({'foo': 1, 'bar': 2}) != Schema("{'foo': 1, 'bar': 2}") def test_inequality_negative(): assert not Schema('foo') != Schema('foo') assert not Schema(['foo', 'bar', 'baz']) != Schema(['foo', 'bar', 'baz']) # Ensure two Schemas w/ two equivalent dicts initialized in a different # order are considered equal. dict_a = {} dict_a['foo'] = 1 dict_a['bar'] = 2 dict_a['baz'] = 3 dict_b = {} dict_b['baz'] = 3 dict_b['bar'] = 2 dict_b['foo'] = 1 assert not Schema(dict_a) != Schema(dict_b) def test_repr(): """Verify that __repr__ returns valid Python expressions""" match = Match('a pattern', msg='message') replace = Replace('you', 'I', msg='you and I') range_ = Range( min=0, max=42, min_included=False, max_included=False, msg='number not in range' ) coerce_ = Coerce(int, msg="moo") all_ = All('10', Coerce(int), msg='all msg') maybe_int = Maybe(int) assert repr(match) == "Match('a pattern', msg='message')" assert repr(replace) == "Replace('you', 'I', msg='you and I')" assert ( repr(range_) == "Range(min=0, max=42, min_included=False, max_included=False, msg='number not in range')" ) assert repr(coerce_) == "Coerce(int, msg='moo')" assert repr(all_) == "All('10', Coerce(int, msg=None), msg='all msg')" assert repr(maybe_int) == "Any(None, %s, msg=None)" % str(int) def test_list_validation_messages(): """Make sure useful error messages are available""" def is_even(value): if value % 2: raise Invalid('%i is not even' % value) return value schema = Schema(dict(even_numbers=[All(int, is_even)])) with pytest.raises( MultipleInvalid, match=r"3 is not even @ data\['even_numbers'\]\[0\]" ) as ctx: schema(dict(even_numbers=[3])) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], Invalid) assert str(ctx.value.errors[0]) == "3 is not even @ data['even_numbers'][0]" def test_nested_multiple_validation_errors(): """Make sure useful error messages are available""" def is_even(value): if value % 2: raise Invalid('%i is not even' % value) return value schema = Schema(dict(even_numbers=All([All(int, is_even)], Length(min=1)))) with pytest.raises( MultipleInvalid, match=r"3 is not even @ data\['even_numbers'\]\[0\]" ) as ctx: schema(dict(even_numbers=[3])) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], Invalid) assert str(ctx.value.errors[0]) == "3 is not even @ data['even_numbers'][0]" def test_humanize_error(): data = {'a': 'not an int', 'b': [123]} schema = Schema({'a': int, 'b': [str]}) with pytest.raises(MultipleInvalid) as ctx: schema(data) assert len(ctx.value.errors) == 2 assert humanize_error(data, ctx.value) == ( "expected int for dictionary value @ data['a']. Got 'not an int'\nexpected str @ data['b'][0]. Got 123" ) def test_fix_157(): s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) assert ['one'] == s(['one']) with pytest.raises(MultipleInvalid): s(['four']) def test_range_inside(): s = Schema(Range(min=0, max=10)) assert 5 == s(5) def test_range_outside(): s = Schema(Range(min=0, max=10)) with pytest.raises(MultipleInvalid): s(12) with pytest.raises(MultipleInvalid): s(-1) def test_range_no_upper_limit(): s = Schema(Range(min=0)) assert 123 == s(123) with pytest.raises(MultipleInvalid): s(-1) def test_range_no_lower_limit(): s = Schema(Range(max=10)) assert -1 == s(-1) with pytest.raises(MultipleInvalid): s(123) def test_range_excludes_nan(): s = Schema(Range(min=0, max=10)) pytest.raises(MultipleInvalid, s, float('nan')) def test_range_excludes_none(): s = Schema(Range(min=0, max=10)) pytest.raises(MultipleInvalid, s, None) def test_range_excludes_string(): s = Schema(Range(min=0, max=10)) with pytest.raises(MultipleInvalid): s("abc") def test_range_excludes_unordered_object(): class MyObject(object): pass s = Schema(Range(min=0, max=10)) pytest.raises(MultipleInvalid, s, MyObject()) def test_clamp_inside(): s = Schema(Clamp(min=1, max=10)) assert 5 == s(5) def test_clamp_above(): s = Schema(Clamp(min=1, max=10)) assert 10 == s(12) def test_clamp_below(): s = Schema(Clamp(min=1, max=10)) assert 1 == s(-3) def test_clamp_invalid(): s = Schema(Clamp(min=1, max=10)) if sys.version_info.major >= 3: with pytest.raises(MultipleInvalid): s(None) with pytest.raises(MultipleInvalid): s("abc") else: assert 1 == s(None) def test_length_ok(): v1 = ['a', 'b', 'c'] s = Schema(Length(min=1, max=10)) assert v1 == s(v1) v2 = "abcde" assert v2 == s(v2) def test_length_too_short(): v1 = [] s = Schema(Length(min=1, max=10)) with pytest.raises(MultipleInvalid): s(v1) with pytest.raises(MultipleInvalid): v2 = '' s(v2) def test_length_too_long(): v = ['a', 'b', 'c'] s = Schema(Length(min=0, max=2)) with pytest.raises(MultipleInvalid): s(v) def test_length_invalid(): v = None s = Schema(Length(min=0, max=2)) with pytest.raises(MultipleInvalid): s(v) def test_equal(): s = Schema(Equal(1)) s(1) pytest.raises(Invalid, s, 2) s = Schema(Equal('foo')) s('foo') pytest.raises(Invalid, s, 'bar') s = Schema(Equal([1, 2])) s([1, 2]) pytest.raises(Invalid, s, []) pytest.raises(Invalid, s, [1, 2, 3]) # Evaluates exactly, not through validators s = Schema(Equal(str)) pytest.raises(Invalid, s, 'foo') def test_unordered(): # Any order is OK s = Schema(Unordered([2, 1])) s([2, 1]) s([1, 2]) # Amount of errors is OK pytest.raises(Invalid, s, [2, 0]) pytest.raises(MultipleInvalid, s, [0, 0]) # Different length is NOK pytest.raises(Invalid, s, [1]) pytest.raises(Invalid, s, [1, 2, 0]) pytest.raises(MultipleInvalid, s, [1, 2, 0, 0]) # Other type than list or tuple is NOK pytest.raises(Invalid, s, 'foo') pytest.raises(Invalid, s, 10) # Validators are evaluated through as schemas s = Schema(Unordered([int, str])) s([1, '2']) s(['1', 2]) s = Schema(Unordered([{'foo': int}, []])) s([{'foo': 3}, []]) # Most accurate validators must be positioned on left s = Schema(Unordered([int, 3])) pytest.raises(Invalid, s, [3, 2]) s = Schema(Unordered([3, int])) s([3, 2]) def test_maybe(): s = Schema(Maybe(int)) assert s(1) == 1 assert s(None) is None pytest.raises(Invalid, s, 'foo') s = Schema(Maybe({str: Coerce(int)})) assert s({'foo': '100'}) == {'foo': 100} assert s(None) is None pytest.raises(Invalid, s, {'foo': 'bar'}) def test_maybe_accepts_msg(): s = Schema(Maybe(int, msg='int or None expected')) with raises(MultipleInvalid, 'int or None expected'): assert s([]) def test_maybe_returns_default_error(): schema = Schema(Maybe(Range(1, 2))) # The following should be valid schema(None) schema(1) schema(2) try: # Should trigger a MultipleInvalid exception schema(3) except MultipleInvalid as e: assert str(e) == "not a valid value" else: assert False, "Did not raise correct Invalid" def test_schema_empty_list(): s = Schema([]) s([]) try: s([123]) except MultipleInvalid as e: assert str(e) == "not a valid value @ data[123]" else: assert False, "Did not raise correct Invalid" try: s({'var': 123}) except MultipleInvalid as e: assert str(e) == "expected a list" else: assert False, "Did not raise correct Invalid" def test_schema_empty_dict(): s = Schema({}) s({}) try: s({'var': 123}) except MultipleInvalid as e: assert str(e) == "extra keys not allowed @ data['var']" else: assert False, "Did not raise correct Invalid" try: s([123]) except MultipleInvalid as e: assert str(e) == "expected a dictionary" else: assert False, "Did not raise correct Invalid" def test_schema_empty_dict_key(): """https://github.com/alecthomas/voluptuous/pull/434""" s = Schema({'var': []}) s({'var': []}) try: s({'var': [123]}) except MultipleInvalid as e: assert str(e) == "not a valid value for dictionary value @ data['var']" else: assert False, "Did not raise correct Invalid" def test_schema_decorator_match_with_args(): @validate(int) def fn(arg): return arg fn(1) def test_schema_decorator_unmatch_with_args(): @validate(int) def fn(arg): return arg pytest.raises(Invalid, fn, 1.0) def test_schema_decorator_match_with_kwargs(): @validate(arg=int) def fn(arg): return arg fn(1) def test_schema_decorator_unmatch_with_kwargs(): @validate(arg=int) def fn(arg): return arg pytest.raises(Invalid, fn, 1.0) def test_schema_decorator_match_return_with_args(): @validate(int, __return__=int) def fn(arg): return arg fn(1) def test_schema_decorator_unmatch_return_with_args(): @validate(int, __return__=int) def fn(arg): return "hello" pytest.raises(Invalid, fn, 1) def test_schema_decorator_match_return_with_kwargs(): @validate(arg=int, __return__=int) def fn(arg): return arg fn(1) def test_schema_decorator_unmatch_return_with_kwargs(): @validate(arg=int, __return__=int) def fn(arg): return "hello" pytest.raises(Invalid, fn, 1) def test_schema_decorator_return_only_match(): @validate(__return__=int) def fn(arg): return arg fn(1) def test_schema_decorator_return_only_unmatch(): @validate(__return__=int) def fn(arg): return "hello" pytest.raises(Invalid, fn, 1) def test_schema_decorator_partial_match_called_with_args(): @validate(arg1=int) def fn(arg1, arg2): return arg1 fn(1, "foo") def test_schema_decorator_partial_unmatch_called_with_args(): @validate(arg1=int) def fn(arg1, arg2): return arg1 pytest.raises(Invalid, fn, "bar", "foo") def test_schema_decorator_partial_match_called_with_kwargs(): @validate(arg2=int) def fn(arg1, arg2): return arg1 fn(arg1="foo", arg2=1) def test_schema_decorator_partial_unmatch_called_with_kwargs(): @validate(arg2=int) def fn(arg1, arg2): return arg1 pytest.raises(Invalid, fn, arg1=1, arg2="foo") def test_number_validation_with_string(): """Test with Number with string""" schema = Schema({"number": Number(precision=6, scale=2)}) try: schema({"number": 'teststr'}) except MultipleInvalid as e: assert ( str(e) == "Value must be a number enclosed with string for dictionary value @ data['number']" ) else: assert False, "Did not raise Invalid for String" def test_number_validation_with_invalid_precision_invalid_scale(): """Test with Number with invalid precision and scale""" schema = Schema({"number": Number(precision=6, scale=2)}) try: schema({"number": '123456.712'}) except MultipleInvalid as e: assert ( str(e) == "Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']" ) else: assert False, "Did not raise Invalid for String" def test_number_validation_with_valid_precision_scale_yield_decimal_true(): """Test with Number with valid precision and scale""" schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=True)}) out_ = schema({"number": '1234.00'}) assert float(out_.get("number")) == 1234.00 def test_number_when_precision_scale_none_yield_decimal_true(): """Test with Number with no precision and scale""" schema = Schema({"number": Number(yield_decimal=True)}) out_ = schema({"number": '12345678901234'}) assert out_.get("number") == 12345678901234 def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): """Test with Number with no precision and valid scale case 1""" schema = Schema({"number": Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789.34'}) assert float(out_.get("number")) == 123456789.34 def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): """Test with Number with no precision and valid scale case 2 with zero in decimal part""" schema = Schema({"number": Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789012.00'}) assert float(out_.get("number")) == 123456789012.00 def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): """Test with Number with no precision and invalid scale""" schema = Schema({"number": Number(scale=2, yield_decimal=True)}) try: schema({"number": '12345678901.234'}) except MultipleInvalid as e: assert ( str(e) == "Scale must be equal to 2 for dictionary value @ data['number']" ) else: assert False, "Did not raise Invalid for String" def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): """Test with Number with no precision and valid scale""" schema = Schema({"number": Number(precision=14, yield_decimal=True)}) out_ = schema({"number": '1234567.8901234'}) assert float(out_.get("number")) == 1234567.8901234 def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): """Test with Number with no precision and invalid scale""" schema = Schema({"number": Number(precision=14, yield_decimal=True)}) try: schema({"number": '12345674.8901234'}) except MultipleInvalid as e: assert ( str(e) == "Precision must be equal to 14 for dictionary value @ data['number']" ) else: assert False, "Did not raise Invalid for String" def test_number_validation_with_valid_precision_scale_yield_decimal_false(): """Test with Number with valid precision, scale and no yield_decimal""" schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=False)}) out_ = schema({"number": '1234.00'}) assert out_.get("number") == '1234.00' def test_named_tuples_validate_as_tuples(): NT = collections.namedtuple('NT', ['a', 'b']) nt = NT(1, 2) t = (1, 2) Schema((int, int))(nt) Schema((int, int))(t) Schema(NT(int, int))(nt) Schema(NT(int, int))(t) def test_datetime(): schema = Schema({"datetime": Datetime()}) schema({"datetime": "2016-10-24T14:01:57.102152Z"}) pytest.raises(MultipleInvalid, schema, {"datetime": "2016-10-24T14:01:57"}) def test_date(): schema = Schema({"date": Date()}) schema({"date": "2016-10-24"}) pytest.raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) def test_date_custom_format(): schema = Schema({"date": Date("%Y%m%d")}) schema({"date": "20161024"}) pytest.raises(MultipleInvalid, schema, {"date": "2016-10-24"}) def test_ordered_dict(): if not hasattr(collections, 'OrderedDict'): # collections.OrderedDict was added in Python2.7; only run if present return schema = Schema({Number(): Number()}) # x, y pairs (for interpolation or something) data = collections.OrderedDict( [ (5.0, 3.7), (24.0, 8.7), (43.0, 1.5), (62.0, 2.1), (71.5, 6.7), (90.5, 4.1), (109.0, 3.9), ] ) out = schema(data) assert isinstance(out, collections.OrderedDict), 'Collection is no longer ordered' assert data.keys() == out.keys(), 'Order is not consistent' def test_marker_hashable(): """Verify that you can get schema keys, even if markers were used""" definition = { Required('x'): int, Optional('y'): float, Remove('j'): int, Remove(int): str, int: int, } assert definition.get('x') == int assert definition.get('y') == float assert Required('x') == Required('x') assert Required('x') != Required('y') # Remove markers are not hashable assert definition.get('j') is None def test_schema_infer(): schema = Schema.infer({'str': 'foo', 'bool': True, 'int': 42, 'float': 3.14}) assert schema == Schema( { Required('str'): str, Required('bool'): bool, Required('int'): int, Required('float'): float, } ) def test_schema_infer_dict(): schema = Schema.infer({'a': {'b': {'c': 'foo'}}}) assert schema == Schema({Required('a'): {Required('b'): {Required('c'): str}}}) def test_schema_infer_list(): schema = Schema.infer({'list': ['foo', True, 42, 3.14]}) assert schema == Schema({Required('list'): [str, bool, int, float]}) def test_schema_infer_scalar(): assert Schema.infer('foo') == Schema(str) assert Schema.infer(True) == Schema(bool) assert Schema.infer(42) == Schema(int) assert Schema.infer(3.14) == Schema(float) assert Schema.infer({}) == Schema(dict) assert Schema.infer([]) == Schema(list) def test_schema_infer_accepts_kwargs(): schema = Schema.infer({'str': 'foo', 'bool': True}, required=False, extra=True) # Subset of schema should be acceptable thanks to required=False. schema({'bool': False}) # Keys that are in schema should still match required types. try: schema({'str': 42}) except Invalid: pass else: assert False, 'Did not raise Invalid for Number' # Extra fields should be acceptable thanks to extra=True. schema({'str': 'bar', 'int': 42}) def test_validation_performance(): """ This test comes to make sure the validation complexity of dictionaries is done in a linear time. To achieve this a custom marker is used in the scheme that counts each time it is evaluated. By doing so we can determine if the validation is done in linear complexity. Prior to issue https://github.com/alecthomas/voluptuous/issues/259 this was exponential. """ num_of_keys = 1000 schema_dict = {} data = {} data_extra_keys = {} counter = [0] class CounterMarker(Marker): def __call__(self, *args, **kwargs): counter[0] += 1 return super(CounterMarker, self).__call__(*args, **kwargs) for i in range(num_of_keys): schema_dict[CounterMarker(str(i))] = str data[str(i)] = str(i) data_extra_keys[str(i * 2)] = str( i ) # half of the keys are present, and half aren't schema = Schema(schema_dict, extra=ALLOW_EXTRA) schema(data) assert counter[0] <= num_of_keys, "Validation complexity is not linear! %s > %s" % ( counter[0], num_of_keys, ) counter[0] = 0 # reset counter schema(data_extra_keys) assert counter[0] <= num_of_keys, "Validation complexity is not linear! %s > %s" % ( counter[0], num_of_keys, ) def test_IsDir(): schema = Schema(IsDir()) pytest.raises(MultipleInvalid, schema, 3) schema(os.path.dirname(os.path.abspath(__file__))) def test_IsFile(): schema = Schema(IsFile()) pytest.raises(MultipleInvalid, schema, 3) schema(os.path.abspath(__file__)) def test_PathExists(): schema = Schema(PathExists()) pytest.raises(MultipleInvalid, schema, 3) schema(os.path.abspath(__file__)) def test_description(): marker = Marker(Schema(str), description='Hello') assert marker.description == 'Hello' optional = Optional('key', description='Hello') assert optional.description == 'Hello' exclusive = Exclusive('alpha', 'angles', description='Hello') assert exclusive.description == 'Hello' inclusive = Inclusive('alpha', 'angles', description='Hello') assert inclusive.description == 'Hello' required = Required('key', description='Hello') assert required.description == 'Hello' def test_SomeOf_min_validation(): validator = All( Length(min=8), SomeOf( min_valid=3, validators=[ Match(r'.*[A-Z]', 'no uppercase letters'), Match(r'.*[a-z]', 'no lowercase letters'), Match(r'.*[0-9]', 'no numbers'), Match(r'.*[$@$!%*#?&^:;/<,>|{}()\-\'._+=]', 'no symbols'), ], ), ) validator('ffe532A1!') with raises(MultipleInvalid, 'length of value must be at least 8'): validator('a') with raises(MultipleInvalid, 'no uppercase letters, no lowercase letters'): validator('1232!#4111') with raises(MultipleInvalid, 'no lowercase letters, no symbols'): validator('3A34SDEF5') def test_SomeOf_max_validation(): validator = SomeOf( max_valid=2, validators=[ Match(r'.*[A-Z]', 'no uppercase letters'), Match(r'.*[a-z]', 'no lowercase letters'), Match(r'.*[0-9]', 'no numbers'), ], msg='max validation test failed', ) validator('Aa') with raises(TooManyValid, 'max validation test failed'): validator('Aa1') def test_self_validation(): schema = Schema({"number": int, "follow": Self}) with raises(MultipleInvalid): schema({"number": "abc"}) with raises(MultipleInvalid): schema({"follow": {"number": '123456.712'}}) schema({"follow": {"number": 123456}}) schema({"follow": {"follow": {"number": 123456}}}) def test_any_error_has_path(): """https://github.com/alecthomas/voluptuous/issues/347""" s = Schema({Optional('q'): int, Required('q2'): Any(int, msg='toto')}) with pytest.raises(MultipleInvalid) as ctx: s({'q': 'str', 'q2': 'tata'}) assert ( ctx.value.errors[0].path == ['q'] and ctx.value.errors[1].path == ['q2'] ) or (ctx.value.errors[1].path == ['q'] and ctx.value.errors[0].path == ['q2']) def test_all_error_has_path(): """https://github.com/alecthomas/voluptuous/issues/347""" s = Schema( { Optional('q'): int, Required('q2'): All([str, Length(min=10)], msg='toto'), } ) with pytest.raises(MultipleInvalid) as ctx: s({'q': 'str', 'q2': 12}) assert len(ctx.value.errors) == 2 assert ( isinstance(ctx.value.errors[0], TypeInvalid) and isinstance(ctx.value.errors[1], AllInvalid) ) or ( isinstance(ctx.value.errors[1], TypeInvalid) and isinstance(ctx.value.errors[0], AllInvalid) ) assert ( ctx.value.errors[0].path == ['q'] and ctx.value.errors[1].path == ['q2'] ) or (ctx.value.errors[1].path == ['q'] and ctx.value.errors[0].path == ['q2']) def test_match_error_has_path(): """https://github.com/alecthomas/voluptuous/issues/347""" s = Schema( { Required('q2'): Match("a"), } ) with pytest.raises(MultipleInvalid) as ctx: s({'q2': 12}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], MatchInvalid) assert ctx.value.errors[0].path == ['q2'] def test_path_with_string(): """Most common dict use with strings as keys""" s = Schema({'string_key': int}) with pytest.raises(MultipleInvalid) as ctx: s({'string_key': 'str'}) assert ctx.value.errors[0].path == ['string_key'] def test_path_with_list_index(): """Position of the offending list index included in path as int""" s = Schema({'string_key': [int]}) with pytest.raises(MultipleInvalid) as ctx: s({'string_key': [123, 'should be int']}) assert ctx.value.errors[0].path == ['string_key', 1] def test_path_with_tuple_index(): """Position of the offending tuple index included in path as int""" s = Schema({'string_key': (int,)}) with pytest.raises(MultipleInvalid) as ctx: s({'string_key': (123, 'should be int')}) assert ctx.value.errors[0].path == ['string_key', 1] def test_path_with_integer_dict_key(): """Not obvious case with dict having not strings, but integers as keys""" s = Schema({1337: int}) with pytest.raises(MultipleInvalid) as ctx: s({1337: 'should be int'}) assert ctx.value.errors[0].path == [1337] def test_path_with_float_dict_key(): """Not obvious case with dict having not strings, but floats as keys""" s = Schema({13.37: int}) with pytest.raises(MultipleInvalid) as ctx: s({13.37: 'should be int'}) assert ctx.value.errors[0].path == [13.37] def test_path_with_tuple_dict_key(): """Not obvious case with dict having not strings, but tuples as keys""" s = Schema({('fancy', 'key'): int}) with pytest.raises(MultipleInvalid) as ctx: s({('fancy', 'key'): 'should be int'}) assert ctx.value.errors[0].path == [('fancy', 'key')] def test_path_with_arbitrary_hashable_dict_key(): """Not obvious case with dict having not strings, but arbitrary hashable objects as keys""" class HashableObjectWhichWillBeKeyInDict: def __hash__(self): return 1337 # dummy hash, used only for illustration s = Schema({HashableObjectWhichWillBeKeyInDict: [int]}) hashable_obj_provided_in_input = HashableObjectWhichWillBeKeyInDict() with pytest.raises(MultipleInvalid) as ctx: s({hashable_obj_provided_in_input: [0, 1, 'should be int']}) assert ctx.value.errors[0].path == [hashable_obj_provided_in_input, 2] def test_self_any(): schema = Schema({"number": int, "follow": Any(Self, "stop")}) with pytest.raises(MultipleInvalid) as ctx: schema({"number": "abc"}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], TypeInvalid) with raises(MultipleInvalid): schema({"follow": {"number": '123456.712'}}) schema({"follow": {"number": 123456}}) schema({"follow": {"follow": {"number": 123456}}}) schema({"follow": {"follow": {"number": 123456, "follow": "stop"}}}) def test_self_all(): schema = Schema( { "number": int, "follow": All(Self, Schema({"extra_number": int}, extra=ALLOW_EXTRA)), }, extra=ALLOW_EXTRA, ) with pytest.raises(MultipleInvalid) as ctx: schema({"number": "abc"}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], TypeInvalid) with pytest.raises(MultipleInvalid) as ctx: schema({"follow": {"number": '123456.712'}}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], TypeInvalid) schema({"follow": {"number": 123456}}) schema({"follow": {"follow": {"number": 123456}}}) schema({"follow": {"number": 123456, "extra_number": 123}}) with pytest.raises(MultipleInvalid) as ctx: schema({"follow": {"number": 123456, "extra_number": "123"}}) assert len(ctx.value.errors) == 1 assert isinstance(ctx.value.errors[0], TypeInvalid) def test_SomeOf_on_bounds_assertion(): with raises( AssertionError, 'when using "SomeOf" you should specify at least one of min_valid and max_valid', ): SomeOf(validators=[]) def test_comparing_voluptuous_object_to_str(): assert Optional('Classification') < 'Name' def test_set_of_integers(): schema = Schema({int}) with raises(Invalid, 'expected a set'): schema(42) with raises(Invalid, 'expected a set'): schema(frozenset([42])) schema(set()) schema(set([42])) schema(set([42, 43, 44])) with pytest.raises(MultipleInvalid, match="invalid value in set") as ctx: schema(set(['abc'])) assert len(ctx.value.errors) == 1 def test_frozenset_of_integers(): schema = Schema(frozenset([int])) with raises(Invalid, 'expected a frozenset'): schema(42) with raises(Invalid, 'expected a frozenset'): schema(set([42])) schema(frozenset()) schema(frozenset([42])) schema(frozenset([42, 43, 44])) with pytest.raises(MultipleInvalid, match="invalid value in frozenset") as ctx: schema(frozenset(['abc'])) assert len(ctx.value.errors) == 1 def test_set_of_integers_and_strings(): schema = Schema({int, str}) with raises(Invalid, 'expected a set'): schema(42) schema(set()) schema(set([42])) schema(set(['abc'])) schema(set([42, 'abc'])) with pytest.raises(MultipleInvalid, match="invalid value in set") as ctx: schema(set([None])) assert len(ctx.value.errors) == 1 def test_frozenset_of_integers_and_strings(): schema = Schema(frozenset([int, str])) with raises(Invalid, 'expected a frozenset'): schema(42) schema(frozenset()) schema(frozenset([42])) schema(frozenset(['abc'])) schema(frozenset([42, 'abc'])) with pytest.raises(MultipleInvalid, match="invalid value in frozenset") as ctx: schema(frozenset([None])) assert len(ctx.value.errors) == 1 def test_lower_util_handles_various_inputs(): assert Lower(3) == "3" assert Lower(u"3") == u"3" assert Lower(b'\xe2\x98\x83'.decode("UTF-8")) == b'\xe2\x98\x83'.decode("UTF-8") assert Lower(u"A") == u"a" def test_upper_util_handles_various_inputs(): assert Upper(3) == "3" assert Upper(u"3") == u"3" assert Upper(b'\xe2\x98\x83'.decode("UTF-8")) == b'\xe2\x98\x83'.decode("UTF-8") assert Upper(u"a") == u"A" def test_capitalize_util_handles_various_inputs(): assert Capitalize(3) == "3" assert Capitalize(u"3") == u"3" assert Capitalize(b'\xe2\x98\x83'.decode("UTF-8")) == b'\xe2\x98\x83'.decode( "UTF-8" ) assert Capitalize(u"aaa aaa") == u"Aaa aaa" def test_title_util_handles_various_inputs(): assert Title(3) == "3" assert Title(u"3") == u"3" assert Title(b'\xe2\x98\x83'.decode("UTF-8")) == b'\xe2\x98\x83'.decode("UTF-8") assert Title(u"aaa aaa") == u"Aaa Aaa" def test_strip_util_handles_various_inputs(): assert Strip(3) == "3" assert Strip(u"3") == u"3" assert Strip(b'\xe2\x98\x83'.decode("UTF-8")) == b'\xe2\x98\x83'.decode("UTF-8") assert Strip(u" aaa ") == u"aaa" def test_any_required(): schema = Schema(Any({'a': int}, {'b': str}, required=True)) with raises(MultipleInvalid, "required key not provided @ data['a']"): schema({}) def test_any_required_with_subschema(): schema = Schema( Any({'a': Any(float, int)}, {'b': int}, {'c': {'aa': int}}, required=True) ) with raises(MultipleInvalid, "required key not provided @ data['a']"): schema({}) def test_inclusive(): schema = Schema( { Inclusive('x', 'stuff'): int, Inclusive('y', 'stuff'): int, } ) r = schema({}) assert r == {} r = schema({'x': 1, 'y': 2}) assert r == {'x': 1, 'y': 2} with raises( MultipleInvalid, "some but not all values in the same group of inclusion 'stuff' @ data[]", ): schema({'x': 1}) def test_inclusive_defaults(): schema = Schema( { Inclusive('x', 'stuff', default=3): int, Inclusive('y', 'stuff', default=4): int, } ) r = schema({}) assert r == {'x': 3, 'y': 4} with raises( MultipleInvalid, "some but not all values in the same group of inclusion 'stuff' @ data[]", ): r = schema({'x': 1}) def test_exclusive(): schema = Schema( { Exclusive('x', 'stuff'): int, Exclusive('y', 'stuff'): int, } ) r = schema({}) assert r == {} r = schema({'x': 1}) assert r == {'x': 1} with raises( MultipleInvalid, "two or more values in the same group of exclusion 'stuff' @ data[]", ): r = schema({'x': 1, 'y': 2}) def test_any_with_discriminant(): schema = Schema( { 'implementation': Union( { 'type': 'A', 'a-value': str, }, { 'type': 'B', 'b-value': int, }, { 'type': 'C', 'c-value': bool, }, discriminant=lambda value, alternatives: filter( lambda v: v['type'] == value['type'], alternatives ), ) } ) with raises( MultipleInvalid, "expected bool for dictionary value @ data['implementation']['c-value']", ): schema({'implementation': {'type': 'C', 'c-value': None}}) def test_key1(): def as_int(a): return int(a) schema = Schema({as_int: str}) with pytest.raises(MultipleInvalid) as ctx: schema( { '1': 'one', 'two': '2', '3': 'three', 'four': '4', } ) assert len(ctx.value.errors) == 2 assert str(ctx.value.errors[0]) == "not a valid value @ data['two']" assert str(ctx.value.errors[1]) == "not a valid value @ data['four']" def test_key2(): def as_int(a): try: return int(a) except ValueError: raise Invalid('expecting a number') schema = Schema({as_int: str}) with pytest.raises(MultipleInvalid) as ctx: schema( { '1': 'one', 'two': '2', '3': 'three', 'four': '4', } ) assert len(ctx.value.errors) == 2 assert str(ctx.value.errors[0]) == "expecting a number @ data['two']" assert str(ctx.value.errors[1]) == "expecting a number @ data['four']" def test_coerce_enum(): """Test Coerce Enum""" class Choice(Enum): Easy = 1 Medium = 2 Hard = 3 class StringChoice(str, Enum): Easy = "easy" Medium = "medium" Hard = "hard" schema = Schema(Coerce(Choice)) string_schema = Schema(Coerce(StringChoice)) # Valid value assert schema(1) == Choice.Easy assert string_schema("easy") == StringChoice.Easy # Invalid value with raises(Invalid, "expected Choice or one of 1, 2, 3"): schema(4) with raises(Invalid, "expected StringChoice or one of 'easy', 'medium', 'hard'"): string_schema("hello") class MyValueClass(object): def __init__(self, value=None): self.value = value def test_object(): s = Schema(Object({'value': 1}), required=True) s(MyValueClass(value=1)) pytest.raises(MultipleInvalid, s, MyValueClass(value=2)) pytest.raises(MultipleInvalid, s, 345) def test_exception(): s = Schema(None) with pytest.raises(MultipleInvalid) as ctx: s(123) invalid_scalar_excp_repr = "ScalarInvalid('not a valid value')" assert repr(ctx.value) == f"MultipleInvalid([{invalid_scalar_excp_repr}])" assert str(ctx.value.msg) == "not a valid value" assert str(ctx.value.error_message) == "not a valid value" assert str(ctx.value.errors) == f"[{invalid_scalar_excp_repr}]" ctx.value.add("Test Error") assert str(ctx.value.errors) == f"[{invalid_scalar_excp_repr}, 'Test Error']" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/util.py0000644000175000017500000000614614557417777017272 0ustar00philipphilip# F401: "imported but unused" # fmt: off import typing 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 Lower(v: str) -> str: """Transform a string to lower case. >>> s = Schema(Lower) >>> s('HI') 'hi' """ return str(v).lower() def Upper(v: str) -> str: """Transform a string to upper case. >>> s = Schema(Upper) >>> s('hi') 'HI' """ return str(v).upper() def Capitalize(v: str) -> str: """Capitalise a string. >>> s = Schema(Capitalize) >>> s('hello world') 'Hello world' """ return str(v).capitalize() def Title(v: str) -> str: """Title case a string. >>> s = Schema(Title) >>> s('hello world') 'Hello World' """ return str(v).title() def Strip(v: str) -> str: """Strip whitespace from a string. >>> s = Schema(Strip) >>> s(' hello world ') 'hello world' """ return str(v).strip() class DefaultTo(object): """Sets a value to default_value if none provided. >>> s = Schema(DefaultTo(42)) >>> s(None) 42 >>> s = Schema(DefaultTo(list)) >>> s(None) [] """ def __init__(self, default_value, msg: typing.Optional[str] = None) -> None: self.default_value = default_factory(default_value) self.msg = msg def __call__(self, v): if v is None: v = self.default_value() return v def __repr__(self): return 'DefaultTo(%s)' % (self.default_value(),) class SetTo(object): """Set a value, ignoring any previous value. >>> s = Schema(validators.Any(int, SetTo(42))) >>> s(2) 2 >>> s("foo") 42 """ def __init__(self, value) -> None: self.value = default_factory(value) def __call__(self, v): return self.value() def __repr__(self): return 'SetTo(%s)' % (self.value(),) class Set(object): """Convert a list into a set. >>> s = Schema(Set()) >>> s([]) == set([]) True >>> s([1, 2]) == set([1, 2]) True >>> with raises(Invalid, regex="^cannot be presented as set: "): ... s([set([1, 2]), set([3, 4])]) """ def __init__(self, msg: 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)) return set_v def __repr__(self): return 'Set()' class Literal(object): def __init__(self, lit) -> None: self.lit = lit 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)) else: return self.lit def __str__(self): return str(self.lit) def __repr__(self): return repr(self.lit) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706958847.0 voluptuous-0.14.2/voluptuous/validators.py0000644000175000017500000010700314557417777020457 0ustar00philipphilip# fmt: off import datetime import os import re import sys 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 if sys.version_info >= (3,): import urllib.parse as urlparse basestring = str 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 "(?:" # dot-atom r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+" r"(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*$" # quoted-string r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|' r"""\\[\001-\011\013\014\016-\177])*"$)""" # end anchor, because fullmatch is not available in python 2.7 r")\Z", 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, ) # fmt: on __author__ = 'tusharmakkar08' 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') """ @wraps(f) def check(v): t = f(v) if not t: raise ValueError return v return check class Coerce(object): """Coerce a value to a type. If the type constructor throws a ValueError or TypeError, the value will be marked as Invalid. Default behavior: >>> validate = Schema(Coerce(int)) >>> with raises(MultipleInvalid, 'expected int'): ... validate(None) >>> with raises(MultipleInvalid, 'expected int'): ... validate('foo') With custom message: >>> validate = Schema(Coerce(int, "moo")) >>> with raises(MultipleInvalid, 'moo'): ... validate('foo') """ 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__ def __call__(self, v): try: return self.type(v) except (ValueError, TypeError, InvalidOperation): msg = self.msg or ('expected %s' % self.type_name) if not self.msg and Enum and issubclass(self.type, Enum): msg += " or one of %s" % str([e.value for e in self.type])[1:-1] raise CoerceInvalid(msg) def __repr__(self): return 'Coerce(%s, msg=%r)' % (self.type_name, self.msg) @message('value was not true', cls=TrueInvalid) @truth def IsTrue(v): """Assert that a value is true, in the Python sense. >>> validate = Schema(IsTrue()) "In the Python sense" means that implicitly false values, such as empty lists, dictionaries, etc. are treated as "false": >>> with raises(MultipleInvalid, "value was not true"): ... validate([]) >>> validate([1]) [1] >>> with raises(MultipleInvalid, "value was not true"): ... validate(False) ...and so on. >>> try: ... validate([]) ... except MultipleInvalid as e: ... assert isinstance(e.errors[0], TrueInvalid) """ return v @message('value was not false', cls=FalseInvalid) def IsFalse(v): """Assert that a value is false, in the Python sense. (see :func:`IsTrue` for more detail) >>> validate = Schema(IsFalse()) >>> validate([]) [] >>> with raises(MultipleInvalid, "value was not false"): ... validate(True) >>> try: ... validate(True) ... except MultipleInvalid as e: ... assert isinstance(e.errors[0], FalseInvalid) """ if v: raise ValueError return v @message('expected boolean', cls=BooleanInvalid) def Boolean(v): """Convert human-readable boolean values to a bool. Accepted values are 1, true, yes, on, enable, and their negatives. Non-string values are cast to bool. >>> validate = Schema(Boolean()) >>> validate(True) True >>> validate("1") True >>> validate("0") False >>> with raises(MultipleInvalid, "expected boolean"): ... validate('moo') >>> try: ... validate('moo') ... except MultipleInvalid as e: ... assert isinstance(e.errors[0], BooleanInvalid) """ if isinstance(v, basestring): v = v.lower() if v in ('1', 'true', 'yes', 'on', 'enable'): return True if v in ('0', 'false', 'no', 'off', 'disable'): return False raise ValueError return bool(v) class _WithSubValidators(object): """Base class for validators that use sub-validators. Special class to use as a parent class for validators using sub-validators. This class provides the `__voluptuous_compile__` method so the sub-validators are compiled by the parent `Schema`. """ def __init__( self, *validators, msg=None, required=False, discriminant=None, **kwargs ) -> None: self.validators = validators self.msg = msg self.required = required self.discriminant = discriminant def __voluptuous_compile__(self, schema: Schema) -> typing.Callable: self._compiled = [] old_required = schema.required self.schema = schema for v in self.validators: schema.required = self.required self._compiled.append(schema._compile(v)) schema.required = old_required return self._run def _run(self, path: typing.List[typing.Hashable], value): if self.discriminant is not None: self._compiled = [ self.schema._compile(v) for v in self.discriminant(value, self.validators) ] return self._exec(self._compiled, value, path) def __call__(self, v): return self._exec((Schema(val) for val in self.validators), v) def __repr__(self): return '%s(%s, msg=%r)' % ( self.__class__.__name__, ", ".join(repr(v) for v in self.validators), 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. :param msg: Message to deliver to user if validation fails. :param kwargs: All other keyword arguments are passed to the sub-schema constructors. :returns: Return value of the first validator that passes. >>> validate = Schema(Any('true', 'false', ... All(Any(int, bool), Coerce(bool)))) >>> validate('true') 'true' >>> validate(1) True >>> with raises(MultipleInvalid, "not a valid value"): ... validate('moo') msg argument is used >>> validate = Schema(Any(1, 2, 3, msg="Expected 1 2 or 3")) >>> validate(1) 1 >>> with raises(MultipleInvalid, "Expected 1 2 or 3"): ... validate(4) """ def _exec(self, funcs, v, path=None): error = None for func in funcs: try: if path is None: return func(v) else: return func(path, v) except Invalid as e: if error is None or len(e.path) > len(error.path): 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) # Convenience alias Or = Any class Union(_WithSubValidators): """Use the first validated value among those selected by discriminant. :param msg: Message to deliver to user if validation fails. :param discriminant(value, validators): Returns the filtered list of validators based on the value. :param kwargs: All other keyword arguments are passed to the sub-schema constructors. :returns: Return value of the first validator that passes. >>> validate = Schema(Union({'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}, ... discriminant=lambda val, alt: filter( ... lambda v : v['type'] == val['type'] , alt))) >>> validate({'type':'a', 'a_val':'1'}) == {'type':'a', 'a_val':'1'} True >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['b_val']"): ... validate({'type':'b', 'b_val':'5'}) ```discriminant({'type':'b', 'a_val':'5'}, [{'type':'a', 'a_val':'1'},{'type':'b', 'b_val':'2'}])``` is invoked Without the discriminant, the exception would be "extra keys not allowed @ data['b_val']" """ def _exec(self, funcs, v, path=None): error = None for func in funcs: try: if path is None: return func(v) else: return func(path, v) except Invalid as e: if error is None or len(e.path) > len(error.path): 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) # Convenience alias Switch = Union class All(_WithSubValidators): """Value must pass all validators. The output of each validator is passed as input to the next. :param msg: Message to deliver to user if validation fails. :param kwargs: All other keyword arguments are passed to the sub-schema constructors. >>> validate = Schema(All('10', Coerce(int))) >>> validate('10') 10 """ def _exec(self, funcs, v, path=None): try: for func in funcs: if path is None: v = func(v) else: v = func(path, v) except Invalid as e: raise e if self.msg is None else AllInvalid(self.msg, path=path) return v # Convenience alias And = All class Match(object): """Value must be a string that matches the regular expression. >>> validate = Schema(Match(r'^0x[A-F0-9]+$')) >>> validate('0x123EF4') '0x123EF4' >>> with raises(MultipleInvalid, 'does not match regular expression ^0x[A-F0-9]+$'): ... validate('123EF4') >>> with raises(MultipleInvalid, 'expected string or buffer'): ... validate(123) Pattern may also be a compiled regular expression: >>> validate = Schema(Match(re.compile(r'0x[A-F0-9]+', re.I))) >>> validate('0x123ef4') '0x123ef4' """ 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 self.msg = msg def __call__(self, v): try: match = self.pattern.match(v) 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) ) return v def __repr__(self): return 'Match(%r, msg=%r)' % (self.pattern.pattern, self.msg) class Replace(object): """Regex substitution. >>> validate = Schema(All(Replace('you', 'I'), ... Replace('hello', 'goodbye'))) >>> validate('you say hello') 'I say goodbye' """ 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 self.substitution = substitution self.msg = msg def __call__(self, v): return self.pattern.sub(self.substitution, v) def __repr__(self): return 'Replace(%r, %r, msg=%r)' % ( self.pattern.pattern, self.substitution, self.msg, ) 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") return parsed @message('expected an email address', cls=EmailInvalid) def Email(v): """Verify that the value is an email address or not. >>> s = Schema(Email()) >>> with raises(MultipleInvalid, 'expected an email address'): ... s("a.com") >>> with raises(MultipleInvalid, 'expected an email address'): ... s("a@.com") >>> with raises(MultipleInvalid, 'expected an email address'): ... s("a@.com") >>> s('t@x.com') 't@x.com' """ try: if not v or "@" not in v: raise EmailInvalid("Invalid email address") user_part, domain_part = v.rsplit('@', 1) if not (USER_REGEX.match(user_part) and DOMAIN_REGEX.match(domain_part)): raise EmailInvalid("Invalid email address") return v except: # noqa: E722 raise ValueError @message('expected a fully qualified domain name URL', cls=UrlInvalid) def FqdnUrl(v): """Verify that the value is a fully qualified domain name URL. >>> s = Schema(FqdnUrl()) >>> with raises(MultipleInvalid, 'expected a fully qualified domain name URL'): ... s("http://localhost/") >>> s('http://w3.org') 'http://w3.org' """ try: parsed_url = _url_validation(v) if "." not in parsed_url.netloc: raise UrlInvalid("must have a domain name in URL") return v except: # noqa: E722 raise ValueError @message('expected a URL', cls=UrlInvalid) def Url(v): """Verify that the value is a URL. >>> s = Schema(Url()) >>> with raises(MultipleInvalid, 'expected a URL'): ... s(1) >>> s('http://w3.org') 'http://w3.org' """ try: _url_validation(v) return v except: # noqa: E722 raise ValueError @message('Not a file', cls=FileInvalid) @truth def IsFile(v): """Verify the file exists. >>> os.path.basename(IsFile()(__file__)).startswith('validators.py') True >>> with raises(FileInvalid, 'Not a file'): ... IsFile()("random_filename_goes_here.py") >>> with raises(FileInvalid, 'Not a file'): ... IsFile()(None) """ try: if v: v = str(v) return os.path.isfile(v) else: raise FileInvalid('Not a file') except TypeError: raise FileInvalid('Not a file') @message('Not a directory', cls=DirInvalid) @truth def IsDir(v): """Verify the directory exists. >>> IsDir()('/') '/' >>> with raises(DirInvalid, 'Not a directory'): ... IsDir()(None) """ try: if v: v = str(v) return os.path.isdir(v) else: raise DirInvalid("Not a directory") except TypeError: raise DirInvalid("Not a directory") @message('path does not exist', cls=PathInvalid) @truth def PathExists(v): """Verify the path exists, regardless of its type. >>> os.path.basename(PathExists()(__file__)).startswith('validators.py') True >>> with raises(Invalid, 'path does not exist'): ... PathExists()("random_filename_goes_here.py") >>> with raises(PathInvalid, 'Not a Path'): ... PathExists()(None) """ try: if v: v = str(v) return os.path.exists(v) else: raise PathInvalid("Not a Path") except TypeError: raise PathInvalid("Not a Path") 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 None. >>> s = Schema(Maybe(int)) >>> s(10) 10 >>> with raises(Invalid): ... s("string") """ return Any(None, validator, msg=msg) NullableNumber = typing.Union[int, float, None] class Range(object): """Limit a value to a range. Either min or max may be omitted. Either min or max can be excluded from the range of accepted values. :raises Invalid: If the value is outside the range. >>> s = Schema(Range(min=1, max=10, min_included=False)) >>> s(5) 5 >>> s(10) 10 >>> with raises(MultipleInvalid, 'value must be at most 10'): ... s(20) >>> with raises(MultipleInvalid, 'value must be higher than 1'): ... s(1) >>> with raises(MultipleInvalid, 'value must be lower than 10'): ... Schema(Range(max=10, max_included=False))(20) """ 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 self.max_included = max_included self.msg = msg def __call__(self, v): try: 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 ) 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 ) 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 ) 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 ) 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)' ) 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, ) class Clamp(object): """Clamp a value to a range. Either min or max may be omitted. >>> s = Schema(Clamp(min=0, max=1)) >>> s(0.5) 0.5 >>> s(5) 1 >>> s(-1) 0 """ def __init__( self, min: NullableNumber = None, max: NullableNumber = None, msg: typing.Optional[str] = None, ) -> None: self.min = min self.max = max self.msg = msg def __call__(self, v): try: if self.min is not None and v < self.min: v = self.min if self.max is not None and v > self.max: v = 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)' ) def __repr__(self): return 'Clamp(min=%s, max=%s)' % (self.min, self.max) class Length(object): """The length of a value must be in a certain range.""" def __init__( self, min: NullableNumber = None, max: NullableNumber = None, msg: typing.Optional[str] = None, ) -> None: self.min = min self.max = max self.msg = msg def __call__(self, v): 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 ) 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 ) return v # Objects that have no length e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid(self.msg or 'invalid value or type') def __repr__(self): return 'Length(min=%s, max=%s)' % (self.min, self.max) class Datetime(object): """Validate that the value matches the datetime format.""" DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' def __init__( self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None ) -> None: self.format = format or self.DEFAULT_FORMAT self.msg = msg def __call__(self, v): try: datetime.datetime.strptime(v, self.format) except (TypeError, ValueError): raise DatetimeInvalid( self.msg or 'value does not match expected format %s' % self.format ) return v def __repr__(self): return 'Datetime(format=%s)' % self.format class Date(Datetime): """Validate that the value matches the date format.""" DEFAULT_FORMAT = '%Y-%m-%d' def __call__(self, v): try: datetime.datetime.strptime(v, self.format) except (TypeError, ValueError): raise DateInvalid( self.msg or 'value does not match expected format %s' % self.format ) return v def __repr__(self): return 'Date(format=%s)' % self.format class In(object): """Validate that a value is in a collection.""" def __init__( self, container: typing.Iterable, msg: typing.Optional[str] = None ) -> None: self.container = container self.msg = msg def __call__(self, v): try: check = v not in self.container except TypeError: check = True if check: 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): return 'In(%s)' % (self.container,) class NotIn(object): """Validate that a value is not in a collection.""" def __init__( self, container: typing.Iterable, msg: typing.Optional[str] = None ) -> None: self.container = container self.msg = msg def __call__(self, v): try: check = v in self.container except TypeError: check = True if check: 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): return 'NotIn(%s)' % (self.container,) class Contains(object): """Validate that the given schema element is in the sequence being validated. >>> s = Contains(1) >>> s([3, 2, 1]) [3, 2, 1] >>> with raises(ContainsInvalid, 'value is not allowed'): ... s([3, 2]) """ def __init__(self, item, msg: typing.Optional[str] = None) -> None: self.item = item self.msg = msg def __call__(self, v): try: check = self.item not in v except TypeError: check = True if check: raise ContainsInvalid(self.msg or 'value is not allowed') return v def __repr__(self): return 'Contains(%s)' % (self.item,) class ExactSequence(object): """Matches each element in a sequence against the corresponding element in the validators. :param msg: Message to deliver to user if validation fails. :param kwargs: All other keyword arguments are passed to the sub-schema constructors. >>> from voluptuous import Schema, ExactSequence >>> validate = Schema(ExactSequence([str, int, list, list])) >>> validate(['hourly_report', 10, [], []]) ['hourly_report', 10, [], []] >>> validate(('hourly_report', 10, [], [])) ('hourly_report', 10, [], []) """ 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] def __call__(self, v): if not isinstance(v, (list, tuple)) or len(v) != len(self._schemas): raise ExactSequenceInvalid(self.msg) try: v = type(v)(schema(x) for x, schema in zip(v, self._schemas)) except Invalid as e: raise e if self.msg is None else ExactSequenceInvalid(self.msg) return v def __repr__(self): return 'ExactSequence([%s])' % ", ".join(repr(v) for v in self.validators) class Unique(object): """Ensure an iterable does not contain duplicate items. 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. Unique allows ensuring that such array does not contain dupes. >>> s = Schema(Unique()) >>> s([]) [] >>> s([1, 2]) [1, 2] >>> with raises(Invalid, 'contains duplicate items: [1]'): ... s([1, 1, 2]) >>> with raises(Invalid, "contains duplicate items: ['one']"): ... s(['one', 'two', 'one']) >>> with raises(Invalid, regex="^contains unhashable elements: "): ... s([set([1, 2]), set([3, 4])]) >>> s('abc') 'abc' >>> with raises(Invalid, regex="^contains duplicate items: "): ... s('aabbc') """ 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)) 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)) return v def __repr__(self): return 'Unique()' class Equal(object): """Ensure that value matches target. >>> s = Schema(Equal(1)) >>> s(1) 1 >>> with raises(Invalid): ... s(2) Validators are not supported, match must be exact: >>> s = Schema(Equal(str)) >>> with raises(Invalid): ... s('foo') """ 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) ) return v def __repr__(self): return 'Equal({})'.format(self.target) class Unordered(object): """Ensures sequence contains values in unspecified order. >>> s = Schema(Unordered([2, 1])) >>> s([2, 1]) [2, 1] >>> s([1, 2]) [1, 2] >>> s = Schema(Unordered([str, int])) >>> s(['foo', 1]) ['foo', 1] >>> s([1, 'foo']) [1, 'foo'] """ 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] def __call__(self, v): if not isinstance(v, (list, tuple)): 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) ) ) consumed = set() missing = [] for index, value in enumerate(v): found = False for i, s in enumerate(self._schemas): if i in consumed: continue try: s(value) except Invalid: pass else: found = True consumed.add(i) break if not found: missing.append((index, value)) if len(missing) == 1: el = missing[0] 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 ] ) return v def __repr__(self): return 'Unordered([{}])'.format(", ".join(repr(v) for v in self.validators)) class Number(object): """ Verify the number of digits that are present in the number(Precision), and the decimal places(Scale). :raises Invalid: If the value does not match the provided Precision and Scale. >>> schema = Schema(Number(precision=6, scale=2)) >>> schema('1234.01') '1234.01' >>> schema = Schema(Number(precision=6, scale=2, yield_decimal=True)) >>> schema('1234.01') Decimal('1234.01') """ 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 self.yield_decimal = yield_decimal def __call__(self, v): """ :param v: is a number enclosed with string :return: Decimal number """ 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) ) else: if self.precision is not None and precision != 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) if self.yield_decimal: return decimal_num else: return v def __repr__(self): return 'Number(precision=%s, scale=%s, msg=%s)' % ( self.precision, self.scale, self.msg, ) def _get_precision_scale(self, number) -> typing.Tuple[int, int, Decimal]: """ :param number: :return: tuple(precision, scale, decimal_number) """ try: decimal_num = Decimal(number) except InvalidOperation: raise Invalid(self.msg or 'Value must be a number enclosed with string') 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): """Value must pass at least some validations, determined by the given parameter. Optionally, number of passed validations can be capped. The output of each validator is passed as input to the next. :param min_valid: Minimum number of valid schemas. :param validators: List of schemas or validators to match input against. :param max_valid: Maximum number of valid schemas. :param msg: Message to deliver to user if validation fails. :param kwargs: All other keyword arguments are passed to the sub-schema constructors. :raises NotEnoughValid: If the minimum number of validations isn't met. :raises TooManyValid: If the maximum number of validations is exceeded. >>> validate = Schema(SomeOf(min_valid=2, validators=[Range(1, 5), Any(float, int), 6.6])) >>> validate(6.6) 6.6 >>> validate(3) 3 >>> with raises(MultipleInvalid, 'value must be at most 5, not a valid value'): ... validate(6.2) """ 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) def _exec(self, funcs, v, path=None): errors = [] funcs = list(funcs) for func in funcs: try: if path is None: v = func(v) else: v = func(path, v) except Invalid as e: errors.append(e) passed_count = len(funcs) - len(errors) if self.min_valid <= passed_count <= self.max_valid: return v msg = self.msg if not msg: msg = ', '.join(map(str, errors)) if passed_count > self.max_valid: raise TooManyValid(msg) raise NotEnoughValid(msg) 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, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1706959351.2467322 voluptuous-0.14.2/voluptuous.egg-info/0000755000175000017500000000000014557420767017417 5ustar00philipphilip././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706959351.0 voluptuous-0.14.2/voluptuous.egg-info/PKG-INFO0000644000175000017500000005032314557420767020517 0ustar00philipphilipMetadata-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 [](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 ''.format(self) ... >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) >>> schema(Structure(q='one')) ``` ### Allow None values To allow value to be None as well, use Any: ```pycon >>> from voluptuous import Any >>> schema = Schema(Any(None, int)) >>> schema(None) >>> schema(5) 5 ``` ## Error reporting Validators must throw an `Invalid` exception if invalid data is passed to them. All other exceptions are treated as errors in the validator and will not be caught. Each `Invalid` exception has an associated `path` attribute representing the path in the data structure to our currently validating value, as well as an `error_message` attribute that contains the message of the original exception. This is especially useful when you want to catch `Invalid` exceptions and give some feedback to the user, for instance in the context of an HTTP API. ```pycon >>> def validate_email(email): ... """Validate email.""" ... if not "@" in email: ... raise Invalid("This email is invalid.") ... return email >>> schema = Schema({"email": validate_email}) >>> exc = None >>> try: ... schema({"email": "whatever"}) ... except MultipleInvalid as e: ... exc = e >>> str(exc) "This email is invalid. for dictionary value @ data['email']" >>> exc.path ['email'] >>> exc.msg 'This email is invalid.' >>> exc.error_message 'This email is invalid.' ``` The `path` attribute is used during error reporting, but also during matching to determine whether an error should be reported to the user or if the next match should be attempted. This is determined by comparing the depth of the path where the check is, to the depth of the path where the error occurred. If the error is more than one level deeper, it is reported. The upshot of this is that *matching is depth-first and fail-fast*. To illustrate this, here is an example schema: ```pycon >>> schema = Schema([[2, 3], 6]) ``` Each value in the top-level list is matched depth-first in-order. Given input data of `[[6]]`, the inner list will match the first element of the schema, but the literal `6` will not match any of the elements of that list. This error will be reported back to the user immediately. No backtracking is attempted: ```pycon >>> try: ... schema([[6]]) ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e >>> str(exc) == "not a valid value @ data[0][0]" True ``` If we pass the data `[6]`, the `6` is not a list type and so will not recurse into the first element of the schema. Matching will continue on to the second element in the schema, and succeed: ```pycon >>> schema([6]) [6] ``` ## 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706959351.0 voluptuous-0.14.2/voluptuous.egg-info/SOURCES.txt0000644000175000017500000000070214557420767021302 0ustar00philipphilipCHANGELOG.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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706959351.0 voluptuous-0.14.2/voluptuous.egg-info/dependency_links.txt0000644000175000017500000000000114557420767023465 0ustar00philipphilip ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1706959351.0 voluptuous-0.14.2/voluptuous.egg-info/top_level.txt0000644000175000017500000000001314557420767022143 0ustar00philipphilipvoluptuous