From 42a2390e635243e87f1cecd5d40438ecd22a694c Mon Sep 17 00:00:00 2001 From: minboost Date: Mon, 19 Jan 2015 15:13:04 -0800 Subject: [PATCH 001/261] Updated README for extra keys setting --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 60fc0715..e0151a0f 100644 --- a/README.md +++ b/README.md @@ -284,16 +284,27 @@ True ``` -This behaviour can be altered on a per-schema basis with -`Schema(..., extra=True)`: +This behaviour can be altered on a per-schema basis. To allow +additional keys use +`Schema(..., extra=ALLOW_EXTRA)`: ```pycon ->>> schema = Schema({2: 3}, extra=True) +>>> 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 +>>> 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: From 49cede786c284e9f4a44f782bb46fb31407dd836 Mon Sep 17 00:00:00 2001 From: minboost Date: Mon, 19 Jan 2015 22:34:24 -0800 Subject: [PATCH 002/261] Fixed docs for doctest --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e0151a0f..045b67f1 100644 --- a/README.md +++ b/README.md @@ -289,6 +289,7 @@ 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} @@ -299,6 +300,7 @@ 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} From be88e17c5253ee315c8ad8e0b42240983926fc7f Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Sat, 31 Jan 2015 00:10:58 +0000 Subject: [PATCH 003/261] Fixed spelling of word --- voluptuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index 0e0b11f2..76cf6b7d 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -293,7 +293,7 @@ class PathInvalid(Invalid): class LiteralInvalid(Invalid): - """The litteral values do not match.""" + """The literal values do not match.""" class Schema(object): From d98fa0ab002b5f2eaa28c7414641df38623c2010 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 4 Mar 2015 09:31:32 +1100 Subject: [PATCH 004/261] Aliases: Or = Any, And = All eg. Or(And(basestring, Length(max=30)), int) --- voluptuous.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/voluptuous.py b/voluptuous.py index 76cf6b7d..8434f052 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1,3 +1,4 @@ + # encoding: utf-8 # # Copyright (C) 2010-2013 Alec Thomas @@ -1296,6 +1297,10 @@ def f(v): return f +# Convenience alias +Or = Any + + def All(*validators, **kwargs): """Value must pass all validators. @@ -1321,6 +1326,10 @@ def f(v): return f +# Convenience alias +And = All + + def Match(pattern, msg=None): """Value must be a string that matches the regular expression. From d2b73dd2dd8a90c86d9ed78f3fa6340ada5603fb Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Thu, 16 Apr 2015 10:16:41 +0300 Subject: [PATCH 005/261] Added Unique and Set Unique takes any iterable, that can be converted into a set, and ensures it contains no duplicate elements. Set simply converts an iterable into a set. --- .gitignore | 1 + voluptuous.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/.gitignore b/.gitignore index 597a8052..62f5a1b1 100755 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist .coverage .tox MANIFEST +.idea diff --git a/voluptuous.py b/voluptuous.py index 8434f052..bda73ed4 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1651,6 +1651,69 @@ def __repr__(self): return repr(self.lit) +def Unique(msg=None): + """Ensure an iterable does not contain duplicate items. + + Only iterables convertable 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, "contains unhashable elements: unhashable type: 'set'"): + ... s([{1, 2}, {3, 4}]) + >>> s('abc') + 'abc' + >>> with raises(Invalid, "contains duplicate items: ['a', 'b']"): + ... s('aabbc') + """ + @wraps(Unique) + def f(v): + try: + set_v = set(v) + except TypeError as e: + raise TypeInvalid(msg or 'contains unhashable elements: {}'.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(msg or 'contains duplicate items: {}'.format(dupes)) + + return v + return f + + +def Set(msg=None): + """Convert a list into a set. + + >>> s = Schema(Set()) + >>> s([]) + set([]) + >>> s([1, 2]) + set([1, 2]) + >>> with raises(Invalid, "cannot be presented as set: unhashable type: 'set'"): + ... s([{1, 2}, {3, 4}]) + """ + @wraps(Set) + def f(v): + try: + set_v = set(v) + except Exception as e: + raise TypeInvalid(msg or 'cannot be presented as set: {}'.format(e)) + + return set_v + return f + + if __name__ == '__main__': import doctest doctest.testmod() From caf6002eda4916df57cc3c6ec5b4ee307d3a3496 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Fri, 17 Apr 2015 14:56:12 +0300 Subject: [PATCH 006/261] Fixed Unique and Set tests in other python versions --- voluptuous.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index bda73ed4..fc111694 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -111,12 +111,14 @@ @contextmanager -def raises(exc, msg=None): +def raises(exc, msg=None, regex=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) class Undefined(object): @@ -1669,11 +1671,11 @@ def Unique(msg=None): ... s([1, 1, 2]) >>> with raises(Invalid, "contains duplicate items: ['one']"): ... s(['one', 'two', 'one']) - >>> with raises(Invalid, "contains unhashable elements: unhashable type: 'set'"): - ... s([{1, 2}, {3, 4}]) + >>> with raises(Invalid, regex="^contains unhashable elements: "): + ... s([set([1, 2]), set([3, 4])]) >>> s('abc') 'abc' - >>> with raises(Invalid, "contains duplicate items: ['a', 'b']"): + >>> with raises(Invalid, regex="^contains duplicate items: "): ... s('aabbc') """ @wraps(Unique) @@ -1681,12 +1683,12 @@ def f(v): try: set_v = set(v) except TypeError as e: - raise TypeInvalid(msg or 'contains unhashable elements: {}'.format(e)) + raise TypeInvalid(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(msg or 'contains duplicate items: {}'.format(dupes)) + raise Invalid(msg or 'contains duplicate items: {0}'.format(dupes)) return v return f @@ -1696,19 +1698,19 @@ def Set(msg=None): """Convert a list into a set. >>> s = Schema(Set()) - >>> s([]) - set([]) - >>> s([1, 2]) - set([1, 2]) - >>> with raises(Invalid, "cannot be presented as set: unhashable type: 'set'"): - ... s([{1, 2}, {3, 4}]) + >>> 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])]) """ @wraps(Set) def f(v): try: set_v = set(v) except Exception as e: - raise TypeInvalid(msg or 'cannot be presented as set: {}'.format(e)) + raise TypeInvalid(msg or 'cannot be presented as set: {0}'.format(e)) return set_v return f From 112fba22fe67263e544ef352a08944e58aa395cc Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Tue, 12 May 2015 23:02:23 +0100 Subject: [PATCH 007/261] Fixes issue #118 by guarding against a TypeError: unhashable type: 'list' exception --- voluptuous.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index fc111694..10a4c45a 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1536,7 +1536,11 @@ def In(container, msg=None): """Validate that a value is in a collection.""" @wraps(In) def validator(value): - if value not in container: + try: + check = value not in container + except TypeError: + check = True + if check: raise InInvalid(msg or 'value is not allowed') return value return validator From f0585518cc7d6952c4a4ed22bbe376868a2e2e00 Mon Sep 17 00:00:00 2001 From: nramirezuy Date: Tue, 2 Jun 2015 17:27:21 -0300 Subject: [PATCH 008/261] ExactSequence fix for item assignment --- voluptuous.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 10a4c45a..36813fd2 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1622,6 +1622,8 @@ def ExactSequence(validators, **kwargs): >>> validate = Schema(ExactSequence([str, int, list, list])) >>> validate(['hourly_report', 10, [], []]) ['hourly_report', 10, [], []] + >>> validate(('hourly_report', 10, [], [])) + ('hourly_report', 10, [], []) """ msg = kwargs.pop('msg', None) schemas = [Schema(val, **kwargs) for val in validators] @@ -1630,8 +1632,7 @@ def f(v): if not isinstance(v, (list, tuple)): raise ExactSequenceInvalid(msg) try: - for i, schema in enumerate(schemas): - v[i] = schema(v[i]) + v = type(v)(schema(x) for x, schema in zip(v, schemas)) except Invalid as e: raise e if msg is None else ExactSequenceInvalid(msg) return v From 2544f06429b687395fb674e9cef7630688159d9b Mon Sep 17 00:00:00 2001 From: Richard O'Dwyer Date: Sun, 28 Jun 2015 22:13:31 +0100 Subject: [PATCH 009/261] Use https in setup --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index eb685671..b9d76839 100644 --- a/setup.py +++ b/setup.py @@ -25,8 +25,8 @@ setup( name='voluptuous', - url='http://github.com/alecthomas/voluptuous', - download_url='http://pypi.python.org/pypi/voluptuous', + url='https://github.com/alecthomas/voluptuous', + download_url='https://pypi.python.org/pypi/voluptuous', version=version, description=description, long_description=long_description, From d7f67a157940269c8b8a01ec05ef917f2b8ffb5d Mon Sep 17 00:00:00 2001 From: nat Date: Mon, 3 Aug 2015 21:09:57 -0400 Subject: [PATCH 010/261] Url scheme and host validation --- tests.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- voluptuous.py | 4 +++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index 9f72f0c5..9b04a631 100644 --- a/tests.py +++ b/tests.py @@ -3,7 +3,7 @@ import voluptuous from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, - MultipleInvalid, LiteralInvalid + Url, MultipleInvalid, LiteralInvalid ) @@ -111,3 +111,47 @@ def test_literal(): assert_equal(type(e.errors[0]), LiteralInvalid) else: assert False, "Did not raise Invalid" + + +def test_url_validation(): + """ test with valid URL """ + schema = Schema({"url": Url()}) + out_ = schema({"url": "http://example.com/"}) + + assert 'http://example.com/', out_.get("url") + + +def test_url_validation_with_none(): + """ test with invalid None url""" + schema = Schema({"url": Url()}) + try: + schema({"url": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_url_validation_with_empty_string(): + """ test with empty string URL """ + schema = Schema({"url": Url()}) + try: + schema({"url": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_url_validation_without_host(): + """ test with empty host URL """ + schema = Schema({"url": Url()}) + try: + schema({"url": 'http://'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" diff --git a/voluptuous.py b/voluptuous.py index 36813fd2..0c2caa33 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1391,7 +1391,9 @@ def Url(v): 'http://w3.org' """ try: - urlparse.urlparse(v) + parsed = urlparse.urlparse(v) + if not parsed.scheme or not parsed.netloc: + raise UrlInvalid("must have a URL scheme and host") return v except: raise ValueError From f92ec1861fc0d447829e6b52843f74c7c9f7ea45 Mon Sep 17 00:00:00 2001 From: Arthur Burkart Date: Sat, 12 Sep 2015 22:09:51 -0400 Subject: [PATCH 011/261] Adds string transform for stripping whitespace I find I make this transform a lot. It would be nice to add it to the framework --- voluptuous.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index 0c2caa33..8182cc49 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -85,10 +85,11 @@ True """ import datetime +import inspect import os import re import sys -import inspect + from contextlib import contextmanager from functools import wraps @@ -1588,6 +1589,16 @@ def Title(v): return str(v).title() +def Strip(v): + """Strip whitespace from a string. + + >>> s = Schema(Strip) + >>> s(' hello world ') + 'hello world' + """ + return str(v).strip() + + def DefaultTo(default_value, msg=None): """Sets a value to default_value if none provided. From 4340ffd6baf9b28f693fe68a0c16a77d9c210237 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 17 Sep 2015 15:58:19 +1000 Subject: [PATCH 012/261] Add SetTo(n) to force setting a value. Useful with Any(). Fixes #130. --- voluptuous.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/voluptuous.py b/voluptuous.py index 8182cc49..372aa4c2 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1619,6 +1619,23 @@ def f(v): return f +def SetTo(value): + """Set a value, ignoring any previous value. + + >>> s = Schema(Any(int, SetTo(42))) + >>> s(2) + 2 + >>> s("foo") + 42 + """ + value = default_factory(value) + + @wraps(SetTo) + def f(v): + return value() + return f + + class ExactSequenceInvalid(Invalid): pass From 095a44d0c0aa66e3eea49f94c13a0112dcc525f6 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Fri, 2 Oct 2015 15:15:30 -0700 Subject: [PATCH 013/261] Fix a TypeError when using a copy of dictionary fields. Previously, when using a copy of a dictionary of fields, a TypeError would be raised for invalid schemas, since the copy of `UNDEFINED` would not be the same object. This commit changes the comparison to `isinstance` to ensure that copies of `UNDEFINED` are correctly recognized as undefined. --- tests.py | 18 ++++++++++++++++++ voluptuous.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 9b04a631..642d7249 100644 --- a/tests.py +++ b/tests.py @@ -1,3 +1,4 @@ +import copy from nose.tools import assert_equal import voluptuous @@ -155,3 +156,20 @@ def test_url_validation_without_host(): "expected a URL for dictionary value @ data['url']") else: assert False, "Did not raise Invalid for empty string url" + + +def test_copy_dict_undefined(): + """ test with a copied dictionary """ + fields = { + Required("foo"): int + } + copied_fields = copy.deepcopy(fields) + + schema = Schema(copied_fields) + + # This used to raise a `TypeError` because the instance of `Undefined` + # was a copy, so object comparison would not work correctly. + try: + schema({"foo": "bar"}) + except Exception as e: + assert isinstance(e, MultipleInvalid) diff --git a/voluptuous.py b/voluptuous.py index 372aa4c2..0da9aa3d 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -458,7 +458,7 @@ def validate_mapping(path, iterable, out): # set defaults for any that can have defaults for key in default_keys: - if key.default != UNDEFINED: # if the user provides a default with the node + if not isinstance(key.default, Undefined): # if the user provides a default with the node out[key.schema] = key.default() if key in required_keys: required_keys.discard(key) From 44bb82507d6f16a0f2d31ce24f65d2067979e04d Mon Sep 17 00:00:00 2001 From: Frank Lazzarini Date: Mon, 2 Nov 2015 11:36:30 +0100 Subject: [PATCH 014/261] Implemented __lt__ on Marker objects --- voluptuous.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/voluptuous.py b/voluptuous.py index 0da9aa3d..a344b675 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -872,6 +872,9 @@ def __str__(self): def __repr__(self): return repr(self.schema) + def __lt__(self, other): + return self.schema < other.schema + class Optional(Marker): """Mark a node in the schema as optional, and optionally provide a default From 235e308f7bcb72844f5b4085b46c5257c791c1f1 Mon Sep 17 00:00:00 2001 From: Frank Lazzarini Date: Mon, 2 Nov 2015 11:37:02 +0100 Subject: [PATCH 015/261] Sorting unittest --- tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests.py b/tests.py index 642d7249..395ec67a 100644 --- a/tests.py +++ b/tests.py @@ -173,3 +173,13 @@ def test_copy_dict_undefined(): 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 From e75674df90b0b14b244608647c34b6009bc36289 Mon Sep 17 00:00:00 2001 From: Heikki Hokkanen Date: Sun, 6 Dec 2015 09:40:00 +0200 Subject: [PATCH 016/261] Mention use of Any to allow None values --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 045b67f1..e868c720 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,20 @@ attribute-value pair in the corresponding object: ``` +### 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 From f3891bc38558151b6e3c26aea416ebc17029dc5d Mon Sep 17 00:00:00 2001 From: Heikki Hokkanen Date: Mon, 7 Dec 2015 18:36:19 +0200 Subject: [PATCH 017/261] Implement Schema.extend --- tests.py | 26 ++++++++++++++++++++++++++ voluptuous.py | 19 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/tests.py b/tests.py index 395ec67a..b3527894 100644 --- a/tests.py +++ b/tests.py @@ -183,3 +183,29 @@ def test_sorting(): expected = [bar, foo] result = sorted(items) assert result == expected + + +def test_schema_extend(): + """Verify that Schema.extend copies schema keys from both.""" + + base = Schema({'a': int}, required=True) + extension = {'b': str} + extended = base.extend(extension) + + assert base.schema == {'a': int} + assert extension == {'b': str} + assert extended.schema == {'a': int, 'b': str} + assert extended.required == base.required + assert extended.extra == base.extra + + +def test_schema_extend_overrides(): + """Verify that Schema.extend can override required/extra parameters.""" + + base = Schema({'a': int}, required=True) + extended = base.extend({'b': str}, required=False, extra=voluptuous.ALLOW_EXTRA) + + assert base.required == True + assert base.extra == voluptuous.PREVENT_EXTRA + assert extended.required == False + assert extended.extra == voluptuous.ALLOW_EXTRA diff --git a/voluptuous.py b/voluptuous.py index a344b675..e4f1b6b6 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -716,6 +716,25 @@ def _compile_list(self, schema): """ return self._compile_sequence(schema, list) + def extend(self, schema, required=None, extra=None): + """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. + + :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` + """ + + result = self.schema.copy() + result.update(schema) + + result_required = (required if required is not None else self.required) + result_extra = (extra if extra is not None else self.extra) + return Schema(result, required=result_required, extra=result_extra) + def _compile_scalar(schema): """A scalar value. From 88e3ca5d50f85d5646c89fdcfbb3b2b2ddb7cb27 Mon Sep 17 00:00:00 2001 From: Heikki Hokkanen Date: Mon, 7 Dec 2015 18:48:13 +0200 Subject: [PATCH 018/261] README: Document Schema.extend --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 045b67f1..366f0a99 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,23 @@ 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 From d5a7e27dace1fb13194a239dfe84be25fc18bcb1 Mon Sep 17 00:00:00 2001 From: Heikki Hokkanen Date: Tue, 8 Dec 2015 20:33:04 +0200 Subject: [PATCH 019/261] Schema.extend works only for dicts --- voluptuous.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/voluptuous.py b/voluptuous.py index e4f1b6b6..5cc6a7cd 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -723,11 +723,15 @@ def extend(self, schema, required=None, extra=None): 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 type(self.schema) == dict and type(schema) == dict, 'Both schemas must be dictionary-based' + result = self.schema.copy() result.update(schema) From a4ed88636cb2d343ea2bfedfe2bcb07385ccc221 Mon Sep 17 00:00:00 2001 From: Jimmy Ngo Date: Mon, 14 Dec 2015 20:11:43 -0800 Subject: [PATCH 020/261] Have iteritems work with any Mapping class that implements iteritems --- voluptuous.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 5cc6a7cd..7fe839ae 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -100,11 +100,11 @@ unicode = str basestring = str ifilter = filter - iteritems = dict.items + iteritems_attr = 'items' else: from itertools import ifilter import urlparse - iteritems = dict.iteritems + iteritems_attr = 'iteritems' __author__ = 'Alec Thomas ' @@ -139,6 +139,11 @@ def default_factory(value): return lambda: value +def iteritems(mapping): + """Return iteritems for Mappings.""" + return getattr(mapping, iteritems_attr)() + + # 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 From 3d880a2d759e039288769a64ee0d634b722ea31a Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 16 Dec 2015 01:08:22 +1100 Subject: [PATCH 021/261] Fix some bugs. Including #71. --- setup.py | 2 +- voluptuous.py | 13 ++++--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index b9d76839..2fc07251 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ with open('README.rst', 'w') as f: f.write(long_description) atexit.register(lambda: os.unlink('README.rst')) -except ImportError: +except (ImportError, OSError): print('WARNING: Could not locate pandoc, using Markdown long_description.') with open('README.md') as f: long_description = f.read() diff --git a/voluptuous.py b/voluptuous.py index 7fe839ae..a137cba6 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -84,12 +84,12 @@ ... 'Users': {'snmp_community': 'monkey'}}}} True """ +import collections import datetime import inspect import os import re import sys - from contextlib import contextmanager from functools import wraps @@ -100,11 +100,11 @@ unicode = str basestring = str ifilter = filter - iteritems_attr = 'items' + iteritems = lambda d: d.items() else: from itertools import ifilter import urlparse - iteritems_attr = 'iteritems' + iteritems = lambda d: d.iteritems() __author__ = 'Alec Thomas ' @@ -139,11 +139,6 @@ def default_factory(value): return lambda: value -def iteritems(mapping): - """Return iteritems for Mappings.""" - return getattr(mapping, iteritems_attr)() - - # 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 @@ -351,7 +346,7 @@ def _compile(self, schema): return lambda _, v: v if isinstance(schema, Object): return self._compile_object(schema) - if isinstance(schema, dict): + if isinstance(schema, collections.Mapping): return self._compile_dict(schema) elif isinstance(schema, list): return self._compile_list(schema) From c6d4adefed5ac2ea97451eb0afe6f0f8653f8e2b Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 16 Dec 2015 01:10:19 +1100 Subject: [PATCH 022/261] Bump version. --- voluptuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index a137cba6..4c5902d8 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -108,7 +108,7 @@ __author__ = 'Alec Thomas ' -__version__ = '0.8.7' +__version__ = '0.8.8' @contextmanager From 314a9f73e717bee42925cfdffd41912b68c4da76 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 26 Dec 2015 13:47:58 +1100 Subject: [PATCH 023/261] Support bool literals. Fixes #75. --- voluptuous.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 4c5902d8..2b0951bc 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1,4 +1,3 @@ - # encoding: utf-8 # # Copyright (C) 2010-2013 Alec Thomas @@ -355,7 +354,7 @@ def _compile(self, schema): type_ = type(schema) if type_ is type: type_ = schema - if type_ in (int, long, str, unicode, float, complex, object, + if type_ in (bool, int, long, str, unicode, float, complex, object, list, dict, type(None)) or callable(schema): return _compile_scalar(schema) raise SchemaError('unsupported schema data type %r' % From 6dee4ec811e5437b217ab2419c15bd0efa1a54d5 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 20 Jan 2016 09:56:16 +1100 Subject: [PATCH 024/261] Correctly report error location with inclusive/exclusive. Fixes #143. --- voluptuous.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 2b0951bc..4b9bc01e 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -299,6 +299,14 @@ class LiteralInvalid(Invalid): """The literal values do not match.""" +class VirtualPathComponent(str): + def __str__(self): + return '<' + self + '>' + + def __repr__(self): + return self.__str__() + + class Schema(object): """A validation schema. @@ -606,7 +614,8 @@ def validate_dict(path, 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 - errors.append(ExclusiveInvalid(msg, path)) + next_path = path + [VirtualPathComponent(label)] + errors.append(ExclusiveInvalid(msg, next_path)) break exists = True @@ -624,7 +633,8 @@ def validate_dict(path, data): if msg is None: msg = ("some but not all values in the same group of " "inclusion '%s'") % label - errors.append(InclusiveInvalid(msg, path)) + next_path = path + [VirtualPathComponent(label)] + errors.append(InclusiveInvalid(msg, next_path)) break if errors: @@ -936,7 +946,7 @@ class Exclusive(Optional): Keys inside a same group of exclusion cannot be together, it only makes sense for dictionaries: - >>> with raises(MultipleInvalid, "two or more values in the same group of exclusion 'angles'"): + >>> with raises(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: @@ -956,7 +966,7 @@ class Exclusive(Optional): ... } ... }) - >>> with raises(MultipleInvalid, "Please, use only one type of authentication at the same time."): + >>> with raises(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'}}) """ @@ -980,7 +990,7 @@ class Inclusive(Optional): Keys inside a same group of inclusive must exist together, it only makes sense for dictionaries: - >>> with raises(MultipleInvalid, "some but not all values in the same group of inclusion 'file'"): + >>> with raises(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: @@ -990,16 +1000,16 @@ class Inclusive(Optional): For example, API can return 'height' and 'width' together, but not separately. - >>> msg = 'Height and width must exist together' + >>> msg = "Height and width must exist together" >>> schema = Schema({ ... Inclusive('height', 'size', msg=msg): int, ... Inclusive('width', 'size', msg=msg): int ... }) - >>> with raises(MultipleInvalid, msg): + >>> with raises(MultipleInvalid, msg + " @ data[]"): ... schema({'height': 100}) - >>> with raises(MultipleInvalid, msg): + >>> with raises(MultipleInvalid, msg + " @ data[]"): ... schema({'width': 100}) >>> data = {'height': 100, 'width': 100} From f2241ce5edf6e16b3f3456f08f9b5bd7afe64a8d Mon Sep 17 00:00:00 2001 From: Peter Marsh Date: Wed, 20 Jan 2016 13:18:32 +0000 Subject: [PATCH 025/261] Add NotIn validation Relatively self explanitory, this is a valdiator that ensures values are not in a given collection of values, e.g. schema = Schema({"color": NotIn(frozenset(["blue", "red", "yellow"]))}) schema({"color": "orange"}) # valid schema({"color": "blue"}) # invalid --- tests.py | 14 +++++++++++++- voluptuous.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index b3527894..249f7fe2 100644 --- a/tests.py +++ b/tests.py @@ -4,7 +4,7 @@ import voluptuous from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, - Url, MultipleInvalid, LiteralInvalid + Url, MultipleInvalid, LiteralInvalid, NotIn ) @@ -46,6 +46,18 @@ def test_in(): schema({"color": "blue"}) +def test_not_in(): + """Verify that NotIn works.""" + schema = Schema({"color": NotIn(frozenset(["blue", "red", "yellow"]))}) + schema({"color": "orange"}) + try: + schema({"color": "blue"}) + except Invalid as e: + assert_equal(str(e), "value is not allowed for dictionary value @ data['color']") + else: + assert False, "Did not raise NotInInvalid" + + def test_remove(): """Verify that Remove works.""" # remove dict keys diff --git a/voluptuous.py b/voluptuous.py index 4b9bc01e..05abba0b 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1584,6 +1584,24 @@ def validator(value): return validator +class NotInInvalid(Invalid): + pass + + +def NotIn(container, msg=None): + """Validate that a value is not in a collection.""" + @wraps(NotIn) + def validator(value): + try: + check = value in container + except TypeError: + check = True + if check: + raise NotInInvalid(msg or 'value is not allowed') + return value + return validator + + def Lower(v): """Transform a string to lower case. From 0e0c25b49c3e95c28071e7d417cd112370edee20 Mon Sep 17 00:00:00 2001 From: Cameron Waeland Date: Thu, 28 Jan 2016 18:26:56 -0500 Subject: [PATCH 026/261] Added additional @wraps where appropriate * Improves introspectability of `All`, `Match`, `Replace`, and `ExactSequence` * Also makes them consistent with the other validation functions --- voluptuous.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/voluptuous.py b/voluptuous.py index 05abba0b..16705680 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1354,6 +1354,7 @@ def All(*validators, **kwargs): msg = kwargs.pop('msg', None) schemas = [Schema(val, **kwargs) for val in validators] + @wraps(All) def f(v): try: for schema in schemas: @@ -1389,6 +1390,7 @@ def Match(pattern, msg=None): if isinstance(pattern, basestring): pattern = re.compile(pattern) + @wraps(Match) def f(v): try: match = pattern.match(v) @@ -1411,6 +1413,7 @@ def Replace(pattern, substitution, msg=None): if isinstance(pattern, basestring): pattern = re.compile(pattern) + @wraps(Replace) def f(v): return pattern.sub(substitution, v) return f @@ -1711,6 +1714,7 @@ def ExactSequence(validators, **kwargs): msg = kwargs.pop('msg', None) schemas = [Schema(val, **kwargs) for val in validators] + @wraps(ExactSequence) def f(v): if not isinstance(v, (list, tuple)): raise ExactSequenceInvalid(msg) From 5eccbef584f30c366e1a778950848f7528acb9af Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 12 Feb 2016 18:44:11 -0800 Subject: [PATCH 027/261] Make many of the validators have useful __repr__ Instead of having many validators be inner functions with non-useful __repr__ instead rework those functions to be instances of classes with __call__ methods instead so that those objects now can have meaningful __repr__ methods that can be useful for seeing exactly what a schema is composed of. Fixes #146 --- voluptuous.py | 396 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 258 insertions(+), 138 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 16705680..64f33534 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -318,6 +318,12 @@ class Schema(object): validate and optionally convert the value. """ + _extra_to_name = { + REMOVE_EXTRA: 'REMOVE_EXTRA', + ALLOW_EXTRA: 'ALLOW_EXTRA', + PREVENT_EXTRA: 'PREVENT_EXTRA', + } + def __init__(self, schema, required=False, extra=PREVENT_EXTRA): """Create a new Schema. @@ -338,6 +344,11 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA): self.extra = int(extra) # ensure the value is an integer self._compiled = self._compile(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: @@ -1059,6 +1070,9 @@ def __call__(self, v): super(Remove, self).__call__(v) return self.__class__ + def __repr__(self): + return "Remove(%r)" % (self.schema,) + def Extra(_): """Allow keys in the data that are not present in the schema.""" @@ -1069,8 +1083,7 @@ def Extra(_): # deprecated object, so we just leave an alias here instead. extra = Extra - -def Msg(schema, msg, cls=None): +class Msg(object): """Report a user-friendly message if a schema fails to validate. >>> validate = Schema( @@ -1099,21 +1112,26 @@ def Msg(schema, msg, cls=None): ... assert isinstance(e.errors[0], RangeInvalid) """ - schema = Schema(schema) - - if cls and not issubclass(cls, Invalid): - raise SchemaError("Msg can only use subclases of Invalid as custom class") + def __init__(self, schema, msg, cls=None): + if cls and not issubclass(cls, Invalid): + raise SchemaError("Msg can only use subclases of" + " Invalid as custom class") + self._schema = schema + self.schema = Schema(schema) + self.msg = msg + self.cls = cls - @wraps(Msg) - def f(v): + def __call__(self, v): try: - return schema(v) + return self.schema(v) except Invalid as e: if len(e.path) > 1: raise e else: - raise (cls or Invalid)(msg) - return f + raise (self.cls or Invalid)(self.msg) + + def __repr__(self): + return 'Msg(%s, %s, cls=%s)' % (self._schema, self.msg, self.cls) def message(default=None, cls=None): @@ -1182,13 +1200,12 @@ def check(v): return check -def Coerce(type, msg=None): +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)) @@ -1203,13 +1220,21 @@ def Coerce(type, msg=None): >>> with raises(MultipleInvalid, 'moo'): ... validate('foo') """ - @wraps(Coerce) - def f(v): + + def __init__(self, type, msg=None): + self.type = type + self.msg = msg + self.type_name = type.__name__ + + def __call__(self, v): try: - return type(v) + return self.type(v) except (ValueError, TypeError): - raise CoerceInvalid(msg or ('expected %s' % type.__name__)) - return f + msg = self.msg or ('expected %s' % self.type_name) + raise CoerceInvalid(msg) + + def __repr__(self): + return 'Coerce(%s)' % (self.type_name) @message('value was not true', cls=TrueInvalid) @@ -1292,7 +1317,7 @@ def Boolean(v): return bool(v) -def Any(*validators, **kwargs): +class Any(object): """Use the first validated value. :param msg: Message to deliver to user if validation fails. @@ -1316,13 +1341,15 @@ def Any(*validators, **kwargs): >>> with raises(MultipleInvalid, "Expected 1 2 or 3"): ... validate(4) """ - msg = kwargs.pop('msg', None) - schemas = [Schema(val, **kwargs) for val in validators] - @wraps(Any) - def f(v): + def __init__(self, *validators, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): error = None - for schema in schemas: + for schema in self._schemas: try: return schema(v) except Invalid as e: @@ -1330,16 +1357,18 @@ def f(v): error = e else: if error: - raise error if msg is None else AnyInvalid(msg) - raise AnyInvalid(msg or 'no valid value found') - return f + raise error if self.msg is None else AnyInvalid(self.msg) + raise AnyInvalid(self.msg or 'no valid value found') + + def __repr__(self): + return 'Any([%s])' % (", ".join(repr(v) for v in self.validators)) # Convenience alias Or = Any -def All(*validators, **kwargs): +class All(object): """Value must pass all validators. The output of each validator is passed as input to the next. @@ -1351,25 +1380,29 @@ def All(*validators, **kwargs): >>> validate('10') 10 """ - msg = kwargs.pop('msg', None) - schemas = [Schema(val, **kwargs) for val in validators] - @wraps(All) - def f(v): + def __init__(self, *validators, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): try: - for schema in schemas: + for schema in self._schemas: v = schema(v) except Invalid as e: - raise e if msg is None else AllInvalid(msg) + raise e if self.msg is None else AllInvalid(self.msg) return v - return f + + def __repr__(self): + return 'All([%s])' % (", ".join(repr(v) for v in self.validators)) # Convenience alias And = All -def Match(pattern, msg=None): +class Match(object): """Value must be a string that matches the regular expression. >>> validate = Schema(Match(r'^0x[A-F0-9]+$')) @@ -1387,22 +1420,27 @@ def Match(pattern, msg=None): >>> validate('0x123ef4') '0x123ef4' """ - if isinstance(pattern, basestring): - pattern = re.compile(pattern) - @wraps(Match) - def f(v): + def __init__(self, pattern, msg=None): + if isinstance(pattern, basestring): + pattern = re.compile(pattern) + self.pattern = pattern + self.msg = msg + + def __call__(self, v): try: - match = pattern.match(v) + match = self.pattern.match(v) except TypeError: raise MatchInvalid("expected string or buffer") if not match: - raise MatchInvalid(msg or 'does not match regular expression') + raise MatchInvalid(self.msg or 'does not match regular expression') return v - return f + + def __repr__(self): + return 'Match(%s)' % (self.pattern.pattern) -def Replace(pattern, substitution, msg=None): +class Replace(object): """Regex substitution. >>> validate = Schema(All(Replace('you', 'I'), @@ -1410,13 +1448,20 @@ def Replace(pattern, substitution, msg=None): >>> validate('you say hello') 'I say goodbye' """ - if isinstance(pattern, basestring): - pattern = re.compile(pattern) - @wraps(Replace) - def f(v): - return pattern.sub(substitution, v) - return f + def __init__(self, pattern, substitution, msg=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(%s, %s)' % (self.pattern.pattern, + self.substitution) @message('expected a URL', cls=UrlInvalid) @@ -1432,7 +1477,7 @@ def Url(v): try: parsed = urlparse.urlparse(v) if not parsed.scheme or not parsed.netloc: - raise UrlInvalid("must have a URL scheme and host") + raise UrlInvalid("must have a URL scheme and host") return v except: raise ValueError @@ -1475,7 +1520,7 @@ def PathExists(v): return os.path.exists(v) -def Range(min=None, max=None, min_included=True, max_included=True, msg=None): +class Range(object): """Limit a value to a range. Either min or max may be omitted. @@ -1495,25 +1540,42 @@ def Range(min=None, max=None, min_included=True, max_included=True, msg=None): >>> with raises(MultipleInvalid, 'value must be lower than 10'): ... Schema(Range(max=10, max_included=False))(20) """ - @wraps(Range) - def f(v): - if min_included: - if min is not None and v < min: - raise RangeInvalid(msg or 'value must be at least %s' % min) + + def __init__(self, min=None, max=None, min_included=True, + max_included=True, msg=None): + self.min = min + self.max = max + self.min_included = min_included + self.max_included = max_included + self.msg = msg + + def __call__(self, v): + if self.min_included: + if self.min is not None and v < self.min: + raise RangeInvalid( + self.msg or 'value must be at least %s' % self.min) else: - if min is not None and v <= min: - raise RangeInvalid(msg or 'value must be higher than %s' % min) - if max_included: - if max is not None and v > max: - raise RangeInvalid(msg or 'value must be at most %s' % max) + if self.min is not None and 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 v > self.max: + raise RangeInvalid( + self.msg or 'value must be at most %s' % self.max) else: - if max is not None and v >= max: - raise RangeInvalid(msg or 'value must be lower than %s' % max) + if self.max is not None and v >= self.max: + raise RangeInvalid( + self.msg or 'value must be lower than %s' % self.max) return v - return f + + def __repr__(self): + return ('Range(min=%s, max=%s, min_included=%s,' + ' max_included=%s)' (self.min, self.max, + self.min_included, + self.max_included)) -def Clamp(min=None, max=None, msg=None): +class Clamp(object): """Clamp a value to a range. Either min or max may be omitted. @@ -1524,85 +1586,121 @@ def Clamp(min=None, max=None, msg=None): 1 >>> s(-1) 0 - """ - @wraps(Clamp) - def f(v): - if min is not None and v < min: - v = min - if max is not None and v > max: - v = max + + def __init__(self, min=None, max=None, msg=None): + self.min = min + self.max = max + self.msg = msg + + def __call__(self, v): + 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 - return f + + def __repr__(self): + return 'Clamp(min=%s, max=%s)' % (self.min, self.max) class LengthInvalid(Invalid): pass -def Length(min=None, max=None, msg=None): +class Length(object): """The length of a value must be in a certain range.""" - @wraps(Length) - def f(v): - if min is not None and len(v) < min: - raise LengthInvalid(msg or 'length of value must be at least %s' % min) - if max is not None and len(v) > max: - raise LengthInvalid(msg or 'length of value must be at most %s' % max) + + def __init__(self, min=None, max=None, msg=None): + self.min = min + self.max = max + self.msg = msg + + def __call__(self, v): + 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 - return f + + def __repr__(self): + return 'Length(min=%s, max=%s)' % (self.min, self.max) class DatetimeInvalid(Invalid): """The value is not a formatted datetime string.""" -def Datetime(format=None, msg=None): +class Datetime(object): """Validate that the value matches the datetime format.""" - @wraps(Datetime) - def f(v): - check_format = format or '%Y-%m-%dT%H:%M:%S.%fZ' + + DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' + + def __init__(self, format=None, msg=None): + self.format = format or self.DEFAULT_FORMAT + self.msg = msg + + def __call__(self, v): try: - datetime.datetime.strptime(v, check_format) + datetime.datetime.strptime(v, self.format) except (TypeError, ValueError): - raise DatetimeInvalid(msg or 'value does not match expected format %s' % check_format) + raise DatetimeInvalid( + self.msg or 'value does not match' + ' expected format %s' % self.format) return v - return f + + def __repr__(self): + return 'Datetime(format=%s)' % self.format class InInvalid(Invalid): pass -def In(container, msg=None): +class In(object): """Validate that a value is in a collection.""" - @wraps(In) - def validator(value): + + def __init__(self, container, msg=None): + self.container = container + self.msg = msg + + def __call__(self, v): try: - check = value not in container + check = v not in self.container except TypeError: check = True if check: - raise InInvalid(msg or 'value is not allowed') - return value - return validator + raise InInvalid(self.msg or 'value is not allowed') + return v + + def __repr__(self): + return 'In(%s)' % (self.container,) class NotInInvalid(Invalid): pass -def NotIn(container, msg=None): +class NotIn(object): """Validate that a value is not in a collection.""" - @wraps(NotIn) - def validator(value): + + def __init__(self, container, msg=None): + self.container = container + self.msg = msg + + def __call__(self, v): try: - check = value in container + check = v in self.container except TypeError: check = True if check: - raise NotInInvalid(msg or 'value is not allowed') - return value - return validator + raise NotInInvalid(self.msg or 'value is not allowed') + return v + + def __repr__(self): + return 'NotIn(%s)' % (self.container,) def Lower(v): @@ -1655,7 +1753,7 @@ def Strip(v): return str(v).strip() -def DefaultTo(default_value, msg=None): +class DefaultTo(object): """Sets a value to default_value if none provided. >>> s = Schema(DefaultTo(42)) @@ -1665,17 +1763,21 @@ def DefaultTo(default_value, msg=None): >>> s(None) [] """ - default_value = default_factory(default_value) - @wraps(DefaultTo) - def f(v): + def __init__(self, default_value, msg=None): + self.default_value = default_factory(default_value) + self.msg = msg + + def __call__(self, v): if v is None: - v = default_value() + v = self.default_value() return v - return f + + def __repr__(self): + return 'DefaultTo(%s)' % (self.default_value(),) -def SetTo(value): +class SetTo(object): """Set a value, ignoring any previous value. >>> s = Schema(Any(int, SetTo(42))) @@ -1684,19 +1786,22 @@ def SetTo(value): >>> s("foo") 42 """ - value = default_factory(value) - @wraps(SetTo) - def f(v): - return value() - return f + def __init__(self, value): + self.value = default_factory(value) + + def __call__(self, v): + return self.value() + + def __repr__(self): + return 'SetTo(%s)' % (self.value(),) class ExactSequenceInvalid(Invalid): pass -def ExactSequence(validators, **kwargs): +class ExactSequence(object): """Matches each element in a sequence against the corresponding element in the validators. @@ -1711,19 +1816,24 @@ def ExactSequence(validators, **kwargs): >>> validate(('hourly_report', 10, [], [])) ('hourly_report', 10, [], []) """ - msg = kwargs.pop('msg', None) - schemas = [Schema(val, **kwargs) for val in validators] - @wraps(ExactSequence) - def f(v): + def __init__(self, validators, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): if not isinstance(v, (list, tuple)): - raise ExactSequenceInvalid(msg) + raise ExactSequenceInvalid(self.msg) try: - v = type(v)(schema(x) for x, schema in zip(v, schemas)) + v = type(v)(schema(x) for x, schema in zip(v, self._schemas)) except Invalid as e: - raise e if msg is None else ExactSequenceInvalid(msg) + raise e if self.msg is None else ExactSequenceInvalid(self.msg) return v - return f + + def __repr__(self): + return 'ExactSequence([%s])' % (", ".join(repr(v) + for v in self.validators)) class Literal(object): @@ -1745,7 +1855,7 @@ def __repr__(self): return repr(self.lit) -def Unique(msg=None): +class Unique(object): """Ensure an iterable does not contain duplicate items. Only iterables convertable to a set are supported (native types and @@ -1770,23 +1880,28 @@ def Unique(msg=None): >>> with raises(Invalid, regex="^contains duplicate items: "): ... s('aabbc') """ - @wraps(Unique) - def f(v): + + def __init__(self, msg=None): + self.msg = msg + + def __call__(self, v): try: set_v = set(v) except TypeError as e: - raise TypeInvalid(msg or 'contains unhashable elements: {0}'.format(e)) - + raise TypeInvalid( + self.msg or 'contains unhashable elements: {0}'.format(e)) if len(set_v) != len(v): seen = set() dupes = list(set(x for x in v if x in seen or seen.add(x))) - raise Invalid(msg or 'contains duplicate items: {0}'.format(dupes)) - + raise Invalid( + self.msg or 'contains duplicate items: {0}'.format(dupes)) return v - return f + + def __repr__(self): + return 'Unique()' -def Set(msg=None): +class Set(object): """Convert a list into a set. >>> s = Schema(Set()) @@ -1797,15 +1912,20 @@ def Set(msg=None): >>> with raises(Invalid, regex="^cannot be presented as set: "): ... s([set([1, 2]), set([3, 4])]) """ - @wraps(Set) - def f(v): + + def __init__(self, msg=None): + self.msg = msg + + def __call__(self, v): try: set_v = set(v) except Exception as e: - raise TypeInvalid(msg or 'cannot be presented as set: {0}'.format(e)) - + raise TypeInvalid( + self.msg or 'cannot be presented as set: {0}'.format(e)) return set_v - return f + + def __repr__(self): + return 'Set()' if __name__ == '__main__': From 8f91dd3908dad5987cde0814781b3cde5a51b80b Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Mon, 7 Mar 2016 11:50:26 +1100 Subject: [PATCH 028/261] 0.8.9 release. Fixes #149. --- voluptuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index 64f33534..6e9362a3 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -107,7 +107,7 @@ __author__ = 'Alec Thomas ' -__version__ = '0.8.8' +__version__ = '0.8.9' @contextmanager From e8b7693c5be1b0b701561a574388cff8f7a71fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1o=20Belica?= Date: Mon, 7 Mar 2016 14:01:52 +0100 Subject: [PATCH 029/261] Added recursive schema into README #128 --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 5abf336f..d9911257 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,21 @@ True ``` +### Recursive schema + +There is no syntax to have a recursive schema. The best way to do it is to have a wrapper like this: + +```pycon +>>> from voluptuous import Schema, Any +>>> def s2(v): +... return s1(v) +... +>>> s1 = Schema({"key": Any(s2, "value")}) +>>> s1({"key": {"key": "value"}}) +{'key': {'key': 'value'}} + +``` + ### Extending an existing Schema Often it comes handy to have a base `Schema` that is extended with more From 15c3ec50f4b170a410e293c4628125c072e23657 Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Wed, 23 Mar 2016 10:59:00 +0100 Subject: [PATCH 030/261] Run static analysis on the exact source set It prevents running flake8 on the entire virtualenv when located at top source directory. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dcc0800d..db8cc556 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ commands = [testenv:flake8] deps = flake8 -commands = flake8 +commands = flake8 setup.py voluptuous.py tests.py [testenv:py26] basepython = python2.6 From 149da0b597e942d6d76ed38c2d6520ce30b9d82e Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Wed, 23 Mar 2016 11:04:03 +0100 Subject: [PATCH 031/261] Add missing % formatting character --- voluptuous.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 6e9362a3..78cc4d26 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1570,9 +1570,9 @@ def __call__(self, v): def __repr__(self): return ('Range(min=%s, max=%s, min_included=%s,' - ' max_included=%s)' (self.min, self.max, - self.min_included, - self.max_included)) + ' max_included=%s)' % (self.min, self.max, + self.min_included, + self.max_included)) class Clamp(object): From 13a6bf1e6a375cfa41e1975677ed0a1f80528c7d Mon Sep 17 00:00:00 2001 From: Tristan Carel Date: Wed, 23 Mar 2016 10:55:22 +0100 Subject: [PATCH 032/261] Improve `repr` representation of some classes * returned strings are now valid Python expressions * keep the `msg` parameter --- tests.py | 23 ++++++++++++++++++++++- voluptuous.py | 23 ++++++++++++++--------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/tests.py b/tests.py index 249f7fe2..0dd4e9d8 100644 --- a/tests.py +++ b/tests.py @@ -4,7 +4,8 @@ import voluptuous from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, - Url, MultipleInvalid, LiteralInvalid, NotIn + Url, MultipleInvalid, LiteralInvalid, NotIn, Match, + Replace, Range, Coerce, All, ) @@ -221,3 +222,23 @@ def test_schema_extend_overrides(): assert base.extra == voluptuous.PREVENT_EXTRA assert extended.required == False assert extended.extra == voluptuous.ALLOW_EXTRA + + +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') + + assert_equal(repr(match), "Match('a pattern', msg='message')") + assert_equal(repr(replace), "Replace('you', 'I', msg='you and I')") + assert_equal( + repr(range_), + "Range(min=0, max=42, min_included=False, " \ + "max_included=False, msg='number not in range')" + ) + assert_equal(repr(coerce_), "Coerce(int, msg='moo')") + assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") diff --git a/voluptuous.py b/voluptuous.py index 6e9362a3..3ab4de4a 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1234,7 +1234,7 @@ def __call__(self, v): raise CoerceInvalid(msg) def __repr__(self): - return 'Coerce(%s)' % (self.type_name) + return 'Coerce(%s, msg=%r)' % (self.type_name, self.msg) @message('value was not true', cls=TrueInvalid) @@ -1395,7 +1395,10 @@ def __call__(self, v): return v def __repr__(self): - return 'All([%s])' % (", ".join(repr(v) for v in self.validators)) + return 'All(%s, msg=%r)' % ( + ", ".join(repr(v) for v in self.validators), + self.msg + ) # Convenience alias @@ -1437,7 +1440,7 @@ def __call__(self, v): return v def __repr__(self): - return 'Match(%s)' % (self.pattern.pattern) + return 'Match(%r, msg=%r)' % (self.pattern.pattern, self.msg) class Replace(object): @@ -1460,8 +1463,9 @@ def __call__(self, v): return self.pattern.sub(self.substitution, v) def __repr__(self): - return 'Replace(%s, %s)' % (self.pattern.pattern, - self.substitution) + return 'Replace(%r, %r, msg=%r)' % (self.pattern.pattern, + self.substitution, + self.msg) @message('expected a URL', cls=UrlInvalid) @@ -1569,10 +1573,11 @@ def __call__(self, v): return v def __repr__(self): - return ('Range(min=%s, max=%s, min_included=%s,' - ' max_included=%s)' (self.min, self.max, - self.min_included, - self.max_included)) + 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): From cfd51825e2a81fbd0f4f1720673bf7f532f09192 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 29 Mar 2016 08:25:18 +0100 Subject: [PATCH 033/261] Prevent individual errors from validating a list's items from being lost. This improves error messages seen when using custom validators for items in sequences. --- README.md | 2 +- tests.md | 9 ++++++--- tests.py | 23 ++++++++++++++++++++++- voluptuous.py | 10 ++++++---- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d9911257..74490c2a 100644 --- a/README.md +++ b/README.md @@ -522,7 +522,7 @@ backtracking is attempted: ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e ->>> str(exc) == "invalid list value @ data[0][0]" +>>> str(exc) == "not a valid value @ data[0][0]" True ``` diff --git a/tests.md b/tests.md index f098c1b4..b55b1827 100644 --- a/tests.md +++ b/tests.md @@ -16,7 +16,7 @@ value: ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e - >>> str(exc) == 'invalid list value @ data[1]' + >>> str(exc) == 'expected a dictionary @ data[1]' True It should also be accurate for nested values: @@ -35,7 +35,7 @@ It should also be accurate for nested values: ... except MultipleInvalid as e: ... exc = e >>> str(exc) - "invalid list value @ data[0]['four'][0]" + "not a valid value @ data[0]['four'][0]" >>> try: ... schema([{'six': {'seven': 'nine'}}]) @@ -116,8 +116,11 @@ Multiple errors are reported: ... schema([1, 2, 3]) ... except MultipleInvalid as e: ... print([str(i) for i in e.errors]) # doctest: +NORMALIZE_WHITESPACE - ['invalid list value @ data[0]', + ['expected a list @ data[0]', + 'invalid list value @ data[0]', + 'expected a list @ data[1]', 'invalid list value @ data[1]', + 'expected a list @ data[2]', 'invalid list value @ data[2]'] Required fields in dictionary which are invalid should not have required : diff --git a/tests.py b/tests.py index 0dd4e9d8..daae18bf 100644 --- a/tests.py +++ b/tests.py @@ -112,7 +112,7 @@ def test_literal(): try: schema([{"c": 1}]) except Invalid as e: - assert_equal(str(e), 'invalid list value @ data[0]') + assert_equal(str(e), "{'c': 1} not match for {'b': 1} @ data[0]") else: assert False, "Did not raise Invalid" @@ -242,3 +242,24 @@ def test_repr(): ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") + + +def test_list_validation_messages(): + """ Make sure useful error messages are available """ + + def is_even(value): + if value % 2: + raise Invalid('%i is not even' % value) + return value + + schema = Schema(dict(even_numbers=[All(int, is_even)])) + + try: + schema(dict(even_numbers=[3])) + except Invalid as e: + assert_equal(len(e.errors), 2, e.errors) + assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") + assert_equal(str(e.errors[1]), "invalid list value @ data['even_numbers'][0]") + assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + else: + assert False, "Did not raise Invalid" diff --git a/voluptuous.py b/voluptuous.py index 3ab4de4a..c244e6d1 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -664,7 +664,7 @@ def _compile_sequence(self, schema, seq_type): >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] - >>> with raises(MultipleInvalid, 'invalid list value @ data[0]'): + >>> with raises(MultipleInvalid, 'expected int @ data[0]'): ... validator([3.5]) >>> validator([1]) [1] @@ -699,6 +699,8 @@ def validate_sequence(path, data): invalid = e else: if len(invalid.path) <= len(index_path): + if invalid is not None: + errors.append(invalid) invalid = SequenceItemInvalid('invalid %s value' % seq_type_name, index_path) errors.append(invalid) if errors: @@ -714,7 +716,7 @@ def _compile_tuple(self, schema): >>> validator = Schema(('one', 'two', int)) >>> validator(('one',)) ('one',) - >>> with raises(MultipleInvalid, 'invalid tuple value @ data[0]'): + >>> with raises(MultipleInvalid, 'expected int @ data[0]'): ... validator((3.5,)) >>> validator((1,)) (1,) @@ -729,7 +731,7 @@ def _compile_list(self, schema): >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] - >>> with raises(MultipleInvalid, 'invalid list value @ data[0]'): + >>> with raises(MultipleInvalid, 'expected int @ data[0]'): ... validator([3.5]) >>> validator([1]) [1] @@ -1095,7 +1097,7 @@ class Msg(object): Messages are only applied to invalid direct descendants of the schema: >>> validate = Schema(Msg([['one', 'two', int]], 'not okay!')) - >>> with raises(MultipleInvalid, 'invalid list value @ data[0][0]'): + >>> with raises(MultipleInvalid, 'expected int @ data[0][0]'): ... validate([['three']]) The type which is thrown can be overridden but needs to be a subclass of Invalid From 7564a0875f31d3d69b179b915e3f89bcffb26cf8 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 29 Mar 2016 10:45:09 +0100 Subject: [PATCH 034/261] remove unneeded conditional check. --- voluptuous.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index c244e6d1..06249b4d 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -699,8 +699,7 @@ def validate_sequence(path, data): invalid = e else: if len(invalid.path) <= len(index_path): - if invalid is not None: - errors.append(invalid) + errors.append(invalid) invalid = SequenceItemInvalid('invalid %s value' % seq_type_name, index_path) errors.append(invalid) if errors: From f1c9ef3af522c6ba9da93c39eb3d1ebc9f7f0d66 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Tue, 29 Mar 2016 11:36:52 +0100 Subject: [PATCH 035/261] Eliminate unnecessary SequenceItemInvalid exception. In all tested cases, the underlying errors are enough. --- tests.md | 5 +---- tests.py | 3 +-- voluptuous.py | 7 ------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/tests.md b/tests.md index b55b1827..18f6fbaf 100644 --- a/tests.md +++ b/tests.md @@ -117,11 +117,8 @@ Multiple errors are reported: ... except MultipleInvalid as e: ... print([str(i) for i in e.errors]) # doctest: +NORMALIZE_WHITESPACE ['expected a list @ data[0]', - 'invalid list value @ data[0]', 'expected a list @ data[1]', - 'invalid list value @ data[1]', - 'expected a list @ data[2]', - 'invalid list value @ data[2]'] + 'expected a list @ data[2]'] Required fields in dictionary which are invalid should not have required : diff --git a/tests.py b/tests.py index daae18bf..32e66943 100644 --- a/tests.py +++ b/tests.py @@ -257,9 +257,8 @@ def is_even(value): try: schema(dict(even_numbers=[3])) except Invalid as e: - assert_equal(len(e.errors), 2, e.errors) + assert_equal(len(e.errors), 1, e.errors) assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") - assert_equal(str(e.errors[1]), "invalid list value @ data['even_numbers'][0]") assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") else: assert False, "Did not raise Invalid" diff --git a/voluptuous.py b/voluptuous.py index 06249b4d..4b1215ea 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -227,10 +227,6 @@ class InclusiveInvalid(Invalid): """Not all values found in inclusion group.""" -class SequenceItemInvalid(Invalid): - """One of the values found in a sequence was invalid.""" - - class SequenceTypeInvalid(Invalid): """The type found is not a sequence type.""" @@ -698,9 +694,6 @@ def validate_sequence(path, data): raise invalid = e else: - if len(invalid.path) <= len(index_path): - errors.append(invalid) - invalid = SequenceItemInvalid('invalid %s value' % seq_type_name, index_path) errors.append(invalid) if errors: raise MultipleInvalid(errors) From c9cc6b98f0533d9787e6a754a0700c2a29e4a35c Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 30 Mar 2016 17:58:51 +1100 Subject: [PATCH 036/261] 0.8.10 --- voluptuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index 4b1215ea..428394f2 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -107,7 +107,7 @@ __author__ = 'Alec Thomas ' -__version__ = '0.8.9' +__version__ = '0.8.10' @contextmanager From 7ab03c32910e4a594b8f7be5be1c1b758c2187ad Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 1 Apr 2016 09:24:10 +1100 Subject: [PATCH 037/261] Fixes #157 --- tests.py | 10 ++++++++-- voluptuous.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests.py b/tests.py index 32e66943..2ce4d535 100644 --- a/tests.py +++ b/tests.py @@ -1,11 +1,11 @@ import copy -from nose.tools import assert_equal +from nose.tools import assert_equal, assert_raises import voluptuous from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, - Replace, Range, Coerce, All, + Replace, Range, Coerce, All, Any, Length ) @@ -262,3 +262,9 @@ def is_even(value): assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") else: assert False, "Did not raise Invalid" + + +def test_fix_157(): + s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) + assert_equal(['one'], s(['one'])) + assert_raises(MultipleInvalid, s, ['four']) diff --git a/voluptuous.py b/voluptuous.py index 428394f2..6c867614 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -790,10 +790,10 @@ def validate_callable(path, data): raise ValueInvalid('not a valid value', path) except MultipleInvalid as e: for error in e.errors: - error.path = path + error.path + error.path[0:0] = path raise except Invalid as e: - e.path = path + e.path + e.path[0:0] = path raise return validate_callable From 0c3335c2c66317597283a1af8387f2578051b984 Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Wed, 6 Apr 2016 08:27:54 +0100 Subject: [PATCH 038/261] Handle nested MultipleInvalid exceptions. Previously, this would result in an "AttributeError: can't set attribute" from trying to modify the path property on MultipleInvalid. --- tests.py | 21 +++++++++++++++++++++ voluptuous.py | 13 ++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/tests.py b/tests.py index 2ce4d535..c6e232a5 100644 --- a/tests.py +++ b/tests.py @@ -264,6 +264,27 @@ def is_even(value): assert False, "Did not raise Invalid" +def test_nested_multiple_validation_errors(): + """ Make sure useful error messages are available """ + + def is_even(value): + if value % 2: + raise Invalid('%i is not even' % value) + return value + + schema = Schema(dict(even_numbers=All([All(int, is_even)], + Length(min=1)))) + + try: + schema(dict(even_numbers=[3])) + except Invalid as e: + assert_equal(len(e.errors), 1, e.errors) + assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") + assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + else: + assert False, "Did not raise Invalid" + + def test_fix_157(): s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) assert_equal(['one'], s(['one'])) diff --git a/voluptuous.py b/voluptuous.py index 6c867614..6e3bd745 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -180,6 +180,9 @@ def __str__(self): output += ' for ' + self.error_type return output + path + def prepend(self, path): + self.path = path + self.path + class MultipleInvalid(Invalid): def __init__(self, errors=None): @@ -206,6 +209,10 @@ def add(self, error): def __str__(self): return str(self.errors[0]) + def prepend(self, path): + for error in self.errors: + error.prepend(path) + class RequiredFieldInvalid(Invalid): """Required field was missing.""" @@ -788,12 +795,8 @@ def validate_callable(path, data): return schema(data) except ValueError as e: raise ValueInvalid('not a valid value', path) - except MultipleInvalid as e: - for error in e.errors: - error.path[0:0] = path - raise except Invalid as e: - e.path[0:0] = path + e.prepend(path) raise return validate_callable From 167c3d1eb957766d376721b0eb4e93cacc255f64 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Thu, 7 Apr 2016 18:43:36 +0530 Subject: [PATCH 039/261] Readme updated for incorporating url. --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 74490c2a..fd84c64e 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,25 @@ True ``` +### URL's + +URL's in the schema are matched by using `urlparse` library. + +```pycon +>>> from voluptuous import Url +>>> schema = Schema(Url()) +>>> schema('http://w3.org') +'http://w3.org' +>>> try: +... schema('one') +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "expected a URL" +True + +``` + ### Lists Lists in the schema are treated as a set of valid values. Each element From 4a048f830acf683cfaefcb232f726b6d0a43f636 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Thu, 7 Apr 2016 19:32:40 +0530 Subject: [PATCH 040/261] if/else conditions modified --- voluptuous.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/voluptuous.py b/voluptuous.py index 6e3bd745..3a6066f2 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -448,11 +448,9 @@ def validate_mapping(path, iterable, out): if is_remove or remove_key: continue for err in exception_errors: - if len(err.path) > len(key_path): - errors.append(err) - else: + if len(err.path) <= len(key_path): err.error_type = invalid_msg - errors.append(err) + 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 @@ -639,14 +637,11 @@ def validate_dict(path, data): 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 = None + 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 - if msg is None: - msg = ("some but not all values in the same group of " - "inclusion '%s'") % label next_path = path + [VirtualPathComponent(label)] errors.append(InclusiveInvalid(msg, next_path)) break From d045887fb1b3cf5cbe87703db8d14cd296a1a68e Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 8 Apr 2016 17:58:37 +0530 Subject: [PATCH 041/261] Updated url validation code for url's with no domain --- voluptuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index 3a6066f2..f129e8de 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1472,7 +1472,7 @@ def Url(v): """ try: parsed = urlparse.urlparse(v) - if not parsed.scheme or not parsed.netloc: + if not parsed.scheme or not parsed.netloc or "." not in parsed.netloc: raise UrlInvalid("must have a URL scheme and host") return v except: From 7f0f2ad2a674eb8d23ff5a80f381e88d11b5b9fd Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 8 Apr 2016 17:59:12 +0530 Subject: [PATCH 042/261] Test cases for checking validation of domain less url's --- tests.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests.py b/tests.py index c6e232a5..efef5693 100644 --- a/tests.py +++ b/tests.py @@ -135,6 +135,20 @@ def test_url_validation(): assert 'http://example.com/', out_.get("url") +def test_url_without_prefix(): + """ test with invalid domain """ + schema = Schema({"url": Url()}) + url_without_domain = 'http://example/' + + try: + schema({"url": url_without_domain}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for domainless url" + + def test_url_validation_with_none(): """ test with invalid None url""" schema = Schema({"url": Url()}) From ddd5fd6e12db087ffb568df560e409d8aab18e90 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 8 Apr 2016 18:45:01 +0530 Subject: [PATCH 043/261] Did validation for Fully qualified domain url's --- tests.py | 30 ++++++++++++++++++------------ voluptuous.py | 30 +++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/tests.py b/tests.py index efef5693..472db128 100644 --- a/tests.py +++ b/tests.py @@ -5,7 +5,7 @@ from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, - Replace, Range, Coerce, All, Any, Length + Replace, Range, Coerce, All, Any, Length, FqdnUrl ) @@ -127,26 +127,32 @@ def test_literal(): assert False, "Did not raise Invalid" -def test_url_validation(): - """ test with valid URL """ - schema = Schema({"url": Url()}) +def test_fqdn_url_validation(): + """ test with valid fully qualified domain name url """ + schema = Schema({"url": FqdnUrl()}) out_ = schema({"url": "http://example.com/"}) assert 'http://example.com/', out_.get("url") -def test_url_without_prefix(): - """ test with invalid domain """ - schema = Schema({"url": Url()}) - url_without_domain = 'http://example/' - +def test_fqdn_url_without_domain_name(): + """ test with invalid fully qualified domain name url """ + schema = Schema({"url": FqdnUrl()}) try: - schema({"url": url_without_domain}) + schema({"url": None}) except MultipleInvalid as e: assert_equal(str(e), - "expected a URL for dictionary value @ data['url']") + "expected a Fully qualified domain name URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for domainless url" + assert False, "Did not raise Invalid for None url" + + +def test_url_validation(): + """ test with valid URL """ + schema = Schema({"url": Url()}) + out_ = schema({"url": "http://example.com/"}) + + assert 'http://example.com/', out_.get("url") def test_url_validation_with_none(): diff --git a/voluptuous.py b/voluptuous.py index f129e8de..e6253163 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -1460,6 +1460,32 @@ def __repr__(self): self.msg) +def _url_validation(v): + 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 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: + raise ValueError + + @message('expected a URL', cls=UrlInvalid) def Url(v): """Verify that the value is a URL. @@ -1471,9 +1497,7 @@ def Url(v): 'http://w3.org' """ try: - parsed = urlparse.urlparse(v) - if not parsed.scheme or not parsed.netloc or "." not in parsed.netloc: - raise UrlInvalid("must have a URL scheme and host") + _url_validation(v) return v except: raise ValueError From 6025688536ed08e590ea5fe02d63a4ad670b5395 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 8 Apr 2016 19:07:36 +0530 Subject: [PATCH 044/261] More test cases added for FQDN url validation --- tests.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests.py b/tests.py index 472db128..52f8f0a4 100644 --- a/tests.py +++ b/tests.py @@ -147,6 +147,42 @@ def test_fqdn_url_without_domain_name(): assert False, "Did not raise Invalid for None url" +def test_fqdnurl_validation_with_none(): + """ test with invalid None FQDN url""" + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_fqdnurl_validation_with_empty_string(): + """ test with empty string FQDN URL """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_fqdnurl_validation_without_host(): + """ test with empty host FQDN URL """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": 'http://'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + def test_url_validation(): """ test with valid URL """ schema = Schema({"url": Url()}) From 784d1e7c5eef90f31cbc313638c4ebbc44372301 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 14 Apr 2016 10:30:09 +1000 Subject: [PATCH 045/261] 0.8.11 --- voluptuous.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous.py b/voluptuous.py index e6253163..132c3c47 100644 --- a/voluptuous.py +++ b/voluptuous.py @@ -107,7 +107,7 @@ __author__ = 'Alec Thomas ' -__version__ = '0.8.10' +__version__ = '0.8.11' @contextmanager From fafc7204d7121645fc0fa18dc9df521431d000be Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 15 Apr 2016 01:28:24 +0530 Subject: [PATCH 046/261] Squashing commits into 1 --- README.md | 19 + setup.py | 3 + tox.ini | 2 +- voluptuous/__init__.py | 11 + voluptuous/error.py | 177 +++ voluptuous.py => voluptuous/schema_builder.py | 1283 +++-------------- voluptuous/tests/__init__.py | 1 + tests.md => voluptuous/tests/tests.md | 0 tests.py => voluptuous/tests/tests.py | 99 +- voluptuous/util.py | 150 ++ voluptuous/validators.py | 643 +++++++++ 11 files changed, 1275 insertions(+), 1113 deletions(-) create mode 100644 voluptuous/__init__.py create mode 100644 voluptuous/error.py rename voluptuous.py => voluptuous/schema_builder.py (50%) create mode 100644 voluptuous/tests/__init__.py rename tests.md => voluptuous/tests/tests.md (100%) rename tests.py => voluptuous/tests/tests.py (71%) create mode 100644 voluptuous/util.py create mode 100644 voluptuous/validators.py diff --git a/README.md b/README.md index 74490c2a..fd84c64e 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,25 @@ True ``` +### URL's + +URL's in the schema are matched by using `urlparse` library. + +```pycon +>>> from voluptuous import Url +>>> schema = Schema(Url()) +>>> schema('http://w3.org') +'http://w3.org' +>>> try: +... schema('one') +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "expected a URL" +True + +``` + ### Lists Lists in the schema are treated as a set of valid values. Each element diff --git a/setup.py b/setup.py index 2fc07251..57c5cad0 100644 --- a/setup.py +++ b/setup.py @@ -51,4 +51,7 @@ install_requires=[ 'setuptools >= 0.6b1', ], + setup_requires=[ + 'flake8', + ] ) diff --git a/tox.ini b/tox.ini index db8cc556..f88f1ec9 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ commands = [testenv:flake8] deps = flake8 -commands = flake8 setup.py voluptuous.py tests.py +commands = flake8 setup.py voluptuous [testenv:py26] basepython = python2.6 diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py new file mode 100644 index 00000000..63a4d61b --- /dev/null +++ b/voluptuous/__init__.py @@ -0,0 +1,11 @@ +try: + from schema_builder import * + from validators import * + from util import * +except ImportError: + from .schema_builder import * + from .validators import * + from .util import * + +__version__ = '0.8.11' +__author__ = 'tusharmakkar08' diff --git a/voluptuous/error.py b/voluptuous/error.py new file mode 100644 index 00000000..b768b9de --- /dev/null +++ b/voluptuous/error.py @@ -0,0 +1,177 @@ + +class Error(Exception): + """Base validation exception.""" + + +class SchemaError(Error): + """An error was encountered in the schema.""" + + +class Invalid(Error): + """The data was invalid. + + :attr msg: The error message. + :attr path: The path to the error, as a list of keys in the source data. + :attr error_message: The actual error message that was raised, as a + string. + + """ + + def __init__(self, message, path=None, error_message=None, error_type=None): + Error.__init__(self, message) + self.path = path or [] + self.error_message = error_message or message + self.error_type = error_type + + @property + def msg(self): + return self.args[0] + + def __str__(self): + path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \ + if self.path else '' + output = Exception.__str__(self) + if self.error_type: + output += ' for ' + self.error_type + return output + path + + def prepend(self, path): + self.path = path + self.path + + +class MultipleInvalid(Invalid): + def __init__(self, errors=None): + self.errors = errors[:] if errors else [] + + def __repr__(self): + return 'MultipleInvalid(%r)' % self.errors + + @property + def msg(self): + return self.errors[0].msg + + @property + def path(self): + return self.errors[0].path + + @property + def error_message(self): + return self.errors[0].error_message + + def add(self, error): + self.errors.append(error) + + def __str__(self): + return str(self.errors[0]) + + def prepend(self, path): + for error in self.errors: + error.prepend(path) + + +class RequiredFieldInvalid(Invalid): + """Required field was missing.""" + + +class ObjectInvalid(Invalid): + """The value we found was not an object.""" + + +class DictInvalid(Invalid): + """The value found was not a dict.""" + + +class ExclusiveInvalid(Invalid): + """More than one value found in exclusion group.""" + + +class InclusiveInvalid(Invalid): + """Not all values found in inclusion group.""" + + +class SequenceTypeInvalid(Invalid): + """The type found is not a sequence type.""" + + +class TypeInvalid(Invalid): + """The value was not of required type.""" + + +class ValueInvalid(Invalid): + """The value was found invalid by evaluation function.""" + + +class 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 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 InInvalid(Invalid): + pass + + +class NotInInvalid(Invalid): + pass + + +class ExactSequenceInvalid(Invalid): + pass diff --git a/voluptuous.py b/voluptuous/schema_builder.py similarity index 50% rename from voluptuous.py rename to voluptuous/schema_builder.py index 6c867614..cbdfe762 100644 --- a/voluptuous.py +++ b/voluptuous/schema_builder.py @@ -1,12 +1,25 @@ -# encoding: utf-8 -# -# Copyright (C) 2010-2013 Alec Thomas -# All rights reserved. -# -# This software is licensed as described in the file COPYING, which -# you should have received as part of this distribution. -# -# Author: Alec Thomas +import collections +import inspect +import re +from functools import wraps +import sys +from contextlib import contextmanager + +try: + import error as er +except ImportError: + from . import error as er + +if sys.version_info >= (3,): + long = int + unicode = str + basestring = str + ifilter = filter + iteritems = lambda d: d.items() +else: + from itertools import ifilter + + iteritems = lambda d: d.iteritems() """Schema validation for Python data structures. @@ -83,43 +96,11 @@ ... 'Users': {'snmp_community': 'monkey'}}}} True """ -import collections -import datetime -import inspect -import os -import re -import sys -from contextlib import contextmanager -from functools import wraps - - -if sys.version_info >= (3,): - import urllib.parse as urlparse - long = int - unicode = str - basestring = str - ifilter = filter - iteritems = lambda d: d.items() -else: - from itertools import ifilter - import urlparse - iteritems = lambda d: d.iteritems() - - -__author__ = 'Alec Thomas ' -__version__ = '0.8.10' - - -@contextmanager -def raises(exc, msg=None, regex=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) +# 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 class Undefined(object): def __nonzero__(self): @@ -137,170 +118,25 @@ def default_factory(value): return value return lambda: value +@contextmanager +def raises(exc, msg=None, regex=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) -# 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 - - -class Error(Exception): - """Base validation exception.""" - - -class SchemaError(Error): - """An error was encountered in the schema.""" - - -class Invalid(Error): - """The data was invalid. - - :attr msg: The error message. - :attr path: The path to the error, as a list of keys in the source data. - :attr error_message: The actual error message that was raised, as a - string. - - """ - - def __init__(self, message, path=None, error_message=None, error_type=None): - Error.__init__(self, message) - self.path = path or [] - self.error_message = error_message or message - self.error_type = error_type - - @property - def msg(self): - return self.args[0] - - def __str__(self): - path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \ - if self.path else '' - output = Exception.__str__(self) - if self.error_type: - output += ' for ' + self.error_type - return output + path - - -class MultipleInvalid(Invalid): - def __init__(self, errors=None): - self.errors = errors[:] if errors else [] - - def __repr__(self): - return 'MultipleInvalid(%r)' % self.errors - - @property - def msg(self): - return self.errors[0].msg - - @property - def path(self): - return self.errors[0].path - - @property - def error_message(self): - return self.errors[0].error_message - - def add(self, error): - self.errors.append(error) - - def __str__(self): - return str(self.errors[0]) - - -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 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 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.""" +def Extra(_): + """Allow keys in the data that are not present in the schema.""" + raise er.SchemaError('"Extra" should never be called') -class VirtualPathComponent(str): - def __str__(self): - return '<' + self + '>' - def __repr__(self): - return self.__str__() +# 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 class Schema(object): @@ -349,11 +185,11 @@ def __call__(self, data): """Validate data against this schema.""" try: return self._compiled([], data) - except MultipleInvalid: + except er.MultipleInvalid: raise - except Invalid as e: - raise MultipleInvalid([e]) - # return self.validate([], self.schema, data) + except er.Invalid as e: + raise er.MultipleInvalid([e]) + # return self.validate([], self.schema, data) def _compile(self, schema): if schema is Extra: @@ -372,8 +208,8 @@ def _compile(self, schema): if type_ in (bool, int, long, str, unicode, float, complex, object, list, dict, type(None)) or callable(schema): return _compile_scalar(schema) - raise SchemaError('unsupported schema data type %r' % - type(schema).__name__) + raise er.SchemaError('unsupported schema data type %r' % + type(schema).__name__) def _compile_mapping(self, schema, invalid_msg=None): """Create validator for given mapping.""" @@ -413,7 +249,7 @@ def validate_mapping(path, iterable, out): for skey, (ckey, cvalue) in candidates: try: new_key = ckey(key_path, key) - except Invalid as e: + except er.Invalid as e: if len(e.path) > len(key_path): raise if not error or len(e.path) > len(error.path): @@ -432,20 +268,18 @@ def validate_mapping(path, iterable, out): else: remove_key = True continue - except MultipleInvalid as e: + except er.MultipleInvalid as e: exception_errors.extend(e.errors) - except Invalid as e: + 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): - errors.append(err) - else: + if len(err.path) <= len(key_path): err.error_type = invalid_msg - errors.append(err) + 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 @@ -467,8 +301,8 @@ def validate_mapping(path, iterable, out): elif self.extra == ALLOW_EXTRA: out[key] = value elif self.extra != REMOVE_EXTRA: - errors.append(Invalid('extra keys not allowed', key_path)) - # else REMOVE_EXTRA: ignore the key so it's removed from output + errors.append(er.Invalid('extra keys not allowed', key_path)) + # else REMOVE_EXTRA: ignore the key so it's removed from output # set defaults for any that can have defaults for key in default_keys: @@ -480,9 +314,9 @@ def validate_mapping(path, iterable, out): # 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(RequiredFieldInvalid(msg, path + [key])) + errors.append(er.RequiredFieldInvalid(msg, path + [key])) if errors: - raise MultipleInvalid(errors) + raise er.MultipleInvalid(errors) return out @@ -502,7 +336,7 @@ def _compile_object(self, schema): ... self.three = three ... >>> validate = Schema(Object({'one': 'two', 'three': 'four'}, cls=Structure)) - >>> with raises(MultipleInvalid, "not a valid value for object value @ data['one']"): + >>> with raises(er.MultipleInvalid, "not a valid value for object value @ data['one']"): ... validate(Structure(one='three')) """ @@ -511,8 +345,8 @@ def _compile_object(self, schema): def validate_object(path, data): if (schema.cls is not UNDEFINED - and not isinstance(data, schema.cls)): - raise ObjectInvalid('expected a {0!r}'.format(schema.cls), path) + and not isinstance(data, schema.cls)): + raise er.ObjectInvalid('expected a {0!r}'.format(schema.cls), path) iterable = _iterate_object(data) iterable = ifilter(lambda item: item[1] is not None, iterable) out = base_validate(path, iterable, {}) @@ -529,18 +363,18 @@ def _compile_dict(self, schema): A dictionary schema will only validate a dictionary: >>> validate = Schema({}) - >>> with raises(MultipleInvalid, 'expected a dictionary'): + >>> with raises(er.MultipleInvalid, 'expected a dictionary'): ... validate([]) An invalid dictionary value: >>> validate = Schema({'one': 'two', 'three': 'four'}) - >>> with raises(MultipleInvalid, "not a valid value for dictionary value @ data['one']"): + >>> with raises(er.MultipleInvalid, "not a valid value for dictionary value @ data['one']"): ... validate({'one': 'three'}) An invalid key: - >>> with raises(MultipleInvalid, "extra keys not allowed @ data['two']"): + >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['two']"): ... validate({'two': 'three'}) @@ -557,11 +391,11 @@ def _compile_dict(self, schema): purely to validate that the corresponding value is of that type. It will not Coerce the value: - >>> with raises(MultipleInvalid, "extra keys not allowed @ data['10']"): + >>> 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'}) @@ -570,7 +404,7 @@ def _compile_dict(self, schema): Custom message for required key >>> validate = Schema({Required('one', 'required'): 'two'}) - >>> with raises(MultipleInvalid, "required @ data['one']"): + >>> with raises(er.MultipleInvalid, "required @ data['one']"): ... validate({}) (This is to avoid unexpected surprises.) @@ -590,7 +424,7 @@ def _compile_dict(self, schema): ... 'intfield': 'one' ... } ... }) - ... except MultipleInvalid as e: + ... 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']"] @@ -611,7 +445,7 @@ def _compile_dict(self, schema): def validate_dict(path, data): if not isinstance(data, dict): - raise DictInvalid('expected a dictionary', path) + raise er.DictInvalid('expected a dictionary', path) errors = [] for label, group in groups_of_exclusion.items(): @@ -622,30 +456,27 @@ def validate_dict(path, data): 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(ExclusiveInvalid(msg, next_path)) + errors.append(er.ExclusiveInvalid(msg, next_path)) break exists = True if errors: - raise MultipleInvalid(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 = None + 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 - if msg is None: - msg = ("some but not all values in the same group of " - "inclusion '%s'") % label next_path = path + [VirtualPathComponent(label)] - errors.append(InclusiveInvalid(msg, next_path)) + errors.append(er.InclusiveInvalid(msg, next_path)) break if errors: - raise MultipleInvalid(errors) + raise er.MultipleInvalid(errors) out = {} return base_validate(path, iteritems(data), out) @@ -660,7 +491,7 @@ def _compile_sequence(self, schema, seq_type): >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] - >>> with raises(MultipleInvalid, 'expected int @ data[0]'): + >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'): ... validator([3.5]) >>> validator([1]) [1] @@ -670,7 +501,7 @@ def _compile_sequence(self, schema, seq_type): def validate_sequence(path, data): if not isinstance(data, seq_type): - raise SequenceTypeInvalid('expected a %s' % seq_type_name, path) + raise er.SequenceTypeInvalid('expected a %s' % seq_type_name, path) # Empty seq schema, allow any data. if not schema: @@ -689,15 +520,16 @@ def validate_sequence(path, data): if cval is not Remove: # do not include Remove values out.append(cval) break - except Invalid as e: + except er.Invalid as e: if len(e.path) > len(index_path): raise invalid = e else: errors.append(invalid) if errors: - raise MultipleInvalid(errors) + raise er.MultipleInvalid(errors) return type(data)(out) + return validate_sequence def _compile_tuple(self, schema): @@ -708,7 +540,7 @@ def _compile_tuple(self, schema): >>> validator = Schema(('one', 'two', int)) >>> validator(('one',)) ('one',) - >>> with raises(MultipleInvalid, 'expected int @ data[0]'): + >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'): ... validator((3.5,)) >>> validator((1,)) (1,) @@ -723,7 +555,7 @@ def _compile_list(self, schema): >>> validator = Schema(['one', 'two', int]) >>> validator(['one']) ['one'] - >>> with raises(MultipleInvalid, 'expected int @ data[0]'): + >>> with raises(er.MultipleInvalid, 'expected int @ data[0]'): ... validator([3.5]) >>> validator([1]) [1] @@ -761,7 +593,7 @@ def _compile_scalar(schema): >>> _compile_scalar(int)([], 1) 1 - >>> with raises(Invalid, 'expected float'): + >>> with raises(er.Invalid, 'expected float'): ... _compile_scalar(float)([], '1') Callables have @@ -770,7 +602,7 @@ def _compile_scalar(schema): As a convenience, ValueError's are trapped: - >>> with raises(Invalid, 'not a valid value'): + >>> with raises(er.Invalid, 'not a valid value'): ... _compile_scalar(lambda v: float(v))([], 'a') """ if isinstance(schema, type): @@ -779,7 +611,8 @@ def validate_instance(path, data): return data else: msg = 'expected %s' % schema.__name__ - raise TypeInvalid(msg, path) + raise er.TypeInvalid(msg, path) + return validate_instance if callable(schema): @@ -787,19 +620,16 @@ def validate_callable(path, data): try: return schema(data) except ValueError as e: - raise ValueInvalid('not a valid value', path) - except MultipleInvalid as e: - for error in e.errors: - error.path[0:0] = path - raise - except Invalid as e: - e.path[0:0] = path + 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 ScalarInvalid('not a valid value', path) + raise er.ScalarInvalid('not a valid value', path) return data return validate_value @@ -807,6 +637,7 @@ def validate_value(path, data): def _compile_itemsort(): '''return sort function of mappings''' + def is_extra(key_): return key_ is Extra @@ -827,11 +658,11 @@ def is_callable(key_): # 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 + 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 + (5, is_extra)] # Extra lowest priority def item_priority(item_): key_ = item_[0] @@ -843,6 +674,7 @@ def item_priority(item_): return item_priority + _sort_item = _compile_itemsort() @@ -879,6 +711,57 @@ def _iterate_object(obj): raise StopIteration() +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, msg, cls=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.""" @@ -887,21 +770,33 @@ def __init__(self, schema, cls=UNDEFINED): super(Object, self).__init__(schema) +class VirtualPathComponent(str): + def __str__(self): + return '<' + self + '>' + + def __repr__(self): + return self.__str__() + + +## Markers.py + + + class Marker(object): """Mark nodes for special treatment.""" - def __init__(self, schema, msg=None): - self.schema = schema - self._schema = Schema(schema) + def __init__(self, schema_, msg=None): + self.schema = schema_ + self._schema = Schema(schema_) self.msg = msg def __call__(self, v): try: return self._schema(v) - except Invalid as e: + except er.Invalid as e: if not self.msg or len(e.path) > 1: raise - raise Invalid(self.msg) + raise er.Invalid(self.msg) def __str__(self): return str(self.schema) @@ -935,6 +830,7 @@ class Optional(Marker): >>> schema({'key2':'value'}) {'key2': 'value'} """ + def __init__(self, schema, msg=None, default=UNDEFINED): super(Optional, self).__init__(schema, msg=msg) self.default = default_factory(default) @@ -951,7 +847,7 @@ class Exclusive(Optional): Keys inside a same group of exclusion cannot be together, it only makes sense for dictionaries: - >>> with raises(MultipleInvalid, "two or more values in the same group of exclusion 'angles' @ data[]"): + >>> 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: @@ -971,10 +867,11 @@ class Exclusive(Optional): ... } ... }) - >>> with raises(MultipleInvalid, "Please, use only one type of authentication at the same time. @ data[]"): + >>> 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, group_of_exclusion, msg=None): super(Exclusive, self).__init__(schema, msg=msg) self.group_of_exclusion = group_of_exclusion @@ -995,7 +892,7 @@ class Inclusive(Optional): Keys inside a same group of inclusive must exist together, it only makes sense for dictionaries: - >>> with raises(MultipleInvalid, "some but not all values in the same group of inclusion 'file' @ data[]"): + >>> 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: @@ -1011,10 +908,10 @@ class Inclusive(Optional): ... Inclusive('width', 'size', msg=msg): int ... }) - >>> with raises(MultipleInvalid, msg + " @ data[]"): + >>> with raises(er.MultipleInvalid, msg + " @ data[]"): ... schema({'height': 100}) - >>> with raises(MultipleInvalid, msg + " @ data[]"): + >>> with raises(er.MultipleInvalid, msg + " @ data[]"): ... schema({'width': 100}) >>> data = {'height': 100, 'width': 100} @@ -1031,7 +928,7 @@ 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(MultipleInvalid, "required key not provided @ data['key']"): + >>> with raises(er.MultipleInvalid, "required key not provided @ data['key']"): ... schema({}) >>> schema = Schema({Required('key', default='value'): str}) @@ -1041,6 +938,7 @@ class Required(Marker): >>> schema({}) {'key': []} """ + def __init__(self, schema, msg=None, default=UNDEFINED): super(Required, self).__init__(schema, msg=msg) self.default = default_factory(default) @@ -1052,7 +950,7 @@ class Remove(Marker): keys will be treated as extras. >>> schema = Schema({str: int, Remove(int): str}) - >>> with raises(MultipleInvalid, "extra keys not allowed @ data[1]"): + >>> 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} @@ -1060,6 +958,7 @@ class Remove(Marker): >>> schema([1, 2, 3, 4.0, 5, 6.0, '7']) [1, 2, 3, 5, '7'] """ + def __call__(self, v): super(Remove, self).__call__(v) return self.__class__ @@ -1068,66 +967,6 @@ def __repr__(self): return "Remove(%r)" % (self.schema,) -def Extra(_): - """Allow keys in the data that are not present in the schema.""" - raise 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 - -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(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(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(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=RangeInvalid)) - >>> try: - ... validate(['three']) - ... except MultipleInvalid as e: - ... assert isinstance(e.errors[0], RangeInvalid) - """ - - def __init__(self, schema, msg, cls=None): - if cls and not issubclass(cls, Invalid): - raise 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 Invalid as e: - if len(e.path) > 1: - raise e - else: - raise (self.cls or Invalid)(self.msg) - - def __repr__(self): - return 'Msg(%s, %s, cls=%s)' % (self._schema, self.msg, self.cls) - - def message(default=None, cls=None): """Convenience decorator to allow functions to provide a message. @@ -1138,26 +977,26 @@ def message(default=None, cls=None): ... return int(v) >>> validate = Schema(isint()) - >>> with raises(MultipleInvalid, 'not an integer'): + >>> 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(MultipleInvalid, 'bad'): + >>> with raises(er.MultipleInvalid, 'bad'): ... validate('a') The class thrown too: - >>> class IntegerInvalid(Invalid): pass + >>> class IntegerInvalid(er.Invalid): pass >>> validate = Schema(isint('bad', clsoverride=IntegerInvalid)) >>> try: ... validate('a') - ... except MultipleInvalid as e: + ... except er.MultipleInvalid as e: ... assert isinstance(e.errors[0], IntegerInvalid) """ - if cls and not issubclass(cls, Invalid): - raise SchemaError("message can only use subclases of Invalid as custom class") + 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) @@ -1167,766 +1006,10 @@ def wrapper(*args, **kwargs): try: return f(*args, **kwargs) except ValueError: - raise (clsoverride or cls or ValueInvalid)(msg or default or 'invalid value') - return wrapper - return check - return decorator + raise (clsoverride or cls or er.ValueInvalid)(msg or default or 'invalid value') + return wrapper -def truth(f): - """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 + 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, msg=None): - self.type = type - self.msg = msg - self.type_name = type.__name__ - - def __call__(self, v): - try: - return self.type(v) - except (ValueError, TypeError): - msg = self.msg or ('expected %s' % self.type_name) - 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 Any(object): - """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 __init__(self, *validators, **kwargs): - self.validators = validators - self.msg = kwargs.pop('msg', None) - self._schemas = [Schema(val, **kwargs) for val in validators] - - def __call__(self, v): - error = None - for schema in self._schemas: - try: - return schema(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) - raise AnyInvalid(self.msg or 'no valid value found') - - def __repr__(self): - return 'Any([%s])' % (", ".join(repr(v) for v in self.validators)) - - -# Convenience alias -Or = Any - - -class All(object): - """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 __init__(self, *validators, **kwargs): - self.validators = validators - self.msg = kwargs.pop('msg', None) - self._schemas = [Schema(val, **kwargs) for val in validators] - - def __call__(self, v): - try: - for schema in self._schemas: - v = schema(v) - except Invalid as e: - raise e if self.msg is None else AllInvalid(self.msg) - return v - - def __repr__(self): - return 'All(%s, msg=%r)' % ( - ", ".join(repr(v) for v in self.validators), - self.msg - ) - - -# 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"): - ... 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, msg=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') - 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, substitution, msg=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) - - -@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: - parsed = urlparse.urlparse(v) - if not parsed.scheme or not parsed.netloc: - raise UrlInvalid("must have a URL scheme and host") - return v - except: - raise ValueError - - -@message('not a file', cls=FileInvalid) -@truth -def IsFile(v): - """Verify the file exists. - - >>> os.path.basename(IsFile()(__file__)).startswith('voluptuous.py') - True - >>> with raises(FileInvalid, 'not a file'): - ... IsFile()("random_filename_goes_here.py") - """ - return os.path.isfile(v) - - -@message('not a directory', cls=DirInvalid) -@truth -def IsDir(v): - """Verify the directory exists. - - >>> IsDir()('/') - '/' - """ - return os.path.isdir(v) - - -@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('voluptuous.py') - True - >>> with raises(Invalid, 'path does not exist'): - ... PathExists()("random_filename_goes_here.py") - """ - return os.path.exists(v) - - -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=None, max=None, min_included=True, - max_included=True, msg=None): - self.min = min - self.max = max - self.min_included = min_included - self.max_included = max_included - self.msg = msg - - def __call__(self, v): - if self.min_included: - if self.min is not None and v < self.min: - raise RangeInvalid( - self.msg or 'value must be at least %s' % self.min) - else: - if self.min is not None and 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 v > self.max: - raise RangeInvalid( - self.msg or 'value must be at most %s' % self.max) - else: - if self.max is not None and v >= self.max: - raise RangeInvalid( - self.msg or 'value must be lower than %s' % self.max) - return v - - 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=None, max=None, msg=None): - self.min = min - self.max = max - self.msg = msg - - def __call__(self, v): - 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 - - def __repr__(self): - return 'Clamp(min=%s, max=%s)' % (self.min, self.max) - - -class LengthInvalid(Invalid): - pass - - -class Length(object): - """The length of a value must be in a certain range.""" - - def __init__(self, min=None, max=None, msg=None): - self.min = min - self.max = max - self.msg = msg - - def __call__(self, v): - 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 - - def __repr__(self): - return 'Length(min=%s, max=%s)' % (self.min, self.max) - - -class DatetimeInvalid(Invalid): - """The value is not a formatted datetime string.""" - - -class Datetime(object): - """Validate that the value matches the datetime format.""" - - DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - - def __init__(self, format=None, msg=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 InInvalid(Invalid): - pass - - -class In(object): - """Validate that a value is in a collection.""" - - def __init__(self, container, msg=None): - self.container = container - self.msg = msg - - def __call__(self, v): - try: - check = v not in self.container - except TypeError: - check = True - if check: - raise InInvalid(self.msg or 'value is not allowed') - return v - - def __repr__(self): - return 'In(%s)' % (self.container,) - - -class NotInInvalid(Invalid): - pass - - -class NotIn(object): - """Validate that a value is not in a collection.""" - - def __init__(self, container, msg=None): - self.container = container - self.msg = msg - - def __call__(self, v): - try: - check = v in self.container - except TypeError: - check = True - if check: - raise NotInInvalid(self.msg or 'value is not allowed') - return v - - def __repr__(self): - return 'NotIn(%s)' % (self.container,) - - -def Lower(v): - """Transform a string to lower case. - - >>> s = Schema(Lower) - >>> s('HI') - 'hi' - """ - return str(v).lower() - - -def Upper(v): - """Transform a string to upper case. - - >>> s = Schema(Upper) - >>> s('hi') - 'HI' - """ - return str(v).upper() - - -def Capitalize(v): - """Capitalise a string. - - >>> s = Schema(Capitalize) - >>> s('hello world') - 'Hello world' - """ - return str(v).capitalize() - - -def Title(v): - """Title case a string. - - >>> s = Schema(Title) - >>> s('hello world') - 'Hello World' - """ - return str(v).title() - - -def Strip(v): - """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=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(Any(int, SetTo(42))) - >>> s(2) - 2 - >>> s("foo") - 42 - """ - - def __init__(self, value): - self.value = default_factory(value) - - def __call__(self, v): - return self.value() - - def __repr__(self): - return 'SetTo(%s)' % (self.value(),) - - -class ExactSequenceInvalid(Invalid): - pass - - -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 * - >>> 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, **kwargs): - self.validators = validators - self.msg = kwargs.pop('msg', None) - self._schemas = [Schema(val, **kwargs) for val in validators] - - def __call__(self, v): - if not isinstance(v, (list, tuple)): - 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 Literal(object): - def __init__(self, lit): - self.lit = lit - - def __call__(self, value, msg=None): - if self.lit != value: - raise LiteralInvalid( - msg or '%s not match for %s' % (value, self.lit) - ) - else: - return self.lit - - def __str__(self): - return str(self.lit) - - def __repr__(self): - return repr(self.lit) - - -class Unique(object): - """Ensure an iterable does not contain duplicate items. - - Only iterables convertable 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=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 Set(object): - """Convert a list into a set. - - >>> s = Schema(Set()) - >>> s([]) == set([]) - True - >>> s([1, 2]) == set([1, 2]) - True - >>> with raises(Invalid, regex="^cannot be presented as set: "): - ... s([set([1, 2]), set([3, 4])]) - """ - - def __init__(self, msg=None): - self.msg = msg - - def __call__(self, v): - try: - set_v = set(v) - except Exception as e: - raise TypeInvalid( - self.msg or 'cannot be presented as set: {0}'.format(e)) - return set_v - - def __repr__(self): - return 'Set()' - - -if __name__ == '__main__': - import doctest - doctest.testmod() + return decorator diff --git a/voluptuous/tests/__init__.py b/voluptuous/tests/__init__.py new file mode 100644 index 00000000..f29719c7 --- /dev/null +++ b/voluptuous/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'tusharmakkar08' diff --git a/tests.md b/voluptuous/tests/tests.md similarity index 100% rename from tests.md rename to voluptuous/tests/tests.md diff --git a/tests.py b/voluptuous/tests/tests.py similarity index 71% rename from tests.py rename to voluptuous/tests/tests.py index 2ce4d535..787dde5b 100644 --- a/tests.py +++ b/voluptuous/tests/tests.py @@ -1,11 +1,10 @@ import copy from nose.tools import assert_equal, assert_raises -import voluptuous from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, - Replace, Range, Coerce, All, Any, Length + Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA ) @@ -37,8 +36,8 @@ def test_iterate_candidates(): Extra: object, } # toaster should be first. - assert_equal(voluptuous._iterate_mapping_candidates(schema)[0][0], - 'toaster') + from voluptuous.schema_builder import _iterate_mapping_candidates + assert_equal(_iterate_mapping_candidates(schema)[0][0], 'toaster') def test_in(): @@ -127,6 +126,62 @@ def test_literal(): assert False, "Did not raise Invalid" +def test_fqdn_url_validation(): + """ test with valid fully qualified domain name url """ + schema = Schema({"url": FqdnUrl()}) + out_ = schema({"url": "http://example.com/"}) + + assert 'http://example.com/', out_.get("url") + + +def test_fqdn_url_without_domain_name(): + """ test with invalid fully qualified domain name url """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_fqdnurl_validation_with_none(): + """ test with invalid None FQDN url""" + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_fqdnurl_validation_with_empty_string(): + """ test with empty string FQDN URL """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_fqdnurl_validation_without_host(): + """ test with empty host FQDN URL """ + schema = Schema({"url": FqdnUrl()}) + try: + schema({"url": 'http://'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected a Fully qualified domain name URL for dictionary value @ data['url']") + else: + assert False, "Did not raise Invalid for empty string url" + + def test_url_validation(): """ test with valid URL """ schema = Schema({"url": Url()}) @@ -216,12 +271,12 @@ 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=voluptuous.ALLOW_EXTRA) + extended = base.extend({'b': str}, required=False, extra=ALLOW_EXTRA) - assert base.required == True - assert base.extra == voluptuous.PREVENT_EXTRA - assert extended.required == False - assert extended.extra == voluptuous.ALLOW_EXTRA + assert base.required is True + assert base.extra == PREVENT_EXTRA + assert extended.required is False + assert extended.extra == ALLOW_EXTRA def test_repr(): @@ -229,7 +284,7 @@ def test_repr(): 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') + max_included=False, msg='number not in range') coerce_ = Coerce(int, msg="moo") all_ = All('10', Coerce(int), msg='all msg') @@ -237,8 +292,7 @@ def test_repr(): assert_equal(repr(replace), "Replace('you', 'I', msg='you and I')") assert_equal( repr(range_), - "Range(min=0, max=42, min_included=False, " \ - "max_included=False, msg='number not in range')" + "Range(min=0, max=42, min_included=False, max_included=False, msg='number not in range')" ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") @@ -264,6 +318,27 @@ def is_even(value): assert False, "Did not raise Invalid" +def test_nested_multiple_validation_errors(): + """ Make sure useful error messages are available """ + + def is_even(value): + if value % 2: + raise Invalid('%i is not even' % value) + return value + + schema = Schema(dict(even_numbers=All([All(int, is_even)], + Length(min=1)))) + + try: + schema(dict(even_numbers=[3])) + except Invalid as e: + assert_equal(len(e.errors), 1, e.errors) + assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") + assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + else: + assert False, "Did not raise Invalid" + + def test_fix_157(): s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) assert_equal(['one'], s(['one'])) diff --git a/voluptuous/util.py b/voluptuous/util.py new file mode 100644 index 00000000..ca581c98 --- /dev/null +++ b/voluptuous/util.py @@ -0,0 +1,150 @@ +try: + from error import LiteralInvalid, TypeInvalid, Invalid + from schema_builder import * + import validators +except ImportError: + from .error import LiteralInvalid, TypeInvalid, Invalid + from .schema_builder import * + from . import validators + +__author__ = 'tusharmakkar08' + + +def Lower(v): + """Transform a string to lower case. + + >>> s = Schema(Lower) + >>> s('HI') + 'hi' + """ + return str(v).lower() + + +def Upper(v): + """Transform a string to upper case. + + >>> s = Schema(Upper) + >>> s('hi') + 'HI' + """ + return str(v).upper() + + +def Capitalize(v): + """Capitalise a string. + + >>> s = Schema(Capitalize) + >>> s('hello world') + 'Hello world' + """ + return str(v).capitalize() + + +def Title(v): + """Title case a string. + + >>> s = Schema(Title) + >>> s('hello world') + 'Hello World' + """ + return str(v).title() + + +def Strip(v): + """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=None): + self.default_value = default_factory(default_value) + self.msg = msg + + def __call__(self, v): + if v is None: + v = self.default_value() + return v + + def __repr__(self): + return 'DefaultTo(%s)' % (self.default_value(),) + + +class SetTo(object): + """Set a value, ignoring any previous value. + + >>> s = Schema(validators.Any(int, SetTo(42))) + >>> s(2) + 2 + >>> s("foo") + 42 + """ + + def __init__(self, value): + self.value = default_factory(value) + + def __call__(self, v): + return self.value() + + def __repr__(self): + return 'SetTo(%s)' % (self.value(),) + + +class Set(object): + """Convert a list into a set. + + >>> s = Schema(Set()) + >>> s([]) == set([]) + True + >>> s([1, 2]) == set([1, 2]) + True + >>> with raises(Invalid, regex="^cannot be presented as set: "): + ... s([set([1, 2]), set([3, 4])]) + """ + + def __init__(self, msg=None): + self.msg = msg + + def __call__(self, v): + try: + set_v = set(v) + except Exception as e: + raise TypeInvalid( + self.msg or 'cannot be presented as set: {0}'.format(e)) + return set_v + + def __repr__(self): + return 'Set()' + + +class Literal(object): + def __init__(self, lit): + self.lit = lit + + def __call__(self, value, msg=None): + if self.lit != value: + raise LiteralInvalid( + msg or '%s not match for %s' % (value, self.lit) + ) + else: + return self.lit + + def __str__(self): + return str(self.lit) + + def __repr__(self): + return repr(self.lit) diff --git a/voluptuous/validators.py b/voluptuous/validators.py new file mode 100644 index 00000000..3e682399 --- /dev/null +++ b/voluptuous/validators.py @@ -0,0 +1,643 @@ +import os +import re +import datetime +import sys +from functools import wraps + + +try: + from schema_builder import Schema, raises, message + from error import * +except ImportError: + from .schema_builder import Schema, raises, message + from .error import * + + +if sys.version_info >= (3,): + import urllib.parse as urlparse + basestring = str +else: + import urlparse + + +__author__ = 'tusharmakkar08' + + +def truth(f): + """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, msg=None): + self.type = type + self.msg = msg + self.type_name = type.__name__ + + def __call__(self, v): + try: + return self.type(v) + except (ValueError, TypeError): + msg = self.msg or ('expected %s' % self.type_name) + 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 Any(object): + """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 __init__(self, *validators, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): + error = None + for schema in self._schemas: + try: + return schema(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) + raise AnyInvalid(self.msg or 'no valid value found') + + def __repr__(self): + return 'Any([%s])' % (", ".join(repr(v) for v in self.validators)) + + +# Convenience alias +Or = Any + + +class All(object): + """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 __init__(self, *validators, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): + try: + for schema in self._schemas: + v = schema(v) + except Invalid as e: + raise e if self.msg is None else AllInvalid(self.msg) + return v + + def __repr__(self): + return 'All(%s, msg=%r)' % ( + ", ".join(repr(v) for v in self.validators), + self.msg + ) + + +# 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"): + ... 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, msg=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') + 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, substitution, msg=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): + 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 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: + 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: + 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") + """ + return os.path.isfile(v) + + +@message('not a directory', cls=DirInvalid) +@truth +def IsDir(v): + """Verify the directory exists. + + >>> IsDir()('/') + '/' + """ + return os.path.isdir(v) + + +@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") + """ + return os.path.exists(v) + + +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=None, max=None, min_included=True, + max_included=True, msg=None): + self.min = min + self.max = max + self.min_included = min_included + self.max_included = max_included + self.msg = msg + + def __call__(self, v): + if self.min_included: + if self.min is not None and v < self.min: + raise RangeInvalid( + self.msg or 'value must be at least %s' % self.min) + else: + if self.min is not None and 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 v > self.max: + raise RangeInvalid( + self.msg or 'value must be at most %s' % self.max) + else: + if self.max is not None and v >= self.max: + raise RangeInvalid( + self.msg or 'value must be lower than %s' % self.max) + return v + + 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=None, max=None, msg=None): + self.min = min + self.max = max + self.msg = msg + + def __call__(self, v): + 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 + + 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=None, max=None, msg=None): + self.min = min + self.max = max + self.msg = msg + + def __call__(self, v): + 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 + + 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=None, msg=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 In(object): + """Validate that a value is in a collection.""" + + def __init__(self, container, msg=None): + self.container = container + self.msg = msg + + def __call__(self, v): + try: + check = v not in self.container + except TypeError: + check = True + if check: + raise InInvalid(self.msg or 'value is not allowed') + 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, msg=None): + self.container = container + self.msg = msg + + def __call__(self, v): + try: + check = v in self.container + except TypeError: + check = True + if check: + raise NotInInvalid(self.msg or 'value is not allowed') + return v + + def __repr__(self): + return 'NotIn(%s)' % (self.container,) + + +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 * + >>> 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, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): + if not isinstance(v, (list, tuple)): + 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 convertable 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=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()' From 03625de137f16de00e39159952a5f1e1dfa4958d Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 15 Apr 2016 10:10:26 +0530 Subject: [PATCH 047/261] setup.py changed to support packages --- .gitignore | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 62f5a1b1..92024168 100755 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ dist .tox MANIFEST .idea +venv diff --git a/setup.py b/setup.py index 57c5cad0..41464982 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ long_description=long_description, license='BSD', platforms=['any'], - py_modules=['voluptuous'], + packages=['voluptuous'], author='Alec Thomas', author_email='alec@swapoff.org', classifiers=[ From c670d98c75beac78383e64efcbe77ddee73b7de2 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sat, 23 Apr 2016 22:50:04 +0530 Subject: [PATCH 048/261] Email validator added --- voluptuous/error.py | 4 ++++ voluptuous/validators.py | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/voluptuous/error.py b/voluptuous/error.py index b768b9de..9d3774d6 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -141,6 +141,10 @@ class UrlInvalid(Invalid): """The value is not a url.""" +class EmailInvalid(Invalid): + """The value is not a email.""" + + class FileInvalid(Invalid): """The value is not a file.""" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 3e682399..c0fa87f9 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -321,6 +321,51 @@ def _url_validation(v): return parsed +@message('expected an Email', cls=UrlInvalid) +def Email(v): + """Verify that the value is an Email or not. + + >>> s = Schema(Email()) + >>> with raises(MultipleInvalid, 'expected an Email'): + ... s("a.com") + >>> with raises(MultipleInvalid, 'expected an Email'): + ... s("a@.com") + >>> with raises(MultipleInvalid, 'expected an Email'): + ... s("a@.com") + >>> s('t@x.com') + 't@x.com' + """ + try: + if not v or "@" not in v: + raise EmailInvalid("Invalid Email") + user_part, domain_part = v.rsplit('@', 1) + + # Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py + user_regex = re.compile( + # 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])*"$)""", + re.IGNORECASE + ) + domain_regex = re.compile( + # domain + r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' + 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}\]$', + re.IGNORECASE) + + if not (user_regex.match(user_part) and domain_regex.match(domain_part)): + raise EmailInvalid("Invalid Email") + return v + except: + 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. From dd85b3871519048323fb4b44164a65827e6f2d9f Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sat, 23 Apr 2016 22:59:17 +0530 Subject: [PATCH 049/261] Test cases added for email --- voluptuous/tests/tests.py | 48 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 787dde5b..0f4a9e11 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -3,7 +3,7 @@ from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, - Url, MultipleInvalid, LiteralInvalid, NotIn, Match, + Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA ) @@ -126,6 +126,50 @@ def test_literal(): assert False, "Did not raise Invalid" +def test_email_validation(): + """ test with valid email """ + schema = Schema({"email": Email()}) + out_ = schema({"email": "example@example.com"}) + + assert 'example@example.com"', out_.get("url") + + +def test_email_validation_with_none(): + """ test with invalid None Email""" + schema = Schema({"email": Email()}) + try: + schema({"email": None}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an Email for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for None url" + + +def test_email_validation_with_empty_string(): + """ test with empty string Email""" + schema = Schema({"email": Email()}) + try: + schema({"email": ''}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an Email for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for empty string url" + + +def test_email_validation_without_host(): + """ test with empty host name in email """ + schema = Schema({"email": Email()}) + try: + schema({"email": 'a@.com'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an Email for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for empty string url" + + def test_fqdn_url_validation(): """ test with valid fully qualified domain name url """ schema = Schema({"url": FqdnUrl()}) @@ -138,7 +182,7 @@ def test_fqdn_url_without_domain_name(): """ test with invalid fully qualified domain name url """ schema = Schema({"url": FqdnUrl()}) try: - schema({"url": None}) + schema({"url": "http://localhost/"}) except MultipleInvalid as e: assert_equal(str(e), "expected a Fully qualified domain name URL for dictionary value @ data['url']") From e4df4a62a59baea892839cae4eec6020f871d8ab Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sun, 24 Apr 2016 06:10:21 +0530 Subject: [PATCH 050/261] regex precompiled at import time --- voluptuous/validators.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index c0fa87f9..379b15d7 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -19,6 +19,24 @@ else: import urlparse +# Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py +USER_REGEX = re.compile( + # 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])*"$)""", + re.IGNORECASE +) +DOMAIN_REGEX = re.compile( + # domain + r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' + 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}\]$', + re.IGNORECASE) __author__ = 'tusharmakkar08' @@ -340,26 +358,7 @@ def Email(v): raise EmailInvalid("Invalid Email") user_part, domain_part = v.rsplit('@', 1) - # Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py - user_regex = re.compile( - # 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])*"$)""", - re.IGNORECASE - ) - domain_regex = re.compile( - # domain - r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' - 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}\]$', - re.IGNORECASE) - - if not (user_regex.match(user_part) and domain_regex.match(domain_part)): + if not (USER_REGEX.match(user_part) and DOMAIN_REGEX.match(domain_part)): raise EmailInvalid("Invalid Email") return v except: From fdbf8ce741751425c3685fc1466c9a9e483125fd Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sun, 24 Apr 2016 13:15:25 +0530 Subject: [PATCH 051/261] Flake8 errors resolved; Tox now running successfully --- tox.ini | 2 +- voluptuous/__init__.py | 2 ++ voluptuous/schema_builder.py | 25 ++++++++++++------------- voluptuous/util.py | 4 ++-- voluptuous/validators.py | 12 +++++++++--- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/tox.ini b/tox.ini index f88f1ec9..2725abe5 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ commands = [testenv:flake8] deps = flake8 -commands = flake8 setup.py voluptuous +commands = flake8 --doctests setup.py voluptuous [testenv:py26] basepython = python2.6 diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 63a4d61b..20e0d553 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -1,3 +1,5 @@ +# flake8: noqa + try: from schema_builder import * from validators import * diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index cbdfe762..8a9874e2 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -15,11 +15,14 @@ unicode = str basestring = str ifilter = filter - iteritems = lambda d: d.items() + + def iteritems(d): + return d.items() else: from itertools import ifilter - iteritems = lambda d: d.iteritems() + def iteritems(d): + return d.iteritems() """Schema validation for Python data structures. @@ -102,6 +105,7 @@ 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 + class Undefined(object): def __nonzero__(self): return False @@ -118,6 +122,7 @@ def default_factory(value): return value return lambda: value + @contextmanager def raises(exc, msg=None, regex=None): try: @@ -216,15 +221,11 @@ def _compile_mapping(self, schema, invalid_msg=None): invalid_msg = invalid_msg or 'mapping value' # Keys that may be required - all_required_keys = set(key for key in schema - if key is not Extra - and ((self.required and not isinstance(key, (Optional, Remove))) - or isinstance(key, Required))) + all_required_keys = set(key for key in schema if key is not Extra and ( + (self.required and not isinstance(key, (Optional, Remove))) or isinstance(key, Required))) # Keys that may have defaults - all_default_keys = set(key for key in schema - if isinstance(key, Required) - or isinstance(key, Optional)) + all_default_keys = set(key for key in schema if isinstance(key, Required) or isinstance(key, Optional)) _compiled_schema = {} for skey, svalue in iteritems(schema): @@ -344,8 +345,7 @@ def _compile_object(self, schema): schema, invalid_msg='object value') def validate_object(path, data): - if (schema.cls is not UNDEFINED - and not isinstance(data, schema.cls)): + if schema.cls is not UNDEFINED and not isinstance(data, schema.cls): raise er.ObjectInvalid('expected a {0!r}'.format(schema.cls), path) iterable = _iterate_object(data) iterable = ifilter(lambda item: item[1] is not None, iterable) @@ -778,8 +778,7 @@ def __repr__(self): return self.__str__() -## Markers.py - +# Markers.py class Marker(object): diff --git a/voluptuous/util.py b/voluptuous/util.py index ca581c98..1a5f1815 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,10 +1,10 @@ try: from error import LiteralInvalid, TypeInvalid, Invalid - from schema_builder import * + from schema_builder import Schema, default_factory, raises import validators except ImportError: from .error import LiteralInvalid, TypeInvalid, Invalid - from .schema_builder import * + from .schema_builder import Schema, default_factory, raises from . import validators __author__ = 'tusharmakkar08' diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 379b15d7..728d6b49 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -7,10 +7,16 @@ try: from schema_builder import Schema, raises, message - from error import * + from error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, + AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, + PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, InInvalid, TypeInvalid, + NotInInvalid) except ImportError: from .schema_builder import Schema, raises, message - from .error import * + from .error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, + AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, + PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, InInvalid, TypeInvalid, + NotInInvalid) if sys.version_info >= (3,): @@ -614,7 +620,7 @@ class ExactSequence(object): :param kwargs: All other keyword arguments are passed to the sub-Schema constructors. - >>> from voluptuous import * + >>> from voluptuous import Schema, ExactSequence >>> validate = Schema(ExactSequence([str, int, list, list])) >>> validate(['hourly_report', 10, [], []]) ['hourly_report', 10, [], []] From 90942adf0496a4c26ff63e2ac05009dfad883fe4 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sun, 24 Apr 2016 13:45:42 +0530 Subject: [PATCH 052/261] W503 resolution made clear --- voluptuous/schema_builder.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8a9874e2..9f4d84d3 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -221,11 +221,15 @@ def _compile_mapping(self, schema, invalid_msg=None): invalid_msg = invalid_msg or 'mapping value' # Keys that may be required - all_required_keys = set(key for key in schema if key is not Extra and ( - (self.required and not isinstance(key, (Optional, Remove))) or isinstance(key, Required))) + all_required_keys = set(key for key in schema + if key is not Extra and + ((self.required and not isinstance(key, (Optional, Remove))) or + isinstance(key, Required))) # Keys that may have defaults - all_default_keys = set(key for key in schema if isinstance(key, Required) or isinstance(key, Optional)) + all_default_keys = set(key for key in schema + if isinstance(key, Required) or + isinstance(key, Optional)) _compiled_schema = {} for skey, svalue in iteritems(schema): From 4dd45f29009d81d1badc763a70dfd31f4d47c2a9 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sun, 24 Apr 2016 13:45:42 +0530 Subject: [PATCH 053/261] W503 resolution made clear --- .gitignore | 1 + docs/Makefile | 230 +++++++++++++++++++++++++++ docs/__init__.py | 1 + docs/conf.py | 293 +++++++++++++++++++++++++++++++++++ docs/index.rst | 23 +++ docs/voluptuous.rst | 35 +++++ voluptuous/schema_builder.py | 10 +- 7 files changed, 590 insertions(+), 3 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/__init__.py create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/voluptuous.rst diff --git a/.gitignore b/.gitignore index 92024168..e1c2457b 100755 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ dist MANIFEST .idea venv +docs/_build diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..3ebc3e16 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,230 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) + $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Voluptuous.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Voluptuous.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Voluptuous" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Voluptuous" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 00000000..f29719c7 --- /dev/null +++ b/docs/__init__.py @@ -0,0 +1 @@ +__author__ = 'tusharmakkar08' diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..cde7e288 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +# +# Voluptuous documentation build configuration file, created by +# sphinx-quickstart on Fri May 13 18:38:05 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Voluptuous' +copyright = u'2016, Alec Thomas' +author = u'Alec Thomas' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.8.11' +# The full version, including alpha/beta/rc tags. +release = u'0.8.11' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +#html_title = u'Voluptuous v0.8.11' + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +#html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Voluptuousdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Voluptuous.tex', u'Voluptuous Documentation', + u'Alec Thomas', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'voluptuous', u'Voluptuous Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Voluptuous', u'Voluptuous Documentation', + author, 'Voluptuous', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..e61b532c --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,23 @@ +.. Voluptuous documentation master file, created by + sphinx-quickstart on Mon May 9 19:29:37 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Voluptuous's documentation! +====================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + voluptuous + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/voluptuous.rst b/docs/voluptuous.rst new file mode 100644 index 00000000..0df86449 --- /dev/null +++ b/docs/voluptuous.rst @@ -0,0 +1,35 @@ +Voluptuous Package +================== + +:mod:`schema_builder` Module +----------------------------- + +.. automodule:: voluptuous.schema_builder + :members: + :undoc-members: + :show-inheritance: + +:mod:`error` Module +----------------------------- + +.. automodule:: voluptuous.error + :members: + :undoc-members: + :show-inheritance: + +:mod:`validators` Module +----------------------------- + +.. automodule:: voluptuous.validators + :members: + :undoc-members: + :show-inheritance: + + +:mod:`util` Module +----------------------------- + +.. automodule:: voluptuous.util + :members: + :undoc-members: + :show-inheritance: diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8a9874e2..9f4d84d3 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -221,11 +221,15 @@ def _compile_mapping(self, schema, invalid_msg=None): invalid_msg = invalid_msg or 'mapping value' # Keys that may be required - all_required_keys = set(key for key in schema if key is not Extra and ( - (self.required and not isinstance(key, (Optional, Remove))) or isinstance(key, Required))) + all_required_keys = set(key for key in schema + if key is not Extra and + ((self.required and not isinstance(key, (Optional, Remove))) or + isinstance(key, Required))) # Keys that may have defaults - all_default_keys = set(key for key in schema if isinstance(key, Required) or isinstance(key, Optional)) + all_default_keys = set(key for key in schema + if isinstance(key, Required) or + isinstance(key, Optional)) _compiled_schema = {} for skey, svalue in iteritems(schema): From 2ac9cb212b86ffbb838baa15d44cc6674eb41c22 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Sun, 15 May 2016 14:29:45 +0530 Subject: [PATCH 054/261] Documentation made with Sphinx --- docs/conf.py | 18 +++++++++--------- docs/index.rst | 3 +-- docs/modules.rst | 7 +++++++ docs/voluptuous.rst | 30 ++++++++++++++++-------------- voluptuous/validators.py | 2 +- 5 files changed, 34 insertions(+), 26 deletions(-) create mode 100644 docs/modules.rst diff --git a/docs/conf.py b/docs/conf.py index cde7e288..037d2f46 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -52,18 +52,18 @@ master_doc = 'index' # General information about the project. -project = u'Voluptuous' -copyright = u'2016, Alec Thomas' -author = u'Alec Thomas' +project = 'Voluptuous' +copyright = '2016, Alec Thomas' +author = 'Alec Thomas' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = u'0.8.11' +version = '0.8.11' # The full version, including alpha/beta/rc tags. -release = u'0.8.11' +release = '0.8.11' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -231,8 +231,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'Voluptuous.tex', u'Voluptuous Documentation', - u'Alec Thomas', 'manual'), + (master_doc, 'Voluptuous.tex', 'Voluptuous Documentation', + 'Alec Thomas', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -261,7 +261,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'voluptuous', u'Voluptuous Documentation', + (master_doc, 'voluptuous', 'Voluptuous Documentation', [author], 1) ] @@ -275,7 +275,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'Voluptuous', u'Voluptuous Documentation', + (master_doc, 'Voluptuous', 'Voluptuous Documentation', author, 'Voluptuous', 'One line description of project.', 'Miscellaneous'), ] diff --git a/docs/index.rst b/docs/index.rst index e61b532c..25b19ee6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,10 +11,9 @@ Contents: .. toctree:: :maxdepth: 2 + modules voluptuous - - Indices and tables ================== diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 00000000..afb56175 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,7 @@ +voluptuous +========== + +.. toctree:: + :maxdepth: 4 + + voluptuous diff --git a/docs/voluptuous.rst b/docs/voluptuous.rst index 0df86449..c2d31a59 100644 --- a/docs/voluptuous.rst +++ b/docs/voluptuous.rst @@ -1,35 +1,37 @@ -Voluptuous Package +voluptuous package ================== -:mod:`schema_builder` Module ------------------------------ +Submodules +---------- -.. automodule:: voluptuous.schema_builder +voluptuous.error module +----------------------- + +.. automodule:: voluptuous.error :members: :undoc-members: :show-inheritance: -:mod:`error` Module ------------------------------ +voluptuous.schema_builder module +-------------------------------- -.. automodule:: voluptuous.error +.. automodule:: voluptuous.schema_builder :members: :undoc-members: :show-inheritance: -:mod:`validators` Module ------------------------------ +voluptuous.util module +---------------------- -.. automodule:: voluptuous.validators +.. automodule:: voluptuous.util :members: :undoc-members: :show-inheritance: +voluptuous.validators module +---------------------------- -:mod:`util` Module ------------------------------ - -.. automodule:: voluptuous.util +.. automodule:: voluptuous.validators :members: :undoc-members: :show-inheritance: diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 728d6b49..c2645998 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -345,7 +345,7 @@ def _url_validation(v): return parsed -@message('expected an Email', cls=UrlInvalid) +@message('expected an Email', cls=EmailInvalid) def Email(v): """Verify that the value is an Email or not. From 8707e927ca2501d94f87af9a3efc48a60d228433 Mon Sep 17 00:00:00 2001 From: thatneat Date: Wed, 8 Jun 2016 15:54:28 -0700 Subject: [PATCH 055/261] error humanization wrappers for #140 Solution for #140 - provide an easy humanize-error wrapper. --- voluptuous/humanize.py | 39 +++++++++++++++++++++++++++++++++++++++ voluptuous/tests/tests.py | 22 ++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 voluptuous/humanize.py diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py new file mode 100644 index 00000000..ba350238 --- /dev/null +++ b/voluptuous/humanize.py @@ -0,0 +1,39 @@ +from voluptuous import Invalid, MultipleInvalid +from voluptuous.error import Error + + +MAX_VALIDATION_ERROR_ITEM_LENGTH = 500 + + +def _nested_getitem(data, path): + for item_index in path: + try: + data = data[item_index] + except (KeyError, IndexError): + # The index is not present in the dictionary, list or other indexable + return None + return data + + +def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH): + """ Provide a more helpful + complete validation error message than that provided automatically + Invalid and MultipleInvalid do not include the offending value in error messages, + and MultipleInvalid.__str__ only provides the first error. + """ + if isinstance(validation_error, MultipleInvalid): + return '\n'.join(sorted( + humanize_error(data, sub_error, max_sub_error_length) + for sub_error in validation_error.errors + )) + else: + offending_item_summary = repr(_nested_getitem(data, validation_error.path)) + if len(offending_item_summary) > max_sub_error_length: + offending_item_summary = offending_item_summary[:max_sub_error_length - 3] + '...' + return '%s. Got %s' % (validation_error, offending_item_summary) + + +def validate_with_humanized_errors(data, schema, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH): + try: + return schema(data) + except (Invalid, MultipleInvalid) as e: + raise Error(humanize_error(data, e, max_sub_error_length)) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 0f4a9e11..3d012af3 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,6 +6,7 @@ Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA ) +from voluptuous.humanize import humanize_error def test_required(): @@ -383,6 +384,27 @@ def is_even(value): assert False, "Did not raise Invalid" +def test_humanize_error(): + data = { + 'a': 'not an int', + 'b': [123] + } + schema = Schema({ + 'a': int, + 'b': [str] + }) + try: + schema(data) + except MultipleInvalid as e: + assert_equal( + humanize_error(data, e), + "expected int for dictionary value @ data['a']. Got 'not an int'\n" + "expected str @ data['b'][0]. Got 123" + ) + else: + assert False, 'Did not raise MultipleInvalid' + + def test_fix_157(): s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) assert_equal(['one'], s(['one'])) From 25dbd1da7e8d4f2d265830e12a60c0ebc3ccbe43 Mon Sep 17 00:00:00 2001 From: thatneat Date: Thu, 9 Jun 2016 18:25:20 -0700 Subject: [PATCH 056/261] Update travis.yml with python 3.4, 3.5 support --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index b9b14497..14ed583a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ python: # Not quite ready for prime time... - "3.2" - "3.3" + - "3.4" + - "3.5" - "pypy" # command to install dependencies #install: "pip install -r requirements.txt --use-mirrors" From b5f56b54a66815b606d8a2b993ff40f145b8bea2 Mon Sep 17 00:00:00 2001 From: thatneat Date: Thu, 9 Jun 2016 22:33:13 -0700 Subject: [PATCH 057/261] Add python 3.5 to tox.ini test suite Since there are python 3 versions in travis.yml, adding at least one here will make it easier to test python 3 compatibility in development. --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2725abe5..f5219fb3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py26,py27 +envlist = flake8,py26,py27,py35 [flake8] ; E501: line too long (X > 79 characters) @@ -29,3 +29,6 @@ basepython = python2.6 [testenv:py27] basepython = python2.7 + +[testenv:py35] +basepython = python3.5 From 00fa853c7e0eee8116fe2f8fe122554ec48f52c3 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Tue, 21 Jun 2016 18:04:01 +0530 Subject: [PATCH 058/261] Removed docs from master to avoid confusion --- docs/Makefile | 230 ---------------------------------- docs/__init__.py | 1 - docs/conf.py | 293 -------------------------------------------- docs/index.rst | 22 ---- docs/modules.rst | 7 -- docs/voluptuous.rst | 37 ------ 6 files changed, 590 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/__init__.py delete mode 100644 docs/conf.py delete mode 100644 docs/index.rst delete mode 100644 docs/modules.rst delete mode 100644 docs/voluptuous.rst diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 3ebc3e16..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,230 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) - $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Voluptuous.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Voluptuous.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Voluptuous" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Voluptuous" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." diff --git a/docs/__init__.py b/docs/__init__.py deleted file mode 100644 index f29719c7..00000000 --- a/docs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'tusharmakkar08' diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 037d2f46..00000000 --- a/docs/conf.py +++ /dev/null @@ -1,293 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Voluptuous documentation build configuration file, created by -# sphinx-quickstart on Fri May 13 18:38:05 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'Voluptuous' -copyright = '2016, Alec Thomas' -author = 'Alec Thomas' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.8.11' -# The full version, including alpha/beta/rc tags. -release = '0.8.11' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -#html_title = u'Voluptuous v0.8.11' - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -#html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'Voluptuousdoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'Voluptuous.tex', 'Voluptuous Documentation', - 'Alec Thomas', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'voluptuous', 'Voluptuous Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'Voluptuous', 'Voluptuous Documentation', - author, 'Voluptuous', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 25b19ee6..00000000 --- a/docs/index.rst +++ /dev/null @@ -1,22 +0,0 @@ -.. Voluptuous documentation master file, created by - sphinx-quickstart on Mon May 9 19:29:37 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Voluptuous's documentation! -====================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - modules - voluptuous - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index afb56175..00000000 --- a/docs/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -voluptuous -========== - -.. toctree:: - :maxdepth: 4 - - voluptuous diff --git a/docs/voluptuous.rst b/docs/voluptuous.rst deleted file mode 100644 index c2d31a59..00000000 --- a/docs/voluptuous.rst +++ /dev/null @@ -1,37 +0,0 @@ -voluptuous package -================== - -Submodules ----------- - -voluptuous.error module ------------------------ - -.. automodule:: voluptuous.error - :members: - :undoc-members: - :show-inheritance: - -voluptuous.schema_builder module --------------------------------- - -.. automodule:: voluptuous.schema_builder - :members: - :undoc-members: - :show-inheritance: - -voluptuous.util module ----------------------- - -.. automodule:: voluptuous.util - :members: - :undoc-members: - :show-inheritance: - -voluptuous.validators module ----------------------------- - -.. automodule:: voluptuous.validators - :members: - :undoc-members: - :show-inheritance: From 842e464782350ee7156ba4389c1a932e9a9eb4cb Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Tue, 21 Jun 2016 18:06:56 +0530 Subject: [PATCH 059/261] Added Documentation Link --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index fd84c64e..bc25ec1d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ You can also contact me directly via [email](mailto:alec@swapoff.org) or 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/). + ## Show me an example Twitter's [user search API](https://dev.twitter.com/docs/api/1/get/users/search) accepts From ad26313ca5dd7d1859d28ba877f0c204bf633cc2 Mon Sep 17 00:00:00 2001 From: Hugo Lopes Tavares Date: Thu, 7 Jul 2016 16:06:11 -0400 Subject: [PATCH 060/261] Add validate_schema decorator --- voluptuous/schema_builder.py | 14 ++++++++++++++ voluptuous/tests/tests.py | 12 +++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 9f4d84d3..81af7268 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1016,3 +1016,17 @@ def wrapper(*args, **kwargs): return check return decorator + + +def validate_schema(*a, **kw): + schema = Schema(*a, **kw) + + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + result = f(*args, **kwargs) + schema(result) + return result + return wrapper + + return decorator diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 3d012af3..2b772725 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -4,7 +4,8 @@ from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, - Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA + Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, + validate_schema, ) from voluptuous.humanize import humanize_error @@ -409,3 +410,12 @@ def test_fix_157(): s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) assert_equal(['one'], s(['one'])) assert_raises(MultipleInvalid, s, ['four']) + + +def test_schema_decorator(): + @validate_schema(int) + def fn(arg): + return arg + + fn(1) + assert_raises(Invalid, fn, 1.0) From cee2a8c79d7b062802e5dcc88a6c2d41632844e6 Mon Sep 17 00:00:00 2001 From: thatneat Date: Thu, 21 Jul 2016 16:02:58 -0700 Subject: [PATCH 061/261] 0.9.0 ~20 backwards-compatible upgrades since the last release - the time is ripe! Minor version bump is appropriate as per semver.org standard. --- voluptuous/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 20e0d553..e0e098a7 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -9,5 +9,5 @@ from .validators import * from .util import * -__version__ = '0.8.11' +__version__ = '0.9.0' __author__ = 'tusharmakkar08' From 17204b79ca56b65aa3e4cfb20e7ef4555c2e8592 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 22 Jul 2016 21:52:40 +1000 Subject: [PATCH 062/261] 0.9.1 to fix missing Error import. Fixes #187. --- voluptuous/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index e0e098a7..df503fbf 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -4,10 +4,12 @@ from schema_builder import * from validators import * from util import * + from error import * except ImportError: from .schema_builder import * from .validators import * from .util import * + from .error import * -__version__ = '0.9.0' +__version__ = '0.9.1' __author__ = 'tusharmakkar08' From ffba10154b642227ae5948f7556c9889c889f9d7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Jul 2016 19:11:35 -0700 Subject: [PATCH 063/261] Remove setup_requires from setup.py Fixes #189 --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 41464982..61402539 100644 --- a/setup.py +++ b/setup.py @@ -50,8 +50,5 @@ ], install_requires=[ 'setuptools >= 0.6b1', - ], - setup_requires=[ - 'flake8', ] ) From 3741f28b1f007a19a70635b3a1d46d450cf410d2 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Mon, 1 Aug 2016 13:20:51 +1000 Subject: [PATCH 064/261] Bump to 0.9.2 --- voluptuous/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index df503fbf..32cbc406 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -11,5 +11,5 @@ from .util import * from .error import * -__version__ = '0.9.1' +__version__ = '0.9.2' __author__ = 'tusharmakkar08' From c684bae5d50ac685133d30e52cfa6a60877a7bae Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 3 Aug 2016 09:03:50 +1000 Subject: [PATCH 065/261] Include tests in sdist. Fixes #188. --- MANIFEST.in | 2 ++ voluptuous/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index f03451d5..dd15c3a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ include *.md include COPYING +include voluptuous/tests/*.py +include voluptuous/tests/*.md diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 32cbc406..5d568e20 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -11,5 +11,5 @@ from .util import * from .error import * -__version__ = '0.9.2' +__version__ = '0.9.3' __author__ = 'tusharmakkar08' From e92c865d03c326717990537d0d9984773b3cb75b Mon Sep 17 00:00:00 2001 From: Dan Girellini Date: Wed, 17 Aug 2016 15:41:28 -0700 Subject: [PATCH 066/261] Invert Range tests so nan is excluded --- voluptuous/tests/tests.py | 5 +++++ voluptuous/validators.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 2b772725..c16cfa79 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -419,3 +419,8 @@ def fn(arg): fn(1) assert_raises(Invalid, fn, 1.0) + + +def test_range_exlcudes_nan(): + s = Schema(Range(min=0, max=10)) + assert_raises(MultipleInvalid, s, float('nan')) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index c2645998..99bc5232 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -475,19 +475,19 @@ def __init__(self, min=None, max=None, min_included=True, def __call__(self, v): if self.min_included: - if self.min is not None and v < self.min: + 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 v <= self.min: + 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 v > self.max: + 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 v >= self.max: + 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 From 406dca11ddb37ad7a0652a8120fec060346cd667 Mon Sep 17 00:00:00 2001 From: Elliot Marx Date: Mon, 22 Aug 2016 18:13:06 -0700 Subject: [PATCH 067/261] Fix Exact Sequence to Match Lengths --- voluptuous/tests/tests.py | 13 ++++++++++++- voluptuous/validators.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index c16cfa79..11e136ba 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,11 +5,22 @@ Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate_schema, + validate_schema, ExactSequence ) from voluptuous.humanize import humanize_error +def test_exact_sequence(): + schema = Schema(ExactSequence([int, int])) + try: + schema([1, 2, 3]) + except Invalid: + assert True + else: + assert False, "Did not raise Invalid" + assert_equal(schema([1, 2]), [1, 2]) + + def test_required(): """Verify that Required works.""" schema = Schema({Required('q'): 1}) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 99bc5232..7da697f3 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -634,7 +634,7 @@ def __init__(self, validators, **kwargs): self._schemas = [Schema(val, **kwargs) for val in validators] def __call__(self, v): - if not isinstance(v, (list, tuple)): + 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)) From bf392074baf9e47182bf28eb9cd2191c0af8cbcb Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Sun, 11 Sep 2016 13:26:41 +0300 Subject: [PATCH 068/261] Add Equal validator --- voluptuous/tests/tests.py | 18 +++++++++++++++++- voluptuous/validators.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 11e136ba..d545cf63 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,7 +5,7 @@ Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate_schema, ExactSequence + validate_schema, ExactSequence, Equal ) from voluptuous.humanize import humanize_error @@ -435,3 +435,19 @@ def fn(arg): def test_range_exlcudes_nan(): s = Schema(Range(min=0, max=10)) assert_raises(MultipleInvalid, s, float('nan')) + + +def test_equal(): + s = Schema(Equal(1)) + s(1) + assert_raises(Invalid, s, 2) + s = Schema(Equal('foo')) + s('foo') + assert_raises(Invalid, s, 'bar') + s = Schema(Equal([1, 2])) + s([1, 2]) + assert_raises(Invalid, s, []) + assert_raises(Invalid, s, [1, 2, 3]) + # Evaluates exactly, not through validators + s = Schema(Equal(str)) + assert_raises(Invalid, s, 'foo') diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 7da697f3..a2576e0b 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -691,3 +691,32 @@ def __call__(self, 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=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) From 2f36d98f896cb7f1d1cdc99025ed6db7ad5bb2c1 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Sun, 11 Sep 2016 13:30:32 +0300 Subject: [PATCH 069/261] Add Unordered validator --- voluptuous/tests/tests.py | 30 +++++++++++++++++++- voluptuous/validators.py | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index d545cf63..766dd879 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,7 +5,7 @@ Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate_schema, ExactSequence, Equal + validate_schema, ExactSequence, Equal, Unordered ) from voluptuous.humanize import humanize_error @@ -451,3 +451,31 @@ def test_equal(): # Evaluates exactly, not through validators s = Schema(Equal(str)) assert_raises(Invalid, s, 'foo') + + +def test_unordered(): + # Any order is OK + s = Schema(Unordered([2, 1])) + s([2, 1]) + s([1, 2]) + # Amount of errors is OK + assert_raises(Invalid, s, [2, 0]) + assert_raises(MultipleInvalid, s, [0, 0]) + # Different length is NOK + assert_raises(Invalid, s, [1]) + assert_raises(Invalid, s, [1, 2, 0]) + assert_raises(MultipleInvalid, s, [1, 2, 0, 0]) + # Other type than list or tuple is NOK + assert_raises(Invalid, s, 'foo') + assert_raises(Invalid, s, 10) + # Validators are evaluated through as schemas + s = Schema(Unordered([int, str])) + s([1, '2']) + s(['1', 2]) + s = Schema(Unordered([{'foo': int}, []])) + s([{'foo': 3}, []]) + # Most accurate validators must be positioned on left + s = Schema(Unordered([int, 3])) + assert_raises(Invalid, s, [3, 2]) + s = Schema(Unordered([3, int])) + s([3, 2]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index a2576e0b..d751d73b 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -720,3 +720,62 @@ def __call__(self, 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, msg=None, **kwargs): + 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)) From 59211d70bed0361e6001130b716e4ec23313b9ad Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Mon, 12 Sep 2016 11:36:32 +0300 Subject: [PATCH 070/261] Fix empty list evaluation Empty lists are now regarded as is. If a free-form list is needed, use 'list'. --- README.md | 19 +++++++++++++++++++ voluptuous/schema_builder.py | 2 +- voluptuous/tests/tests.py | 6 ++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bc25ec1d..1eae1180 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,25 @@ in the schema list is compared to each value in the input data: ``` +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([]) +>>> schema([1]) # doctest: +IGNORE_EXCEPTION_DETAIL +Traceback (most recent call last): + ... +MultipleInvalid: not a valid value +>>> schema([]) +[] +>>> schema = Schema(list) +>>> schema([]) +[] +>>> schema([1, 2]) +[1, 2] + +``` + ### Validation functions Validators are simple callables that raise an `Invalid` exception when diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 81af7268..5f686c99 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -203,7 +203,7 @@ def _compile(self, schema): return self._compile_object(schema) if isinstance(schema, collections.Mapping): return self._compile_dict(schema) - elif isinstance(schema, list): + elif isinstance(schema, list) and len(schema): return self._compile_list(schema) elif isinstance(schema, tuple): return self._compile_tuple(schema) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 766dd879..79afa49d 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -479,3 +479,9 @@ def test_unordered(): assert_raises(Invalid, s, [3, 2]) s = Schema(Unordered([3, int])) s([3, 2]) + + +def test_empty_list_as_exact(): + s = Schema([]) + assert_raises(Invalid, s, [1]) + s([]) From 3c5ea7f329f1b346b39a30fbf79b23fe42478065 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 16 Sep 2016 19:22:54 +0530 Subject: [PATCH 071/261] Fixes #204: Making Path validators resilient to None. --- voluptuous/validators.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index c2645998..69250935 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -416,8 +416,13 @@ def IsFile(v): True >>> with raises(FileInvalid, 'not a file'): ... IsFile()("random_filename_goes_here.py") + >>> with raises(FileInvalid, 'Not a file'): + ... IsFile()(None) """ - return os.path.isfile(v) + if v: + return os.path.isfile(v) + else: + raise FileInvalid('Not a file') @message('not a directory', cls=DirInvalid) @@ -427,8 +432,13 @@ def IsDir(v): >>> IsDir()('/') '/' + >>> with raises(DirInvalid, 'Not a directory'): + ... IsDir()(None) """ - return os.path.isdir(v) + if v: + return os.path.isdir(v) + else: + raise DirInvalid("Not a directory") @message('path does not exist', cls=PathInvalid) @@ -440,8 +450,13 @@ def PathExists(v): True >>> with raises(Invalid, 'path does not exist'): ... PathExists()("random_filename_goes_here.py") + >>> with raises(PathInvalid, 'Not a Path'): + ... PathExists()(None) """ - return os.path.exists(v) + if v: + return os.path.exists(v) + else: + raise PathInvalid("Not a Path") class Range(object): From 853244511b1bf92ea3a86edb50addc8aa7058f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Nagy?= Date: Sun, 18 Sep 2016 16:05:57 +0200 Subject: [PATCH 072/261] decorator: validate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introducing decorator that is able to validate input arguments and return value of the decorated function. Before calling the wrapped function, the validators specified in the decorator's argument list will be tested against the values passed at the function call. ``` @validate_schema(arg1=int, arg2=int) def foo(arg1, arg2): return arg1 * arg2 ``` After calling the function with the validated arguments, schema specified in `RETURNS_KEY` (currently `__return__`) will be applied against the output. ``` @validate_schema(arg1=int, arg2=int, __returns__=int) def foo(arg1, arg2): return arg1 * arg2 ``` See more in the related test cases. Signed-off-by: Gergő Nagy --- voluptuous/schema_builder.py | 76 ++++++++++++++++++++++++++---- voluptuous/tests/tests.py | 90 ++++++++++++++++++++++++++++++++---- 2 files changed, 147 insertions(+), 19 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 5f686c99..8f938635 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1018,15 +1018,71 @@ def wrapper(*args, **kwargs): return decorator -def validate_schema(*a, **kw): - schema = Schema(*a, **kw) +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] - def decorator(f): - @wraps(f) - def wrapper(*args, **kwargs): - result = f(*args, **kwargs) - schema(result) - return result - return wrapper + 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 - return decorator + +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): + """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 foo(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) 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 diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 79afa49d..86a43ceb 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,7 +5,7 @@ Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate_schema, ExactSequence, Equal, Unordered + validate, ExactSequence, Equal, Unordered ) from voluptuous.humanize import humanize_error @@ -423,14 +423,6 @@ def test_fix_157(): assert_raises(MultipleInvalid, s, ['four']) -def test_schema_decorator(): - @validate_schema(int) - def fn(arg): - return arg - - fn(1) - assert_raises(Invalid, fn, 1.0) - def test_range_exlcudes_nan(): s = Schema(Range(min=0, max=10)) @@ -485,3 +477,83 @@ def test_empty_list_as_exact(): s = Schema([]) assert_raises(Invalid, s, [1]) s([]) + + +def test_schema_decorator_match_with_args(): + @validate(int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_with_args(): + @validate(int) + def fn(arg): + return arg + + assert_raises(Invalid, fn, 1.0) + + +def test_schema_decorator_match_with_kwargs(): + @validate(arg=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_with_kwargs(): + @validate(arg=int) + def fn(arg): + return arg + + assert_raises(Invalid, fn, 1.0) + + +def test_schema_decorator_match_return_with_args(): + @validate(int, __return__=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_return_with_args(): + @validate(int, __return__=int) + def fn(arg): + return "hello" + + assert_raises(Invalid, fn, 1) + + +def test_schema_decorator_match_return_with_kwargs(): + @validate(arg=int, __return__=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_unmatch_return_with_kwargs(): + @validate(arg=int, __return__=int) + def fn(arg): + return "hello" + + assert_raises(Invalid, fn, 1) + + +def test_schema_decorator_return_only_match(): + @validate(__return__=int) + def fn(arg): + return arg + + fn(1) + + +def test_schema_decorator_return_only_unmatch(): + @validate(__return__=int) + def fn(arg): + return "hello" + + assert_raises(Invalid, fn, 1) From 894651e5ce84854374a80c5c1ea66de05743b013 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 20 Sep 2016 20:56:23 +0200 Subject: [PATCH 073/261] Handle non-subscriptable path inhumanize_error --- voluptuous/humanize.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py index ba350238..4b18f750 100644 --- a/voluptuous/humanize.py +++ b/voluptuous/humanize.py @@ -12,6 +12,8 @@ def _nested_getitem(data, path): except (KeyError, IndexError): # The index is not present in the dictionary, list or other indexable return None + except TypeError: # data is not subscriptable + break return data From a30f47fbe74b572d81040c231c792dcba2802611 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 20 Sep 2016 23:36:36 +0200 Subject: [PATCH 074/261] Consistent --- voluptuous/humanize.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py index 4b18f750..91ab2015 100644 --- a/voluptuous/humanize.py +++ b/voluptuous/humanize.py @@ -9,11 +9,10 @@ def _nested_getitem(data, path): for item_index in path: try: data = data[item_index] - except (KeyError, IndexError): - # The index is not present in the dictionary, list or other indexable + except (KeyError, IndexError, TypeError): + # The index is not present in the dictionary, list or other + # indexable or data is not subscriptable return None - except TypeError: # data is not subscriptable - break return data From adeb05b055259d74d8b30e1f25b693445b730fcb Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Thu, 22 Sep 2016 19:33:53 +0530 Subject: [PATCH 075/261] Adding eq for schemas --- voluptuous/schema_builder.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8f938635..c0f5a19c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -153,6 +153,21 @@ class Schema(object): 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: + + >>> 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)) + >>> validate1 = Schema(Object({'one': 'two', 'three': 'four'}, cls=Structure)) + >>> assert validate1 == validate + >>> validate2 = Schema(Object({'one1': 'two1', 'three': 'four'}, cls=Structure)) + >>> assert validate2 != validate """ _extra_to_name = { @@ -181,6 +196,11 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA): self.extra = int(extra) # ensure the value is an integer self._compiled = self._compile(schema) + def __eq__(self, other): + if other == self.schema: + return True + return False + def __repr__(self): return "" % ( self.schema, self._extra_to_name.get(self.extra, '??'), From aa66c6064493e0fb9398f6ab1ec7f586d21c8881 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Thu, 22 Sep 2016 19:35:42 +0530 Subject: [PATCH 076/261] Fixing flake8 issues --- voluptuous/schema_builder.py | 2 +- voluptuous/tests/tests.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index c0f5a19c..07a4be4f 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1073,7 +1073,7 @@ def validate(*a, **kw): Set restriction for returned value: >>> @validate(arg=int, __return__=int) - ... def foo(arg1): + ... def bar(arg1): ... return arg1 * 2 """ diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 86a43ceb..922d62b2 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -423,7 +423,6 @@ def test_fix_157(): assert_raises(MultipleInvalid, s, ['four']) - def test_range_exlcudes_nan(): s = Schema(Range(min=0, max=10)) assert_raises(MultipleInvalid, s, float('nan')) From 4d88b6b9129207fb2b6f8af77d67a397876a6c8a Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 23 Sep 2016 15:19:29 +0530 Subject: [PATCH 077/261] Resolved 208 --- voluptuous/schema_builder.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 07a4be4f..042a7138 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -158,16 +158,12 @@ class Schema(object): 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)) - >>> validate1 = Schema(Object({'one': 'two', 'three': 'four'}, cls=Structure)) - >>> assert validate1 == validate - >>> validate2 = Schema(Object({'one1': 'two1', 'three': 'four'}, cls=Structure)) - >>> assert validate2 != validate + >>> v = Schema({Required('a'): unicode}) + >>> v1 = Schema({Required('a'): unicode}) + >>> v2 = Schema({Required('b'): unicode}) + >>> assert v == v1 + >>> assert v != v2 + """ _extra_to_name = { @@ -197,10 +193,14 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA): self._compiled = self._compile(schema) def __eq__(self, other): - if other == self.schema: + if str(other) == str(self.schema): + # Because repr is combination mixture of object and schema return True return False + def __str__(self): + return str(self.schema) + def __repr__(self): return "" % ( self.schema, self._extra_to_name.get(self.extra, '??'), From aaa6bd26d7c043b7a8c8d44a8920812acf1929eb Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 23 Sep 2016 18:47:52 +0530 Subject: [PATCH 078/261] Adding coveralls badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1eae1180..fc26f40c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Voluptuous is a Python data validation library [![Build Status](https://travis-ci.org/alecthomas/voluptuous.png)](https://travis-ci.org/alecthomas/voluptuous) [![Stories in Ready](https://badge.waffle.io/alecthomas/voluptuous.png?label=ready&title=Ready)](https://waffle.io/alecthomas/voluptuous) +[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) Voluptuous, *despite* the name, is a Python data validation library. It is primarily intended for validating data coming into Python as JSON, From 1cba13ec98fc1a3dd2fbc02e2f67f238a451bdd8 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 23 Sep 2016 19:14:40 +0530 Subject: [PATCH 079/261] Making changes in travis.yml and coveragerc for coveralls and Python3.2 coverage issue fix --- .coveragerc | 2 ++ .travis.yml | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..d9786799 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = *tests* diff --git a/.travis.yml b/.travis.yml index 14ed583a..725c58dd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,11 @@ python: - "3.5" - "pypy" # command to install dependencies -#install: "pip install -r requirements.txt --use-mirrors" +install: + - pip install coveralls + # Need to do this since coverage is broken in travis https://github.com/travis-ci/travis-ci/issues/4866 + - pip install 'coverage<4' # command to run tests -script: nosetests +script: nosetests --with-coverage --cover-package=voluptuous +after_success: + - coveralls \ No newline at end of file From 55fe26de1eef7d248248aa25194d9fb517e51ca7 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Sat, 24 Sep 2016 14:26:49 +0300 Subject: [PATCH 080/261] Always evaluate {} as {} If you want a dict with any content, use 'dict', not Schema({}, extra=ALLOW_EXTRA). --- README.md | 22 ++++++++++++++++++++++ voluptuous/schema_builder.py | 12 +++++++++--- voluptuous/tests/tests.py | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1eae1180..da9540c7 100644 --- a/README.md +++ b/README.md @@ -360,6 +360,28 @@ token `extra` as a key: ``` +However, an empty dict (`{}`) is treated as is. If you want to specify a list that can +contain anything, specify it as `dict`: + +```pycon +>>> schema = Schema({}, extra=ALLOW_EXTRA) # don't do this +>>> try: +... schema({'extra': 1}) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "not a valid value" +True +>>> schema({}) +{} +>>> schema = Schema(dict) # do this instead +>>> schema({}) +{} +>>> schema({'extra': 1}) +{'extra': 1} + +``` + #### Required dictionary keys By default, keys in the schema are not required to be in the data: diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8f938635..04fc86b4 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -201,7 +201,7 @@ def _compile(self, schema): return lambda _, v: v if isinstance(schema, Object): return self._compile_object(schema) - if isinstance(schema, collections.Mapping): + if isinstance(schema, collections.Mapping) and len(schema): return self._compile_dict(schema) elif isinstance(schema, list) and len(schema): return self._compile_list(schema) @@ -366,7 +366,7 @@ def _compile_dict(self, schema): A dictionary schema will only validate a dictionary: - >>> validate = Schema({}) + >>> validate = Schema({'prop': str}) >>> with raises(er.MultipleInvalid, 'expected a dictionary'): ... validate([]) @@ -381,7 +381,6 @@ def _compile_dict(self, schema): >>> 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}) @@ -391,10 +390,17 @@ def _compile_dict(self, schema): >>> validate({10: 'twenty'}) {10: 'twenty'} + An empty dictionary is matched as value: + + >>> validate = Schema({}) + >>> with raises(er.MultipleInvalid, 'not a valid value'): + ... validate([]) + 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: + >>> validate = Schema({'one': 'two', 'three': 'four', int: str}) >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['10']"): ... validate({'10': 'twenty'}) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 86a43ceb..debd09a4 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -479,6 +479,40 @@ def test_empty_list_as_exact(): s([]) +def test_empty_dict_as_exact(): + # {} always evaluates as {} + s = Schema({}) + assert_raises(Invalid, s, {'extra': 1}) + s = Schema({}, extra=ALLOW_EXTRA) # this should not be used + assert_raises(Invalid, s, {'extra': 1}) + + # {...} evaluates as Schema({...}) + s = Schema({'foo': int}) + assert_raises(Invalid, s, {'foo': 1, 'extra': 1}) + s = Schema({'foo': int}, extra=ALLOW_EXTRA) + s({'foo': 1, 'extra': 1}) + + # dict matches {} or {...} + s = Schema(dict) + s({'extra': 1}) + s({}) + s = Schema(dict, extra=PREVENT_EXTRA) + s({'extra': 1}) + s({}) + + # nested {} evaluate as {} + s = Schema({ + 'inner': {} + }, extra=ALLOW_EXTRA) + assert_raises(Invalid, s, {'inner': {'extra': 1}}) + s({}) + s = Schema({ + 'inner': Schema({}, extra=ALLOW_EXTRA) + }) + assert_raises(Invalid, s, {'inner': {'extra': 1}}) + s({}) + + def test_schema_decorator_match_with_args(): @validate(int) def fn(arg): From 14edec3c73c6cc82d744b3a1dbd18498a89a77be Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 27 Sep 2016 21:02:54 +0300 Subject: [PATCH 081/261] Unify doctest conventions in readme --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index da9540c7..5441087a 100644 --- a/README.md +++ b/README.md @@ -224,10 +224,13 @@ contain anything, specify it as `list`: ```pycon >>> schema = Schema([]) ->>> schema([1]) # doctest: +IGNORE_EXCEPTION_DETAIL -Traceback (most recent call last): - ... -MultipleInvalid: not a valid value +>>> try: +... schema([1]) +... raise AssertionError('MultipleInvalid not raised') +... except MultipleInvalid as e: +... exc = e +>>> str(exc) == "not a valid value" +True >>> schema([]) [] >>> schema = Schema(list) From 2fe30adb32a9424ee2360c4b40069bcf1d45f7c0 Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Tue, 27 Sep 2016 21:06:21 +0300 Subject: [PATCH 082/261] Add CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..51127031 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## [Unreleased] + +**Changes**: + +- [#198](https://github.com/alecthomas/voluptuous/issues/198): + `{}` 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)`. + +**New**: + +**Fixes**: + +## 0.9.3 (2016-08-03) + +Changelog not kept for 0.9.3 and earlier releases. diff --git a/README.md b/README.md index 5441087a..f85f838f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ To file a bug, create a [new issue](https://github.com/alecthomas/voluptuous/iss The documentation is provided [here] (http://alecthomas.github.io/voluptuous/). +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). + ## Show me an example Twitter's [user search API](https://dev.twitter.com/docs/api/1/get/users/search) accepts From 5b1624c8589a0f163dcecfcefcf9834b489dd8c4 Mon Sep 17 00:00:00 2001 From: nareshnootoo Date: Wed, 21 Sep 2016 12:52:10 +0530 Subject: [PATCH 083/261] Added Number validation Added test cases for Number validations 1) test cases for while scale is None, precision is None, Both are None 2) Beaking down of Invalid conditions removed extra space 1) Used Decimal and string conversions 2) __get_precision_scale as private Modification in comment Removed InvalidOperation from import Change is class comment Made some changes in test cases class and __call__ method description Fixed class description Fixed class description 1) Moved Decimal import up 2) Added InvalidOperation exception instead of generic 3) Made __get_precision_scale to internal as _get_precision_scale 4) Added assert for doctests 5) Added case with only invalid precision and no scale Added assert for doctests 1) Added Yeild flag(decimal/string) 2) Fixed tests naming convention Fixed exception --- voluptuous/tests/tests.py | 100 +++++++++++++++++++++++++++++++++++++- voluptuous/validators.py | 62 ++++++++++++++++++++++- 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 86a43ceb..cd8965ca 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,7 +5,7 @@ Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered + validate, ExactSequence, Equal, Unordered, Number ) from voluptuous.humanize import humanize_error @@ -557,3 +557,101 @@ def fn(arg): return "hello" assert_raises(Invalid, fn, 1) + + +def test_number_validation_with_string(): + """ test with Number with string""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + try: + schema({"number": 'teststr'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Value must be a number enclosed with string for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_invalid_precision_invalid_scale(): + """ test with Number with invalid precision and scale""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + try: + schema({"number": '123456.712'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale(): + """ test with Number with valid precision and scale""" + schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=True)}) + out_ = schema({"number": '1234.00'}) + assert_equal(float(out_.get("number")), 1234.00) + + +def test_number_when_precision_scale_none(): + """ test with Number with no precision and scale""" + schema = Schema({"number" : Number(yield_decimal=True)}) + out_ = schema({"number": '12345678901234'}) + assert_equal(out_.get("number"), 12345678901234) + + +def test_number_when_precision_none_n_valid_scale_case1(): + """ test with Number with no precision and valid scale case 1""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789.34'}) + assert_equal(float(out_.get("number")), 123456789.34) + + +def test_number_when_precision_none_n_valid_scale_case2(): + """ test with Number with no precision and valid scale case 2 with zero in decimal part""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789012.00'}) + assert_equal(float(out_.get("number")), 123456789012.00) + + +def test_number_when_precision_none_n_invalid_scale(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + try: + schema({"number": '12345678901.234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_when_valid_precision_n_scale_none(): + """ test with Number with no precision and valid scale""" + schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + out_ = schema({"number": '1234567.8901234'}) + assert_equal(float(out_.get("number")), 1234567.8901234) + + +def test_number_when_invalid_precision_n_scale_none(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + try: + schema({"number": '12345674.8901234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 14 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_none(): + """ test with Number with valid precision, scale and no yield_decimal""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + out_ = schema({"number": '1234.00'}) + assert_equal(out_.get("number"), '1234.00') + + +def test_number_validation_with_valid_precision_scale_yield_decimal_false(): + """ test with Number with valid precision, scale and no yield_decimal""" + schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=False)}) + out_ = schema({"number": '1234.00'}) + assert_equal(out_.get("number"), '1234.00') + diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 4e3a3ac8..36a9fde8 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -3,7 +3,7 @@ import datetime import sys from functools import wraps - +from decimal import Decimal, InvalidOperation try: from schema_builder import Schema, raises, message @@ -794,3 +794,63 @@ def __call__(self, 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=None, scale=None, msg=None, yield_decimal=False): + 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): + """ + :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') + + return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) From a51b20cc8162d667a63ae0a7f9b4995a27b1c0fa Mon Sep 17 00:00:00 2001 From: nareshnootoo Date: Wed, 21 Sep 2016 12:52:10 +0530 Subject: [PATCH 084/261] Added Number validation. Added test cases for Number validations 1) test cases for while scale is None, precision is None, Both are None 2) Beaking down of Invalid conditions 1) Used Decimal and string conversions 2) __get_precision_scale as private Modification in comment Made some changes in test cases Fixed class description 1) Moved Decimal import up 2) Added InvalidOperation exception instead of generic 3) Made __get_precision_scale to internal as _get_precision_scale 4) Added assert for doctests 5) Added case with only invalid precision and no scale Added assert for doctests 1) Added Yeild flag(decimal/string) 2) Fixed tests naming convention Fixed exception Fixed test case naming --- voluptuous/tests/tests.py | 22 +++++++--------------- voluptuous/validators.py | 1 - 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index cd8965ca..97b2ce39 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -583,35 +583,35 @@ def test_number_validation_with_invalid_precision_invalid_scale(): assert False, "Did not raise Invalid for String" -def test_number_validation_with_valid_precision_scale(): +def test_number_validation_with_valid_precision_scale_yield_decimal_true(): """ test with Number with valid precision and scale""" schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=True)}) out_ = schema({"number": '1234.00'}) assert_equal(float(out_.get("number")), 1234.00) -def test_number_when_precision_scale_none(): +def test_number_when_precision_scale_none_yield_decimal_true(): """ test with Number with no precision and scale""" schema = Schema({"number" : Number(yield_decimal=True)}) out_ = schema({"number": '12345678901234'}) assert_equal(out_.get("number"), 12345678901234) -def test_number_when_precision_none_n_valid_scale_case1(): +def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): """ test with Number with no precision and valid scale case 1""" schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789.34'}) assert_equal(float(out_.get("number")), 123456789.34) -def test_number_when_precision_none_n_valid_scale_case2(): +def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): """ test with Number with no precision and valid scale case 2 with zero in decimal part""" schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789012.00'}) assert_equal(float(out_.get("number")), 123456789012.00) -def test_number_when_precision_none_n_invalid_scale(): +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: @@ -623,14 +623,14 @@ def test_number_when_precision_none_n_invalid_scale(): assert False, "Did not raise Invalid for String" -def test_number_when_valid_precision_n_scale_none(): +def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): """ test with Number with no precision and valid scale""" schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) out_ = schema({"number": '1234567.8901234'}) assert_equal(float(out_.get("number")), 1234567.8901234) -def test_number_when_invalid_precision_n_scale_none(): +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: @@ -642,16 +642,8 @@ def test_number_when_invalid_precision_n_scale_none(): assert False, "Did not raise Invalid for String" -def test_number_validation_with_valid_precision_scale_yield_decimal_none(): - """ test with Number with valid precision, scale and no yield_decimal""" - schema = Schema({"number" : Number(precision=6, scale=2)}) - out_ = schema({"number": '1234.00'}) - assert_equal(out_.get("number"), '1234.00') - - def test_number_validation_with_valid_precision_scale_yield_decimal_false(): """ test with Number with valid precision, scale and no yield_decimal""" schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=False)}) out_ = schema({"number": '1234.00'}) assert_equal(out_.get("number"), '1234.00') - diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 36a9fde8..51e247fb 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -796,7 +796,6 @@ 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), From 5e3bf148b48bf6a9e24743b5178cdede1b78853a Mon Sep 17 00:00:00 2001 From: Jim Wright Date: Thu, 29 Sep 2016 12:34:35 +0100 Subject: [PATCH 085/261] Prevent the 'u' prefix on keys --- voluptuous/schema_builder.py | 10 +++ voluptuous/tests/tests.py | 122 ++++++++++++++++++++++++++++++++++- voluptuous/util.py | 15 +++++ voluptuous/validators.py | 61 +++++++++++++++++- 4 files changed, 205 insertions(+), 3 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 042a7138..40c59bf6 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -260,12 +260,17 @@ def _compile_mapping(self, schema, invalid_msg=None): candidates = list(_iterate_mapping_candidates(_compiled_schema)) def validate_mapping(path, iterable, out): + try: + from util import to_utf8_py2 + except ImportError: + from .util import to_utf8_py2 required_keys = all_required_keys.copy() # keeps track of all default keys that haven't been filled default_keys = all_default_keys.copy() error = None errors = [] for key, value in iterable: + key = to_utf8_py2(key) key_path = path + [key] remove_key = False @@ -809,6 +814,11 @@ class Marker(object): """Mark nodes for special treatment.""" def __init__(self, schema_, msg=None): + try: + from util import to_utf8_py2 + except ImportError: + from .util import to_utf8_py2 + schema_ = to_utf8_py2(schema_) self.schema = schema_ self._schema = Schema(schema_) self.msg = msg diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 922d62b2..01b7c851 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,13 +1,14 @@ import copy -from nose.tools import assert_equal, assert_raises +from nose.tools import assert_equal, assert_raises, assert_true from voluptuous import ( Schema, Required, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered + validate, ExactSequence, Equal, Unordered, Number ) from voluptuous.humanize import humanize_error +from voluptuous.util import to_utf8_py2, u def test_exact_sequence(): @@ -556,3 +557,120 @@ def fn(arg): return "hello" assert_raises(Invalid, fn, 1) + + +def test_unicode_key_is_converted_to_utf8_when_in_marker(): + """Verify that when using unicode key the 'u' prefix is not thrown in the exception""" + schema = Schema({Required(u('q')): 1}) + # Can't use nose's raises (because we need to access the raised + # exception, nor assert_raises which fails with Python 2.6.9. + try: + schema({}) + except Invalid as e: + assert_equal(str(e), "required key not provided @ data['q']") + + +def test_number_validation_with_string(): + """ test with Number with string""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + try: + schema({"number": 'teststr'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Value must be a number enclosed with string for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_unicode_key_is_converted_to_utf8_when_plain_text(): + key = u('q') + schema = Schema({key: int}) + # Can't use nose's raises (because we need to access the raised + # exception, nor assert_raises which fails with Python 2.6.9. + try: + schema({key: 'will fail'}) + except Invalid as e: + assert_equal(str(e), "expected int for dictionary value @ data['q']") + + +def test_number_validation_with_invalid_precision_invalid_scale(): + """ test with Number with invalid precision and scale""" + schema = Schema({"number" : Number(precision=6, scale=2)}) + try: + schema({"number": '123456.712'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_true(): + """ test with Number with valid precision and scale""" + schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=True)}) + out_ = schema({"number": '1234.00'}) + assert_equal(float(out_.get("number")), 1234.00) + + +def test_number_when_precision_scale_none_yield_decimal_true(): + """ test with Number with no precision and scale""" + schema = Schema({"number" : Number(yield_decimal=True)}) + out_ = schema({"number": '12345678901234'}) + assert_equal(out_.get("number"), 12345678901234) + + +def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): + """ test with Number with no precision and valid scale case 1""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789.34'}) + assert_equal(float(out_.get("number")), 123456789.34) + + +def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): + """ test with Number with no precision and valid scale case 2 with zero in decimal part""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + out_ = schema({"number": '123456789012.00'}) + assert_equal(float(out_.get("number")), 123456789012.00) + + +def test_to_utf8(): + s = u('hello') + assert_true(isinstance(to_utf8_py2(s), str)) + + +def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + try: + schema({"number": '12345678901.234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Scale must be equal to 2 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): + """ test with Number with no precision and valid scale""" + schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + out_ = schema({"number": '1234567.8901234'}) + assert_equal(float(out_.get("number")), 1234567.8901234) + + +def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): + """ test with Number with no precision and invalid scale""" + schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + try: + schema({"number": '12345674.8901234'}) + except MultipleInvalid as e: + assert_equal(str(e), + "Precision must be equal to 14 for dictionary value @ data['number']") + else: + assert False, "Did not raise Invalid for String" + + +def test_number_validation_with_valid_precision_scale_yield_decimal_false(): + """ test with Number with valid precision, scale and no yield_decimal""" + schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=False)}) + out_ = schema({"number": '1234.00'}) + assert_equal(out_.get("number"), '1234.00') diff --git a/voluptuous/util.py b/voluptuous/util.py index 1a5f1815..b2157bc3 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,3 +1,5 @@ +import sys + try: from error import LiteralInvalid, TypeInvalid, Invalid from schema_builder import Schema, default_factory, raises @@ -148,3 +150,16 @@ def __str__(self): def __repr__(self): return repr(self.lit) + + +def to_utf8_py2(data): + if sys.version_info < (3,) and isinstance(data, unicode): + return data.encode('utf-8') + return data + + +def u(x): + if sys.version_info < (3,): + return unicode(x) + else: + return x diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 4e3a3ac8..51e247fb 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -3,7 +3,7 @@ import datetime import sys from functools import wraps - +from decimal import Decimal, InvalidOperation try: from schema_builder import Schema, raises, message @@ -794,3 +794,62 @@ def __call__(self, 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=None, scale=None, msg=None, yield_decimal=False): + 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): + """ + :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') + + return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) From 1998aaa364427d5aa8566a8452c4981a28135449 Mon Sep 17 00:00:00 2001 From: michaelp Date: Fri, 30 Sep 2016 15:53:45 -0400 Subject: [PATCH 086/261] adding the recursive schema extension and literal key interpretation for pull request #227 --- voluptuous/schema_builder.py | 37 +++++++++++++++++++++++++++++++++++- voluptuous/tests/tests.py | 27 +++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 40c59bf6..053d9517 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -608,8 +608,43 @@ def extend(self, schema, required=None, extra=None): assert type(self.schema) == dict and type(schema) == dict, 'Both schemas must be dictionary-based' result = self.schema.copy() - result.update(schema) + # returns the key that may have been passed as arugment 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 iteritems(schema): + + # if the key is already in the dictionary, we need to replace it + # transform key to literal before checking presence + if key_literal(key) in result_key_map: + + result_key = result_key_map[key_literal(key)] + result_value = result[result_key] + + # if both are dictionaries, we need to extend recursively + # create the new extended sub schema, then remove the old key and add the new one + if type(result_value) == dict and type(value) == dict: + 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_required = (required if required is not None else self.required) result_extra = (extra if extra is not None else self.extra) return Schema(result, required=result_required, extra=result_extra) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 01b7c851..8759b136 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -2,7 +2,7 @@ from nose.tools import assert_equal, assert_raises, assert_true from voluptuous import ( - Schema, Required, Extra, Invalid, In, Remove, Literal, + Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number @@ -337,6 +337,31 @@ def test_schema_extend_overrides(): assert extended.extra == ALLOW_EXTRA +def test_schema_extend_key_swap(): + """Verify that Schema.extend can replace keys, even when different markers are used""" + + base = Schema({Optional('a'): int}) + extension = {Required('a'): int} + extended = base.extend(extension) + + assert_equal(len(base.schema), 1) + assert_true(isinstance(list(base.schema)[0], Optional)) + assert_equal(len(extended.schema), 1) + assert_true((list(extended.schema)[0], Required)) + + +def test_subschema_extension(): + """Verify that Schema.extend adds and replaces keys in a subschema""" + + base = Schema({'a': {'b': int, 'c': float}}) + extension = {'d': str, 'a': {'b': str, 'e': int}} + extended = base.extend(extension) + + assert_equal(base.schema, {'a': {'b': int, 'c': float}}) + assert_equal(extension, {'d': str, 'a': {'b': str, 'e': int}}) + assert_equal(extended.schema, {'a': {'b': str, 'c': float, 'e': int}, 'd': str}) + + def test_repr(): """Verify that __repr__ returns valid Python expressions""" match = Match('a pattern', msg='message') From cfc85365915b9aa07b4e5dda0d2626d0e742b5df Mon Sep 17 00:00:00 2001 From: Dan Girellini Date: Sun, 9 Oct 2016 15:14:23 -0700 Subject: [PATCH 087/261] Validate namedtuples as tuples namedtuples can't be initialized with a tuple. Detect that datatype and initialize it with *args notation. Resolves #230 --- voluptuous/schema_builder.py | 10 +++++++++- voluptuous/tests/tests.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 053d9517..9e4b622c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -106,6 +106,10 @@ def iteritems(d): 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 @@ -557,7 +561,11 @@ def validate_sequence(path, data): errors.append(invalid) if errors: raise er.MultipleInvalid(errors) - return type(data)(out) + + if _isnamedtuple(data): + return type(data)(*out) + else: + return type(data)(out) return validate_sequence diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 8759b136..7ddd092c 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,4 +1,5 @@ import copy +import collections from nose.tools import assert_equal, assert_raises, assert_true from voluptuous import ( @@ -699,3 +700,14 @@ def test_number_validation_with_valid_precision_scale_yield_decimal_false(): schema = Schema({"number" : Number(precision=6, scale=2, yield_decimal=False)}) out_ = schema({"number": '1234.00'}) assert_equal(out_.get("number"), '1234.00') + + +def test_named_tuples_validate_as_tuples(): + NT = collections.namedtuple('NT', ['a', 'b']) + nt = NT(1, 2) + t = (1, 2) + + Schema((int, int))(nt) + Schema((int, int))(t) + Schema(NT(int, int))(nt) + Schema(NT(int, int))(t) From 8833a9785ebd2ac292dae33332b6dd8de59e07b4 Mon Sep 17 00:00:00 2001 From: "D.Ivanov" Date: Mon, 24 Oct 2016 16:15:45 +0300 Subject: [PATCH 088/261] Date format support --- voluptuous/error.py | 4 ++++ voluptuous/tests/tests.py | 15 ++++++++++++++- voluptuous/validators.py | 31 +++++++++++++++++++++++++++---- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/voluptuous/error.py b/voluptuous/error.py index 9d3774d6..064aae16 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -169,6 +169,10 @@ 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 diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 6dcfe302..0785d489 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,7 +6,7 @@ Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered, Number + validate, ExactSequence, Equal, Unordered, Number, Date, Datetime ) from voluptuous.humanize import humanize_error from voluptuous.util import to_utf8_py2, u @@ -745,3 +745,16 @@ def test_named_tuples_validate_as_tuples(): Schema((int, int))(t) Schema(NT(int, int))(nt) Schema(NT(int, int))(t) + + +def test_datetime(): + schema = Schema({"datetime": Datetime()}) + schema({"datetime": "2016-10-24T14:01:57.102152Z"}) + assert_raises(MultipleInvalid, schema, {"datetime": "2016-10-24T14:01:57"}) + + +def test_date(): + schema = Schema({"date": Date()}) + schema({"date": "2016-10-24"}) + assert_raises(MultipleInvalid, schema, {"date": "2016-10-2"}) + assert_raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 51e247fb..6a2f88d1 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -9,14 +9,14 @@ from schema_builder import Schema, raises, message from error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, - PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, InInvalid, TypeInvalid, - NotInInvalid) + PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, + TypeInvalid, NotInInvalid) except ImportError: from .schema_builder import Schema, raises, message from .error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, - PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, InInvalid, TypeInvalid, - NotInInvalid) + PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, + TypeInvalid, NotInInvalid) if sys.version_info >= (3,): @@ -587,6 +587,29 @@ 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' + FORMAT_DESCRIPTION = 'yyyy-mm-dd' + + def __call__(self, v): + try: + datetime.datetime.strptime(v, self.format) + if len(v) != len(self.FORMAT_DESCRIPTION): + raise DateInvalid( + self.msg or 'value has invalid length' + ' expected length %d (%s)' % (len(self.FORMAT_DESCRIPTION), self.FORMAT_DESCRIPTION)) + 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.""" From c3136c4449e5651f231468ca3628c55da6c327cc Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Sun, 30 Oct 2016 21:01:18 +0100 Subject: [PATCH 089/261] Clamp: add a blank line to the docstring for proper rendering --- voluptuous/validators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 6a2f88d1..f7ef2da5 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -519,6 +519,7 @@ 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 From e2f5e388e63fea776721202f46ededda7dc17d46 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Wed, 2 Nov 2016 17:10:38 +0530 Subject: [PATCH 090/261] Added update_documentation.sh script for updating gh-pages --- .travis.yml | 3 ++- update_documentation.sh | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 update_documentation.sh diff --git a/.travis.yml b/.travis.yml index 725c58dd..470558ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,5 @@ install: # command to run tests script: nosetests --with-coverage --cover-package=voluptuous after_success: - - coveralls \ No newline at end of file + - coveralls + - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh; fi' \ No newline at end of file diff --git a/update_documentation.sh b/update_documentation.sh new file mode 100644 index 00000000..d27b5b8d --- /dev/null +++ b/update_documentation.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +git checkout gh-pages +git merge master +pip install -r requirements.txt +sphinx-apidoc -o docs -f voluptuous \ No newline at end of file From 8c9df70d1e91ea3a2894b4d7ce9c4707e9a32be2 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Thu, 3 Nov 2016 17:27:05 +0530 Subject: [PATCH 091/261] Issue #217 fix --- .travis.yml | 30 ++++++++++++------------- update_documentation.sh | 50 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 470558ff..462e370a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,20 @@ language: python python: - - "2.6" - - "2.7" - # Not quite ready for prime time... - - "3.2" - - "3.3" - - "3.4" - - "3.5" - - "pypy" -# command to install dependencies +- '2.6' +- '2.7' +- '3.2' +- '3.3' +- '3.4' +- '3.5' +- pypy install: - - pip install coveralls - # Need to do this since coverage is broken in travis https://github.com/travis-ci/travis-ci/issues/4866 - - pip install 'coverage<4' -# command to run tests +- pip install coveralls +- pip install 'coverage<4' script: nosetests --with-coverage --cover-package=voluptuous after_success: - - coveralls - - 'if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh; fi' \ No newline at end of file +- coveralls +- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh $USERNAME $PASSWORD; fi +env: + global: + - secure: UKVFCaFRRECYeNaLJr4POqt6zENBjyUe79U/5b9pEGBFWzXWoJ+EElOFOJdkquL6u3AwL6Bw93GqRIYHKcRW70doCYiEI7p2CuXey2mjoC7bLKdk4Fcrj0MTbiS6WJxEDfcsP/Tj3tv4kPqA4nYYm9DQoNfUX3skns442h0zals= + - secure: EK2dbVB4T7qNFWCSu3tL+l2YnpcrCvPk9E3W05rGZnkT38Do21kVDncf8XRh/5Nn4J6zGmdoHw6NFqeQtF6/+3GNIqEW4PzA5x5pUx1rI6drB0hTaEURG3VYUmLOoQ/thziaEmnez8Qt1hUtn/0Jhl6eUYOtmSTSkDeLz7zehm0= diff --git a/update_documentation.sh b/update_documentation.sh index d27b5b8d..6ea98103 100644 --- a/update_documentation.sh +++ b/update_documentation.sh @@ -1,5 +1,49 @@ #!/usr/bin/env bash -git checkout gh-pages -git merge master + +# Merge pushes to development branch to stable branch +if [ ! -n $2 ] ; then + echo "Usage: merge.sh " + exit 1; +fi + +GIT_USER="$1" +GIT_PASS="$2" + +# Specify the development branch and stable branch names +FROM_BRANCH="master" +TO_BRANCH="gh-pages" + +# Get the current branch +export PAGER=cat +CURRENT_BRANCH=$(git log -n 1 --pretty=%d HEAD | cut -d"," -f3 | cut -d" " -f2 | cut -d")" -f1) +echo "current branch is '$CURRENT_BRANCH'" + +# Create the URL to push merge to +URL=$(git remote -v | head -n1 | cut -f2 | cut -d" " -f1) +echo "Repo url is $URL" +PUSH_URL="https://$GIT_USER:$GIT_PASS@${URL:6}" + +if [ "$CURRENT_BRANCH" = "$FROM_BRANCH" ] ; then + # Checkout the dev branch + #git checkout $FROM_BRANCH && \ + #echo "Checking out $TO_BRANCH..." && \ + + # Checkout the latest stable + git fetch origin ${TO_BRANCH}:${TO_BRANCH} && \ + git checkout ${TO_BRANCH} && \ + + # Merge the dev into latest stable + echo "Merging changes..." && \ + git merge ${FROM_BRANCH} && \ + + # Push changes back to remote vcs + echo "Pushing changes..." && \ + git push ${PUSH_URL} && \ + echo "Merge complete!" || \ + echo "Error Occurred. Merge failed" +else + echo "Not on $FROM_BRANCH. Skipping merge" +fi + pip install -r requirements.txt -sphinx-apidoc -o docs -f voluptuous \ No newline at end of file +sphinx-apidoc -o docs -f voluptuous From 8de48276c2e6dafdfd739bbde1b673ee542f16a4 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Fri, 4 Nov 2016 10:22:26 +0530 Subject: [PATCH 092/261] Fix auto updation travis issue and adding commit, username, password and Building html from sphinx --- update_documentation.sh | 62 ++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/update_documentation.sh b/update_documentation.sh index 6ea98103..76518354 100644 --- a/update_documentation.sh +++ b/update_documentation.sh @@ -13,6 +13,11 @@ GIT_PASS="$2" FROM_BRANCH="master" TO_BRANCH="gh-pages" +# Needed for setting identity +git config --global user.email "tusharmakkar08@gmail.com" +git config --global user.name "Tushar Makkar" +git config --global push.default "simple" + # Get the current branch export PAGER=cat CURRENT_BRANCH=$(git log -n 1 --pretty=%d HEAD | cut -d"," -f3 | cut -d" " -f2 | cut -d")" -f1) @@ -21,29 +26,36 @@ echo "current branch is '$CURRENT_BRANCH'" # Create the URL to push merge to URL=$(git remote -v | head -n1 | cut -f2 | cut -d" " -f1) echo "Repo url is $URL" -PUSH_URL="https://$GIT_USER:$GIT_PASS@${URL:6}" - -if [ "$CURRENT_BRANCH" = "$FROM_BRANCH" ] ; then - # Checkout the dev branch - #git checkout $FROM_BRANCH && \ - #echo "Checking out $TO_BRANCH..." && \ - - # Checkout the latest stable - git fetch origin ${TO_BRANCH}:${TO_BRANCH} && \ - git checkout ${TO_BRANCH} && \ - - # Merge the dev into latest stable - echo "Merging changes..." && \ - git merge ${FROM_BRANCH} && \ - - # Push changes back to remote vcs - echo "Pushing changes..." && \ - git push ${PUSH_URL} && \ - echo "Merge complete!" || \ - echo "Error Occurred. Merge failed" -else - echo "Not on $FROM_BRANCH. Skipping merge" -fi +PUSH_URL="https://$GIT_USER:$GIT_PASS@${URL:8}" + +git remote set-url origin ${PUSH_URL} + +echo "Checking out $FROM_BRANCH..." && \ +git fetch origin ${FROM_BRANCH}:${FROM_BRANCH} && \ +git checkout ${FROM_BRANCH} + + +echo "Checking out $TO_BRANCH..." && \ +# Checkout the latest stable +git fetch origin ${TO_BRANCH}:${TO_BRANCH} && \ +git checkout ${TO_BRANCH} && \ + +# Merge the dev into latest stable +echo "Merging changes..." && \ +git merge ${FROM_BRANCH} && \ + +# Push changes back to remote vcs +echo "Pushing changes..." && \ +git push origin gh-pages &> /dev/null && \ +echo "Merge complete!" || \ +echo "Error Occurred. Merge failed" + +export PYTHONPATH=${PYTHONPATH}:$(pwd):$(pwd)/voluptuous + +pip install -r requirements.txt && sphinx-apidoc -o docs -f voluptuous && +cd docs && make html || +echo "Sphinx not able to generate HTML" -pip install -r requirements.txt -sphinx-apidoc -o docs -f voluptuous +git status && git add . && +git commit -m "Auto updating documentation from $CURRENT_BRANCH" && +git push origin gh-pages &> /dev/null && echo "Documentation pushed" From aa9c39dad4e65303f62e6fcf65b89ef54a9022bb Mon Sep 17 00:00:00 2001 From: Andrea Crotti Date: Mon, 10 Oct 2016 10:21:44 +0100 Subject: [PATCH 093/261] Add a new validator Maybe(type) which can be used for values that are either of a certain type or None --- voluptuous/tests/tests.py | 14 +++++++++++++- voluptuous/validators.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 0785d489..8af39a13 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,7 +6,7 @@ Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered, Number, Date, Datetime + validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date ) from voluptuous.humanize import humanize_error from voluptuous.util import to_utf8_py2, u @@ -371,6 +371,7 @@ def test_repr(): max_included=False, msg='number not in range') coerce_ = Coerce(int, msg="moo") all_ = All('10', Coerce(int), msg='all msg') + maybe_int = Maybe(int) assert_equal(repr(match), "Match('a pattern', msg='message')") assert_equal(repr(replace), "Replace('you', 'I', msg='you and I')") @@ -380,6 +381,7 @@ def test_repr(): ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") + assert_equal(repr(maybe_int), "Maybe(%s)" % str(int)) def test_list_validation_messages(): @@ -499,6 +501,16 @@ def test_unordered(): s([3, 2]) +def test_maybe(): + assert_raises(TypeError, Maybe, lambda x: x) + + s = Schema(Maybe(int)) + assert s(1) == 1 + assert s(None) is None + + assert_raises(Invalid, s, 'foo') + + def test_empty_list_as_exact(): s = Schema([]) assert_raises(Invalid, s, [1]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index f7ef2da5..08fb0bfa 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -459,6 +459,35 @@ def PathExists(v): raise PathInvalid("Not a Path") +class Maybe(object): + """Validate that the object is of a given type or is None. + + :raises Invalid: if the value is not of the type declared and is not None + + >>> s = Schema(Maybe(int)) + >>> s(10) + 10 + >>> with raises(Invalid): + ... s("string") + + """ + def __init__(self, kind, msg=None): + if not isinstance(kind, type): + raise TypeError("kind has to be a type") + + self.kind = kind + self.msg = msg + + def __call__(self, v): + if v is not None and not isinstance(v, self.kind): + raise Invalid(self.msg or "%s must be None or of type %s" % (v, self.kind)) + + return v + + def __repr__(self): + return 'Maybe(%s)' % str(self.kind) + + class Range(object): """Limit a value to a range. From 97751b8e8576c49b53d03237f1d68f63f45ac011 Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Mon, 7 Nov 2016 10:25:16 +0530 Subject: [PATCH 094/261] Immediate fix for Issue_#214 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 462e370a..ff31fa52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ install: script: nosetests --with-coverage --cover-package=voluptuous after_success: - coveralls -- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh $USERNAME $PASSWORD; fi +#- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh $USERNAME $PASSWORD; fi # Fix this later env: global: - secure: UKVFCaFRRECYeNaLJr4POqt6zENBjyUe79U/5b9pEGBFWzXWoJ+EElOFOJdkquL6u3AwL6Bw93GqRIYHKcRW70doCYiEI7p2CuXey2mjoC7bLKdk4Fcrj0MTbiS6WJxEDfcsP/Tj3tv4kPqA4nYYm9DQoNfUX3skns442h0zals= From cdcafe95572e2248ac57e222266a34c8e97c3a5e Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Tue, 15 Nov 2016 17:56:44 +0530 Subject: [PATCH 095/261] Updated twitter api link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8be17a80..976405bf 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,11 @@ See [CHANGELOG.md](CHANGELOG.md). ## Show me an example -Twitter's [user search API](https://dev.twitter.com/docs/api/1/get/users/search) accepts +Twitter's [user search API](https://dev.twitter.com/rest/reference/get/users/search) accepts query URLs like: ``` -$ curl 'http://api.twitter.com/1/users/search.json?q=python&per_page=20&page=1 +$ curl 'http://api.twitter.com/1.1/users/search.json?q=python&per_page=20&page=1' ``` To validate this we might use a schema like: From 8104b6a697dc918c77f771cf329c7aeb33f62137 Mon Sep 17 00:00:00 2001 From: g Date: Tue, 6 Dec 2016 17:11:10 -0800 Subject: [PATCH 096/261] Added support for OrderedDict validation, including as the return type --- .gitignore | 1 + voluptuous/schema_builder.py | 2 +- voluptuous/tests/tests.py | 33 +++++++++++++++++++++++---------- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index e1c2457b..d437888e 100755 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ MANIFEST .idea venv docs/_build +.vscode/ \ No newline at end of file diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index dda6b876..0bcd2c9f 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -517,7 +517,7 @@ def validate_dict(path, data): if errors: raise er.MultipleInvalid(errors) - out = {} + out = data.__class__() return base_validate(path, iteritems(data), out) return validate_dict diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 8af39a13..6b320ef2 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -644,7 +644,7 @@ def test_unicode_key_is_converted_to_utf8_when_in_marker(): def test_number_validation_with_string(): """ test with Number with string""" - schema = Schema({"number" : Number(precision=6, scale=2)}) + schema = Schema({"number": Number(precision=6, scale=2)}) try: schema({"number": 'teststr'}) except MultipleInvalid as e: @@ -667,7 +667,7 @@ def test_unicode_key_is_converted_to_utf8_when_plain_text(): 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)}) + schema = Schema({"number": Number(precision=6, scale=2)}) try: schema({"number": '123456.712'}) except MultipleInvalid as e: @@ -679,28 +679,28 @@ def test_number_validation_with_invalid_precision_invalid_scale(): 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)}) + schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=True)}) out_ = schema({"number": '1234.00'}) assert_equal(float(out_.get("number")), 1234.00) def test_number_when_precision_scale_none_yield_decimal_true(): """ test with Number with no precision and scale""" - schema = Schema({"number" : Number(yield_decimal=True)}) + schema = Schema({"number": Number(yield_decimal=True)}) out_ = schema({"number": '12345678901234'}) assert_equal(out_.get("number"), 12345678901234) def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): """ test with Number with no precision and valid scale case 1""" - schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + schema = Schema({"number": Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789.34'}) assert_equal(float(out_.get("number")), 123456789.34) def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): """ test with Number with no precision and valid scale case 2 with zero in decimal part""" - schema = Schema({"number" : Number(scale=2, yield_decimal=True)}) + schema = Schema({"number": Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789012.00'}) assert_equal(float(out_.get("number")), 123456789012.00) @@ -712,7 +712,7 @@ def test_to_utf8(): 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)}) + schema = Schema({"number": Number(scale=2, yield_decimal=True)}) try: schema({"number": '12345678901.234'}) except MultipleInvalid as e: @@ -724,14 +724,14 @@ def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): 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)}) + schema = Schema({"number": Number(precision=14, yield_decimal=True)}) out_ = schema({"number": '1234567.8901234'}) assert_equal(float(out_.get("number")), 1234567.8901234) def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): """ test with Number with no precision and invalid scale""" - schema = Schema({"number" : Number(precision=14, yield_decimal=True)}) + schema = Schema({"number": Number(precision=14, yield_decimal=True)}) try: schema({"number": '12345674.8901234'}) except MultipleInvalid as e: @@ -743,7 +743,7 @@ def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): 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)}) + schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=False)}) out_ = schema({"number": '1234.00'}) assert_equal(out_.get("number"), '1234.00') @@ -770,3 +770,16 @@ def test_date(): schema({"date": "2016-10-24"}) assert_raises(MultipleInvalid, schema, {"date": "2016-10-2"}) assert_raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) + + +def test_ordered_dict(): + if not hasattr(collections, 'OrderedDict'): + # collections.OrderedDict was added in Python2.7; only run if present + return + schema = Schema({Number(): Number()}) # x, y pairs (for interpolation or something) + data = collections.OrderedDict([(5.0, 3.7), (24.0, 8.7), (43.0, 1.5), + (62.0, 2.1), (71.5, 6.7), (90.5, 4.1), + (109.0, 3.9)]) + out = schema(data) + assert isinstance(out, collections.OrderedDict), 'Collection is no longer ordered' + assert data.keys() == out.keys(), 'Order is not consistent' From b23fe91a04131ac9758aade47bd33706177f6477 Mon Sep 17 00:00:00 2001 From: ziky90 Date: Thu, 12 Jan 2017 11:06:10 +0100 Subject: [PATCH 097/261] added contains validation --- voluptuous/error.py | 4 ++++ voluptuous/tests/tests.py | 14 +++++++++++++- voluptuous/validators.py | 24 ++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/voluptuous/error.py b/voluptuous/error.py index 064aae16..aa87500a 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -101,6 +101,10 @@ 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.""" diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 6b320ef2..1f9b7ffd 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,7 +6,8 @@ Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date + validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, + Contains ) from voluptuous.humanize import humanize_error from voluptuous.util import to_utf8_py2, u @@ -73,6 +74,17 @@ def test_not_in(): assert False, "Did not raise NotInInvalid" +def test_contains(): + """Verify contains validation method.""" + schema = Schema({'color': Contains('red')}) + schema({'color': ['blue', 'red', 'yellow']}) + try: + schema({'color': ['blue', 'yellow']}) + except Invalid as e: + assert_equal(str(e), + "value is not allowed for dictionary value @ data['color']") + + def test_remove(): """Verify that Remove works.""" # remove dict keys diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 08fb0bfa..8756a26c 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -10,13 +10,13 @@ from error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, - TypeInvalid, NotInInvalid) + TypeInvalid, NotInInvalid, ContainsInvalid) except ImportError: from .schema_builder import Schema, raises, message from .error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, - TypeInvalid, NotInInvalid) + TypeInvalid, NotInInvalid, ContainsInvalid) if sys.version_info >= (3,): @@ -680,6 +680,26 @@ def __repr__(self): return 'NotIn(%s)' % (self.container,) +class Contains(object): + """Validate that a value is in a collection.""" + + def __init__(self, item, msg=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. From 00551bfd75a24f98523938da6caec11e35eea535 Mon Sep 17 00:00:00 2001 From: odedfos Date: Tue, 17 Jan 2017 16:43:20 +0200 Subject: [PATCH 098/261] Fix #259 performance issue of exponential complexity where of all dict keys are matched with all keys in schema --- voluptuous/schema_builder.py | 26 +++++++++++++++++++-- voluptuous/tests/tests.py | 44 ++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 0bcd2c9f..3edc8f2f 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -5,6 +5,8 @@ import sys from contextlib import contextmanager +import itertools + try: import error as er except ImportError: @@ -110,6 +112,9 @@ def _isnamedtuple(obj): return isinstance(obj, tuple) and hasattr(obj, '_fields') +primitive_types = (str, unicode, bool, int, float) + + class Undefined(object): def __nonzero__(self): return False @@ -263,6 +268,20 @@ def _compile_mapping(self, schema, invalid_msg=None): 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): try: from util import to_utf8_py2 @@ -278,9 +297,12 @@ def validate_mapping(path, iterable, out): 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) - for skey, (ckey, cvalue) in candidates: + for skey, (ckey, cvalue) in relevant_candidates: try: new_key = ckey(key_path, key) except er.Invalid as e: @@ -634,7 +656,7 @@ def key_literal(key): # for each item in the extension schema, replace duplicates # or add new keys for key, value in iteritems(schema): - + # 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: diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 1f9b7ffd..86bb858f 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -7,8 +7,7 @@ Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, - Contains -) + Contains, Marker) from voluptuous.humanize import humanize_error from voluptuous.util import to_utf8_py2, u @@ -82,7 +81,7 @@ def test_contains(): schema({'color': ['blue', 'yellow']}) except Invalid as e: assert_equal(str(e), - "value is not allowed for dictionary value @ data['color']") + "value is not allowed for dictionary value @ data['color']") def test_remove(): @@ -788,10 +787,47 @@ 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) + 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_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) From ebc12e4824ea8edf82a76d56d81d50a988cea5e9 Mon Sep 17 00:00:00 2001 From: odedfos Date: Tue, 24 Jan 2017 10:40:35 +0200 Subject: [PATCH 099/261] #259 Changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51127031..7ff032cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ **Fixes**: +- [#259](https://github.com/alecthomas/voluptuous/issues/259): + Fixed 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). + ## 0.9.3 (2016-08-03) Changelog not kept for 0.9.3 and earlier releases. From 576bd863d15bf3d0025dcf5f6a456700c31d9ebd Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 4 Feb 2017 19:38:20 -0800 Subject: [PATCH 100/261] Gitter link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 976405bf..d2341b66 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Voluptuous is a Python data validation library -[![Build Status](https://travis-ci.org/alecthomas/voluptuous.png)](https://travis-ci.org/alecthomas/voluptuous) [![Stories in Ready](https://badge.waffle.io/alecthomas/voluptuous.png?label=ready&title=Ready)](https://waffle.io/alecthomas/voluptuous) -[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) +[![Build Status](https://travis-ci.org/alecthomas/voluptuous.png)](https://travis-ci.org/alecthomas/voluptuous) +[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) [![Gitter chat](https://badges.gitter.im/alecthomas.png)](https://gitter.im/alecthomas/Lobby) Voluptuous, *despite* the name, is a Python data validation library. It is primarily intended for validating data coming into Python as JSON, From 6ead2fb8ac2f450aa2ab11097d834b1c7b5c4657 Mon Sep 17 00:00:00 2001 From: Holger Peters Date: Thu, 9 Feb 2017 09:55:56 +0100 Subject: [PATCH 101/261] Don't list setuptools as a runtime dependency --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 61402539..03a1ddd4 100644 --- a/setup.py +++ b/setup.py @@ -47,8 +47,5 @@ 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', - ], - install_requires=[ - 'setuptools >= 0.6b1', ] ) From 78ada809298f52723628645c2f67829cab850809 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Mon, 13 Feb 2017 19:19:10 +0000 Subject: [PATCH 102/261] Minor docstring correction for Inclusive (#267) --- voluptuous/schema_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 3edc8f2f..e426df33 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -984,7 +984,7 @@ def __init__(self, schema, group_of_exclusion, msg=None): class Inclusive(Optional): """ Mark a node in the schema as inclusive. - Exclusive keys inherited from Optional: + Inclusive keys inherited from Optional: >>> schema = Schema({ ... Inclusive('filename', 'file'): str, From adafd7682c6c1e4ba568e94113f6517e3032faed Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 17 Feb 2017 00:22:29 +1100 Subject: [PATCH 103/261] Clear up Contains() documentation and add clarifying tests. --- .gitignore | 3 ++- voluptuous/validators.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d437888e..4a7bc002 100755 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ MANIFEST .idea venv docs/_build -.vscode/ \ No newline at end of file +.vscode/ +.venv diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 8756a26c..2318dd6e 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -681,7 +681,14 @@ def __repr__(self): class Contains(object): - """Validate that a value is in a collection.""" + """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=None): self.item = item From 4f903a9ec1a5dbe6c70357dfefbf4a2a55cf9158 Mon Sep 17 00:00:00 2001 From: Zack Morris Date: Mon, 27 Feb 2017 12:04:29 -0500 Subject: [PATCH 104/261] Update CHANGELOG --- CHANGELOG.md | 51 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ff032cf..53e7fb73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,19 +4,64 @@ **Changes**: -- [#198](https://github.com/alecthomas/voluptuous/issues/198): +- [#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**: -- [#259](https://github.com/alecthomas/voluptuous/issues/259): - Fixed a performance issue of exponential complexity where all of the dict keys were matched against all keys in the schema. +- [#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) From 4594a62f9ef3299011ea51f5cd54c423ec337451 Mon Sep 17 00:00:00 2001 From: Vyacheslav Linnik Date: Tue, 7 Mar 2017 00:24:14 +0400 Subject: [PATCH 105/261] make markers hashable (#273) --- voluptuous/schema_builder.py | 11 +++++++++++ voluptuous/tests/tests.py | 14 ++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index e426df33..633a79e1 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -911,6 +911,15 @@ def __repr__(self): def __lt__(self, other): return self.schema < other.schema + 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 @@ -1070,6 +1079,8 @@ def __call__(self, v): def __repr__(self): return "Remove(%r)" % (self.schema,) + def __hash__(self): + return object.__hash__(self) def message(default=None, cls=None): """Convenience decorator to allow functions to provide a message. diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 86bb858f..884384ab 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -796,6 +796,20 @@ def test_ordered_dict(): assert data.keys() == out.keys(), 'Order is not consistent' +def test_marker_hashable(): + """Verify that you can get schema keys, even if markers were used""" + definition = { + Required('x'): int, Optional('y'): float, + Remove('j'): int, Remove(int): str, int: int + } + assert_equal(definition.get('x'), int) + assert_equal(definition.get('y'), float) + assert_true(Required('x') == Required('x')) + assert_true(Required('x') != Required('y')) + # Remove markers are not hashable + assert_equal(definition.get('j'), None) + + def test_validation_performance(): """ This test comes to make sure the validation complexity of dictionaries is done in a linear time. From 788d714e51a1369fa47b7779db2459f37c3b145f Mon Sep 17 00:00:00 2001 From: Tushar Makkar Date: Mon, 20 Mar 2017 16:50:52 +0530 Subject: [PATCH 106/261] Fixing issue 274 --- voluptuous/schema_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 633a79e1..1227673e 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -239,7 +239,7 @@ def _compile(self, schema): type_ = type(schema) if type_ is type: type_ = schema - if type_ in (bool, int, long, str, unicode, float, complex, object, + if type_ in (bool, bytes, int, long, str, unicode, float, complex, object, list, dict, type(None)) or callable(schema): return _compile_scalar(schema) raise er.SchemaError('unsupported schema data type %r' % From cd0be1764a229a2ef83c60394401843063b2a5b3 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Wed, 12 Apr 2017 19:57:26 +0530 Subject: [PATCH 107/261] Bumped up the version --- CHANGELOG.md | 2 ++ voluptuous/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e7fb73..6cd7273c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +## [0.10.2] + **Changes**: - [#195](https://github.com/alecthomas/voluptuous/pull/195): diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 5d568e20..b2ac93fd 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -11,5 +11,5 @@ from .util import * from .error import * -__version__ = '0.9.3' +__version__ = '0.10.2' __author__ = 'tusharmakkar08' From 44fa104dedea4970f826b88cfe1119ed5657fe4f Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 13 Apr 2017 11:46:34 +0200 Subject: [PATCH 108/261] Remove unicode translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unicode translation added in commit added in 5e3bf148b48bf6a9e24743b5178cdede1b78853a heavily broke Python 2. A basic test such as provided in test_unicode_as_key in this patch does not work anymore because of that conversion. This patches goes back to the behaviour that was in 0.9. --- voluptuous/schema_builder.py | 10 ---------- voluptuous/tests/tests.py | 35 +++++++++-------------------------- voluptuous/util.py | 6 ------ 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 1227673e..8b498145 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -283,17 +283,12 @@ def _compile_mapping(self, schema, invalid_msg=None): additional_candidates.append((skey, (ckey, cvalue))) def validate_mapping(path, iterable, out): - try: - from util import to_utf8_py2 - except ImportError: - from .util import to_utf8_py2 required_keys = all_required_keys.copy() # keeps track of all default keys that haven't been filled default_keys = all_default_keys.copy() error = None errors = [] for key, value in iterable: - key = to_utf8_py2(key) key_path = path + [key] remove_key = False @@ -885,11 +880,6 @@ class Marker(object): """Mark nodes for special treatment.""" def __init__(self, schema_, msg=None): - try: - from util import to_utf8_py2 - except ImportError: - from .util import to_utf8_py2 - schema_ = to_utf8_py2(schema_) self.schema = schema_ self._schema = Schema(schema_) self.msg = msg diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 884384ab..045ad122 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,5 +1,6 @@ import copy import collections +import sys from nose.tools import assert_equal, assert_raises, assert_true from voluptuous import ( @@ -9,7 +10,7 @@ validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, Contains, Marker) from voluptuous.humanize import humanize_error -from voluptuous.util import to_utf8_py2, u +from voluptuous.util import u def test_exact_sequence(): @@ -642,15 +643,13 @@ def fn(arg): assert_raises(Invalid, fn, 1) -def test_unicode_key_is_converted_to_utf8_when_in_marker(): - """Verify that when using unicode key the 'u' prefix is not thrown in the exception""" - schema = Schema({Required(u('q')): 1}) - # Can't use nose's raises (because we need to access the raised - # exception, nor assert_raises which fails with Python 2.6.9. - try: - schema({}) - except Invalid as e: - assert_equal(str(e), "required key not provided @ data['q']") +def test_unicode_as_key(): + if sys.version_info >= (3,): + text_type = str + else: + text_type = unicode + schema = Schema({text_type: int}) + schema({u("foobar"): 1}) def test_number_validation_with_string(): @@ -665,17 +664,6 @@ def test_number_validation_with_string(): assert False, "Did not raise Invalid for String" -def test_unicode_key_is_converted_to_utf8_when_plain_text(): - key = u('q') - schema = Schema({key: int}) - # Can't use nose's raises (because we need to access the raised - # exception, nor assert_raises which fails with Python 2.6.9. - try: - schema({key: 'will fail'}) - except Invalid as e: - assert_equal(str(e), "expected int for dictionary value @ data['q']") - - 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)}) @@ -716,11 +704,6 @@ def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): assert_equal(float(out_.get("number")), 123456789012.00) -def test_to_utf8(): - s = u('hello') - assert_true(isinstance(to_utf8_py2(s), str)) - - 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)}) diff --git a/voluptuous/util.py b/voluptuous/util.py index b2157bc3..3a09297f 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -152,12 +152,6 @@ def __repr__(self): return repr(self.lit) -def to_utf8_py2(data): - if sys.version_info < (3,) and isinstance(data, unicode): - return data.encode('utf-8') - return data - - def u(x): if sys.version_info < (3,): return unicode(x) From c493b8371c5ff15d010da3c2d3eaf57830c13ffd Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Thu, 13 Apr 2017 16:43:18 +0530 Subject: [PATCH 109/261] Version updated to accomodate unicode translation issue --- CHANGELOG.md | 5 +++++ voluptuous/__init__.py | 2 +- voluptuous/error.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cd7273c..89f07c27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +## [0.10.3] + +- [#278](https://github.com/alecthomas/voluptuous/pull/278): Unicode +translation to python 2 issue fixed. + ## [0.10.2] **Changes**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index b2ac93fd..c44c230a 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -11,5 +11,5 @@ from .util import * from .error import * -__version__ = '0.10.2' +__version__ = '0.10.3' __author__ = 'tusharmakkar08' diff --git a/voluptuous/error.py b/voluptuous/error.py index aa87500a..a6da577f 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -36,7 +36,7 @@ def __str__(self): return output + path def prepend(self, path): - self.path = path + self.path + self.path += path class MultipleInvalid(Invalid): From eea609997a085c6d7bd52ee2910c495773a62846 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Thu, 13 Apr 2017 16:47:55 +0530 Subject: [PATCH 110/261] Updated version for Readme in pypi --- CHANGELOG.md | 2 +- voluptuous/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89f07c27..b7b16f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -## [0.10.3] +## [0.10.4] - [#278](https://github.com/alecthomas/voluptuous/pull/278): Unicode translation to python 2 issue fixed. diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index c44c230a..3efb99d3 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -11,5 +11,5 @@ from .util import * from .error import * -__version__ = '0.10.3' +__version__ = '0.10.4' __author__ = 'tusharmakkar08' From 2a690a8e2102b5cfafe8f7b309ac5efcafcfd162 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Thu, 13 Apr 2017 17:19:25 +0530 Subject: [PATCH 111/261] Bumping to 0.10.5 for failing test case --- CHANGELOG.md | 2 +- voluptuous/__init__.py | 2 +- voluptuous/error.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7b16f4f..2887fd3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -## [0.10.4] +## [0.10.5] - [#278](https://github.com/alecthomas/voluptuous/pull/278): Unicode translation to python 2 issue fixed. diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 3efb99d3..a3d0567a 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -11,5 +11,5 @@ from .util import * from .error import * -__version__ = '0.10.4' +__version__ = '0.10.5' __author__ = 'tusharmakkar08' diff --git a/voluptuous/error.py b/voluptuous/error.py index a6da577f..aa87500a 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -36,7 +36,7 @@ def __str__(self): return output + path def prepend(self, path): - self.path += path + self.path = path + self.path class MultipleInvalid(Invalid): From a40494432ab913b2d4166ac5cf6f75d390e06922 Mon Sep 17 00:00:00 2001 From: Nick Gaya Date: Thu, 13 Apr 2017 18:50:19 -0700 Subject: [PATCH 112/261] Better old-style class support --- voluptuous/schema_builder.py | 4 ++-- voluptuous/tests/tests.py | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8b498145..da3b31df 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -237,7 +237,7 @@ def _compile(self, schema): elif isinstance(schema, tuple): return self._compile_tuple(schema) type_ = type(schema) - if type_ is type: + if inspect.isclass(schema): type_ = schema if type_ in (bool, bytes, int, long, str, unicode, float, complex, object, list, dict, type(None)) or callable(schema): @@ -700,7 +700,7 @@ def _compile_scalar(schema): >>> with raises(er.Invalid, 'not a valid value'): ... _compile_scalar(lambda v: float(v))([], 'a') """ - if isinstance(schema, type): + if inspect.isclass(schema): def validate_instance(path, data): if isinstance(data, schema): return data diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 045ad122..09fea5d0 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -5,7 +5,7 @@ from voluptuous import ( Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, - Url, MultipleInvalid, LiteralInvalid, NotIn, Match, Email, + Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, Contains, Marker) @@ -153,6 +153,39 @@ def test_literal(): assert False, "Did not raise Invalid" +def test_class(): + class C1(object): + pass + + schema = Schema(C1) + schema(C1()) + + try: + schema(None) + except MultipleInvalid as e: + assert_equal(str(e), "expected C1") + assert_equal(len(e.errors), 1) + assert_equal(type(e.errors[0]), TypeInvalid) + else: + assert False, "Did not raise Invalid" + + # In Python 2, this will be an old-style class (classobj instance) + class C2: + pass + + schema = Schema(C2) + schema(C2()) + + try: + schema(None) + except MultipleInvalid as e: + assert_equal(str(e), "expected C2") + assert_equal(len(e.errors), 1) + assert_equal(type(e.errors[0]), TypeInvalid) + else: + assert False, "Did not raise Invalid" + + def test_email_validation(): """ test with valid email """ schema = Schema({"email": Email()}) From 25da46e378192b3e995d09965c0af86a31843d9c Mon Sep 17 00:00:00 2001 From: Nick Gaya Date: Mon, 17 Apr 2017 11:08:43 -0700 Subject: [PATCH 113/261] Update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2887fd3d..d72f7c06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +- [#279](https://github.com/alecthomas/voluptuous/pull/279): + Treat Python 2 old-style classes like types when validating. + ## [0.10.5] - [#278](https://github.com/alecthomas/voluptuous/pull/278): Unicode From 1097e782ad355e566fec72ceb41994de9bd46d1e Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Tue, 18 Apr 2017 23:41:59 +0530 Subject: [PATCH 114/261] fixed issue 270 --- voluptuous/schema_builder.py | 1 + voluptuous/tests/tests.py | 24 ++++++++++++++-- voluptuous/validators.py | 54 ++++++++++++++++++++++-------------- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index da3b31df..671c99ef 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1072,6 +1072,7 @@ def __repr__(self): def __hash__(self): return object.__hash__(self) + def message(default=None, cls=None): """Convenience decorator to allow functions to provide a message. diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 09fea5d0..2148acb1 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,6 +1,8 @@ import copy import collections +import os import sys + from nose.tools import assert_equal, assert_raises, assert_true from voluptuous import ( @@ -8,7 +10,7 @@ Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, - Contains, Marker) + Contains, Marker, IsDir, IsFile, PathExists) from voluptuous.humanize import humanize_error from voluptuous.util import u @@ -849,7 +851,7 @@ def __call__(self, *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 + 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) @@ -861,3 +863,21 @@ def __call__(self, *args, **kwargs): 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()) + assert_raises(MultipleInvalid, schema, 3) + schema(os.path.dirname(os.path.abspath(__file__))) + + +def test_IsFile(): + schema = Schema(IsFile()) + assert_raises(MultipleInvalid, schema, 3) + schema(os.path.abspath(__file__)) + + +def test_PathExists(): + schema = Schema(PathExists()) + assert_raises(MultipleInvalid, schema, 3) + schema(os.path.abspath(__file__)) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 2318dd6e..c281f14a 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -18,9 +18,9 @@ PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid) - if sys.version_info >= (3,): import urllib.parse as urlparse + basestring = str else: import urlparse @@ -419,9 +419,13 @@ def IsFile(v): >>> with raises(FileInvalid, 'Not a file'): ... IsFile()(None) """ - if v: - return os.path.isfile(v) - else: + try: + if v: + v = str(v) + return os.path.isfile(v) + else: + raise FileInvalid('Not a file') + except TypeError: raise FileInvalid('Not a file') @@ -435,9 +439,13 @@ def IsDir(v): >>> with raises(DirInvalid, 'Not a directory'): ... IsDir()(None) """ - if v: - return os.path.isdir(v) - else: + try: + if v: + v = str(v) + return os.path.isdir(v) + else: + raise DirInvalid("Not a directory") + except TypeError: raise DirInvalid("Not a directory") @@ -453,9 +461,13 @@ def PathExists(v): >>> with raises(PathInvalid, 'Not a Path'): ... PathExists()(None) """ - if v: - return os.path.exists(v) - else: + try: + if v: + v = str(v) + return os.path.exists(v) + else: + raise PathInvalid("Not a Path") + except TypeError: raise PathInvalid("Not a Path") @@ -471,6 +483,7 @@ class Maybe(object): ... s("string") """ + def __init__(self, kind, msg=None): if not isinstance(kind, type): raise TypeError("kind has to be a type") @@ -704,7 +717,7 @@ def __call__(self, v): return v def __repr__(self): - return 'Contains(%s)' % (self.item, ) + return 'Contains(%s)' % (self.item,) class ExactSequence(object): @@ -866,10 +879,8 @@ def __call__(self, v): 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 - ]) + 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): @@ -904,15 +915,16 @@ def __call__(self, v): """ precision, scale, decimal_num = self._get_precision_scale(v) - if self.precision is not None and self.scale is not None and\ - precision != self.precision and scale != self.scale: - raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" %(self.precision, self.scale)) + if self.precision is not None and self.scale is not None and precision != self.precision\ + and scale != self.scale: + raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" % (self.precision, + self.scale)) else: if self.precision is not None and precision != self.precision: - raise Invalid(self.msg or "Precision must be equal to %s"%self.precision) + raise Invalid(self.msg or "Precision must be equal to %s" % self.precision) - if self.scale is not None and scale != self.scale : - raise Invalid(self.msg or "Scale must be equal to %s"%self.scale) + 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 From a3ac0dd7f5bcb446385d80b9da6a547ef8f7fe90 Mon Sep 17 00:00:00 2001 From: tusharmakkar08 Date: Fri, 21 Apr 2017 10:33:08 +0530 Subject: [PATCH 115/261] Updated changelog and readme --- CHANGELOG.md | 2 ++ README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d72f7c06..45769537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## [Unreleased] +- [#280](https://github.com/alecthomas/voluptuous/pull/280): Making +`IsDir()`, `IsFile()` and `PathExists()` consistent between different python versions. - [#279](https://github.com/alecthomas/voluptuous/pull/279): Treat Python 2 old-style classes like types when validating. diff --git a/README.md b/README.md index d2341b66..55847266 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ To file a bug, create a [new issue](https://github.com/alecthomas/voluptuous/iss ## Documentation -The documentation is provided [here] (http://alecthomas.github.io/voluptuous/). +The documentation is provided [here](http://alecthomas.github.io/voluptuous/). ## Changelog From f9b4da9fbab0941e235b52ef1ae5dd0ffd046f93 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Apr 2017 10:41:30 -0700 Subject: [PATCH 116/261] Allow more extensive schemas in Maybe --- voluptuous/tests/tests.py | 2 -- voluptuous/validators.py | 21 ++++++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 2148acb1..aed2e50c 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -549,8 +549,6 @@ def test_unordered(): def test_maybe(): - assert_raises(TypeError, Maybe, lambda x: x) - s = Schema(Maybe(int)) assert s(1) == 1 assert s(None) is None diff --git a/voluptuous/validators.py b/voluptuous/validators.py index c281f14a..cae7a44a 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -472,9 +472,10 @@ def PathExists(v): class Maybe(object): - """Validate that the object is of a given type or is None. + """Validate that the object matches given validator or is None. - :raises Invalid: if the value is not of the type declared and is not None + :raises Invalid: if the value does not match the given validator and is not + None >>> s = Schema(Maybe(int)) >>> s(10) @@ -484,21 +485,15 @@ class Maybe(object): """ - def __init__(self, kind, msg=None): - if not isinstance(kind, type): - raise TypeError("kind has to be a type") - - self.kind = kind - self.msg = msg + def __init__(self, validator): + self.validator = validator + self.schema = Any(None, validator) def __call__(self, v): - if v is not None and not isinstance(v, self.kind): - raise Invalid(self.msg or "%s must be None or of type %s" % (v, self.kind)) - - return v + return self.schema(v) def __repr__(self): - return 'Maybe(%s)' % str(self.kind) + return 'Maybe(%s)' % self.validator class Range(object): From e4acaa406d1efce2f64b2ea2a0a6aea6ebcabf53 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 30 Apr 2017 11:38:58 -0700 Subject: [PATCH 117/261] Add more test cases --- voluptuous/tests/tests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index aed2e50c..e3ab8279 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -552,9 +552,13 @@ def test_maybe(): s = Schema(Maybe(int)) assert s(1) == 1 assert s(None) is None - assert_raises(Invalid, s, 'foo') + s = Schema(Maybe({str: Coerce(int)})) + assert s({'foo': '100'}) == {'foo': 100} + assert s(None) is None + assert_raises(Invalid, s, {'foo': 'bar'}) + def test_empty_list_as_exact(): s = Schema([]) From f24fc020cb571f1049fa6f8b3766eb7089941c05 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 12 May 2017 23:21:32 +1000 Subject: [PATCH 118/261] Remove Python 3.2 from Travis config. Fixes #288. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ff31fa52..5eb05688 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: python python: - '2.6' - '2.7' -- '3.2' - '3.3' - '3.4' - '3.5' From 33fc381f7df43d7257dd436891d3718af4194676 Mon Sep 17 00:00:00 2001 From: likang Date: Sat, 13 May 2017 12:06:35 +0800 Subject: [PATCH 119/261] absolute import --- voluptuous/__init__.py | 14 ++++---------- voluptuous/schema_builder.py | 6 +----- voluptuous/util.py | 11 +++-------- voluptuous/validators.py | 17 +++++------------ 4 files changed, 13 insertions(+), 35 deletions(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index a3d0567a..a12f11c5 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -1,15 +1,9 @@ # flake8: noqa -try: - from schema_builder import * - from validators import * - from util import * - from error import * -except ImportError: - from .schema_builder import * - from .validators import * - from .util import * - from .error import * +from voluptuous.schema_builder import * +from voluptuous.validators import * +from voluptuous.util import * +from voluptuous.error import * __version__ = '0.10.5' __author__ = 'tusharmakkar08' diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 671c99ef..6e141381 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -6,11 +6,7 @@ from contextlib import contextmanager import itertools - -try: - import error as er -except ImportError: - from . import error as er +from voluptuous import error as er if sys.version_info >= (3,): long = int diff --git a/voluptuous/util.py b/voluptuous/util.py index 3a09297f..434c360c 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,13 +1,8 @@ import sys -try: - from error import LiteralInvalid, TypeInvalid, Invalid - from schema_builder import Schema, default_factory, raises - import validators -except ImportError: - from .error import LiteralInvalid, TypeInvalid, Invalid - from .schema_builder import Schema, default_factory, raises - from . import validators +from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid +from voluptuous.schema_builder import Schema, default_factory, raises +from voluptuous import validators __author__ = 'tusharmakkar08' diff --git a/voluptuous/validators.py b/voluptuous/validators.py index cae7a44a..d3ef5482 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -5,18 +5,11 @@ from functools import wraps from decimal import Decimal, InvalidOperation -try: - from schema_builder import Schema, raises, message - from error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, - AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, - PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, - TypeInvalid, NotInInvalid, ContainsInvalid) -except ImportError: - from .schema_builder import Schema, raises, message - from .error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, - AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, - PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, DateInvalid, InInvalid, - TypeInvalid, NotInInvalid, ContainsInvalid) +from voluptuous.schema_builder import Schema, raises, message +from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, + AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, + RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, + DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid) if sys.version_info >= (3,): import urllib.parse as urlparse From 23923663d5e987f5df5d20ce8bbc4d2ee849e7d7 Mon Sep 17 00:00:00 2001 From: Tymofii Trukhanov Date: Wed, 31 May 2017 17:19:49 +0100 Subject: [PATCH 120/261] Fix Coerce validator to catch decimal.InvalidOperation --- voluptuous/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index d3ef5482..fb2197f0 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -92,7 +92,7 @@ def __init__(self, type, msg=None): def __call__(self, v): try: return self.type(v) - except (ValueError, TypeError): + except (ValueError, TypeError, InvalidOperation): msg = self.msg or ('expected %s' % self.type_name) raise CoerceInvalid(msg) From 885178e7bede27245d9051ecc0fc5cb1642d4cc8 Mon Sep 17 00:00:00 2001 From: Jon Banafato Date: Mon, 26 Jun 2017 17:43:31 -0400 Subject: [PATCH 121/261] Declare support for Python 3.6 - Add 3.6 to the Travis-CI and tox configurations - Add 3.6 language classifier to `setup.py` --- .travis.yml | 1 + setup.py | 1 + tox.ini | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5eb05688..0569e450 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - '3.3' - '3.4' - '3.5' +- '3.6' - pypy install: - pip install coveralls diff --git a/setup.py b/setup.py index 03a1ddd4..d088ffb0 100644 --- a/setup.py +++ b/setup.py @@ -47,5 +47,6 @@ 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.6', ] ) diff --git a/tox.ini b/tox.ini index f5219fb3..76723326 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py26,py27,py35 +envlist = flake8,py26,py27,py35,py36 [flake8] ; E501: line too long (X > 79 characters) @@ -32,3 +32,6 @@ basepython = python2.7 [testenv:py35] basepython = python3.5 + +[testenv:py36] +basepython = python3.6 From d01e188ee0d25212b16e7405f9524df824a94767 Mon Sep 17 00:00:00 2001 From: Jon Banafato Date: Mon, 26 Jun 2017 18:11:11 -0400 Subject: [PATCH 122/261] Drop support for end-of-life Python versions Per the [dev guide](https://docs.python.org/devguide/#status-of-python-branches), the following versions have been end of lifed: - 2.6: 2013-10-29 - 3.1: 2012-04-11 - 3.2: 2016-02-20 Removing support for EOL versions should make future maintenance easier. --- .travis.yml | 1 - setup.py | 2 -- tox.ini | 5 +---- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0569e450..65830711 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: -- '2.6' - '2.7' - '3.3' - '3.4' diff --git a/setup.py b/setup.py index d088ffb0..db6d1bc8 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.6', diff --git a/tox.ini b/tox.ini index 76723326..319d4889 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py26,py27,py35,py36 +envlist = flake8,py27,py35,py36 [flake8] ; E501: line too long (X > 79 characters) @@ -24,9 +24,6 @@ commands = deps = flake8 commands = flake8 --doctests setup.py voluptuous -[testenv:py26] -basepython = python2.6 - [testenv:py27] basepython = python2.7 From 83600c32e45c532a49a217449028af48968211ea Mon Sep 17 00:00:00 2001 From: Jon Banafato Date: Mon, 26 Jun 2017 22:46:29 -0400 Subject: [PATCH 123/261] Sync references to Python versions Bring `setup.py`, `tox.ini`, and `.travis.yml` in sync with respect to supported Python versions. --- setup.py | 1 + tox.ini | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index db6d1bc8..25d6cf52 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ] ) diff --git a/tox.ini b/tox.ini index 319d4889..00015687 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py35,py36 +envlist = flake8,py27,py33,py34,py35,py36 [flake8] ; E501: line too long (X > 79 characters) @@ -27,6 +27,12 @@ commands = flake8 --doctests setup.py voluptuous [testenv:py27] basepython = python2.7 +[testenv:py33] +basepython = python3.3 + +[testenv:py34] +basepython = python3.4 + [testenv:py35] basepython = python3.5 From bb43c577bf12740dc9ef0351a9830d00de531802 Mon Sep 17 00:00:00 2001 From: mattkohl Date: Mon, 31 Jul 2017 17:30:34 +0100 Subject: [PATCH 124/261] Fixed curl command in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55847266..93758545 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Twitter's [user search API](https://dev.twitter.com/rest/reference/get/users/sea query URLs like: ``` -$ curl 'http://api.twitter.com/1.1/users/search.json?q=python&per_page=20&page=1' +$ 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: From 48d1ea8f0f07478e50da38935d7e452f80d8501b Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 8 Sep 2017 11:10:03 +0100 Subject: [PATCH 125/261] Allow partial validation when using validate decorator (#296) --- voluptuous/schema_builder.py | 3 ++- voluptuous/tests/tests.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 6e141381..429ab32c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1171,7 +1171,8 @@ def validate_schema_decorator(func): returns = schema_arguments[RETURNS_KEY] del schema_arguments[RETURNS_KEY] - input_schema = Schema(schema_arguments) if len(schema_arguments) != 0 else lambda x: x + input_schema = (Schema(schema_arguments, extra=ALLOW_EXTRA) + if len(schema_arguments) != 0 else lambda x: x) output_schema = Schema(returns) if returns_defined else lambda x: x @wraps(func) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index e3ab8279..8b82f981 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -680,6 +680,38 @@ def fn(arg): assert_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 + + assert_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 + + assert_raises(Invalid, fn, arg1=1, arg2="foo") + + def test_unicode_as_key(): if sys.version_info >= (3,): text_type = str From 61807818e1bd30a650d9759664e16cecf773679b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Oct 2017 22:05:04 -0700 Subject: [PATCH 126/261] Add description to Marker --- voluptuous/schema_builder.py | 18 +++++++++++------- voluptuous/tests/tests.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 429ab32c..2c430a5b 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -875,10 +875,11 @@ def __repr__(self): class Marker(object): """Mark nodes for special treatment.""" - def __init__(self, schema_, msg=None): + def __init__(self, schema_, msg=None, description=None): self.schema = schema_ self._schema = Schema(schema_) self.msg = msg + self.description = description def __call__(self, v): try: @@ -930,8 +931,9 @@ class Optional(Marker): {'key2': 'value'} """ - def __init__(self, schema, msg=None, default=UNDEFINED): - super(Optional, self).__init__(schema, msg=msg) + def __init__(self, schema, msg=None, default=UNDEFINED, description=None): + super(Optional, self).__init__(schema, msg=msg, + description=description) self.default = default_factory(default) @@ -971,8 +973,9 @@ class Exclusive(Optional): ... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}}) """ - def __init__(self, schema, group_of_exclusion, msg=None): - super(Exclusive, self).__init__(schema, msg=msg) + def __init__(self, schema, group_of_exclusion, msg=None, description=None): + super(Exclusive, self).__init__(schema, msg=msg, + description=description) self.group_of_exclusion = group_of_exclusion @@ -1038,8 +1041,9 @@ class Required(Marker): {'key': []} """ - def __init__(self, schema, msg=None, default=UNDEFINED): - super(Required, self).__init__(schema, msg=msg) + def __init__(self, schema, msg=None, default=UNDEFINED, description=None): + super(Required, self).__init__(schema, msg=msg, + description=description) self.default = default_factory(default) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 8b82f981..48cbfd0e 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,7 +6,7 @@ from nose.tools import assert_equal, assert_raises, assert_true from voluptuous import ( - Schema, Required, Optional, Extra, Invalid, In, Remove, Literal, + Schema, Required, Exclusive, Optional, Extra, Invalid, In, Remove, Literal, Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, @@ -915,3 +915,17 @@ def test_PathExists(): schema = Schema(PathExists()) assert_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' + + required = Required('key', description='Hello') + assert required.description == 'Hello' From 74f5a3460249f31e20a8f7b794b0c76f0fea4c42 Mon Sep 17 00:00:00 2001 From: Thibault Billon Date: Tue, 11 Jul 2017 17:26:19 +0200 Subject: [PATCH 127/261] Revert "Always evaluate {} as {}" as it breaks backward compatibility This reverts commit 55fe26de1eef7d248248aa25194d9fb517e51ca7. --- README.md | 22 ---------------------- voluptuous/schema_builder.py | 12 +++--------- voluptuous/tests/tests.py | 34 ---------------------------------- 3 files changed, 3 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 93758545..9f4bb19c 100644 --- a/README.md +++ b/README.md @@ -368,28 +368,6 @@ token `extra` as a key: ``` -However, an empty dict (`{}`) is treated as is. If you want to specify a list that can -contain anything, specify it as `dict`: - -```pycon ->>> schema = Schema({}, extra=ALLOW_EXTRA) # don't do this ->>> try: -... schema({'extra': 1}) -... raise AssertionError('MultipleInvalid not raised') -... except MultipleInvalid as e: -... exc = e ->>> str(exc) == "not a valid value" -True ->>> schema({}) -{} ->>> schema = Schema(dict) # do this instead ->>> schema({}) -{} ->>> schema({'extra': 1}) -{'extra': 1} - -``` - #### Required dictionary keys By default, keys in the schema are not required to be in the data: diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 2c430a5b..e79a935f 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -226,7 +226,7 @@ def _compile(self, schema): return lambda _, v: v if isinstance(schema, Object): return self._compile_object(schema) - if isinstance(schema, collections.Mapping) and len(schema): + if isinstance(schema, collections.Mapping): return self._compile_dict(schema) elif isinstance(schema, list) and len(schema): return self._compile_list(schema) @@ -408,7 +408,7 @@ def _compile_dict(self, schema): A dictionary schema will only validate a dictionary: - >>> validate = Schema({'prop': str}) + >>> validate = Schema({}) >>> with raises(er.MultipleInvalid, 'expected a dictionary'): ... validate([]) @@ -423,6 +423,7 @@ def _compile_dict(self, schema): >>> 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}) @@ -432,17 +433,10 @@ def _compile_dict(self, schema): >>> validate({10: 'twenty'}) {10: 'twenty'} - An empty dictionary is matched as value: - - >>> validate = Schema({}) - >>> with raises(er.MultipleInvalid, 'not a valid value'): - ... validate([]) - 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: - >>> validate = Schema({'one': 'two', 'three': 'four', int: str}) >>> with raises(er.MultipleInvalid, "extra keys not allowed @ data['10']"): ... validate({'10': 'twenty'}) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 48cbfd0e..351bb8c8 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -566,40 +566,6 @@ def test_empty_list_as_exact(): s([]) -def test_empty_dict_as_exact(): - # {} always evaluates as {} - s = Schema({}) - assert_raises(Invalid, s, {'extra': 1}) - s = Schema({}, extra=ALLOW_EXTRA) # this should not be used - assert_raises(Invalid, s, {'extra': 1}) - - # {...} evaluates as Schema({...}) - s = Schema({'foo': int}) - assert_raises(Invalid, s, {'foo': 1, 'extra': 1}) - s = Schema({'foo': int}, extra=ALLOW_EXTRA) - s({'foo': 1, 'extra': 1}) - - # dict matches {} or {...} - s = Schema(dict) - s({'extra': 1}) - s({}) - s = Schema(dict, extra=PREVENT_EXTRA) - s({'extra': 1}) - s({}) - - # nested {} evaluate as {} - s = Schema({ - 'inner': {} - }, extra=ALLOW_EXTRA) - assert_raises(Invalid, s, {'inner': {'extra': 1}}) - s({}) - s = Schema({ - 'inner': Schema({}, extra=ALLOW_EXTRA) - }) - assert_raises(Invalid, s, {'inner': {'extra': 1}}) - s({}) - - def test_schema_decorator_match_with_args(): @validate(int) def fn(arg): From 95489bd443e9a65489fdce705dd21ec9ea8598af Mon Sep 17 00:00:00 2001 From: Thibault Billon Date: Tue, 11 Jul 2017 17:51:51 +0200 Subject: [PATCH 128/261] Make Schema([]) usage consistent with Schema({}) Validation on an empty list was not raising any exception when given values but an empty dict was. Make it uniform and make them both raise a MultipleInvalid exception on unwanted values. --- README.md | 2 +- voluptuous/schema_builder.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9f4bb19c..12fcee28 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ contain anything, specify it as `list`: ... raise AssertionError('MultipleInvalid not raised') ... except MultipleInvalid as e: ... exc = e ->>> str(exc) == "not a valid value" +>>> str(exc) == "not a valid value @ data[1]" True >>> schema([]) [] diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index e79a935f..81940dcd 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -228,7 +228,7 @@ def _compile(self, schema): return self._compile_object(schema) if isinstance(schema, collections.Mapping): return self._compile_dict(schema) - elif isinstance(schema, list) and len(schema): + elif isinstance(schema, list): return self._compile_list(schema) elif isinstance(schema, tuple): return self._compile_tuple(schema) @@ -551,6 +551,10 @@ def validate_sequence(path, data): # Empty seq schema, allow any data. if not schema: + if data: + raise er.MultipleInvalid([ + er.ValueInvalid('not a valid value', [value]) for value in data + ]) return data out = [] From 9204e832eed4c6f27f483e7bfdc8d29ce0e21bf3 Mon Sep 17 00:00:00 2001 From: Dan Tao Date: Fri, 1 Dec 2017 16:31:08 -0600 Subject: [PATCH 129/261] add Schema.infer method (#311) This introduces the class method Schema.infer, to infer a Schema from concrete data. This will be useful for converting existing known-good data (e.g. API responses) into enforceable schemas. --- voluptuous/schema_builder.py | 44 ++++++++++++++++++++++ voluptuous/tests/tests.py | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 81940dcd..f4244c86 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -197,6 +197,50 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA): self.extra = int(extra) # ensure the value is an integer self._compiled = self._compile(schema) + @classmethod + def infer(cls, data, **kwargs): + """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 iteritems(value)} + 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 str(other) == str(self.schema): # Because repr is combination mixture of object and schema diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 351bb8c8..cc0ed617 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -828,6 +828,79 @@ def test_marker_hashable(): assert_equal(definition.get('j'), None) +def test_schema_infer(): + schema = Schema.infer({ + 'str': 'foo', + 'bool': True, + 'int': 42, + 'float': 3.14 + }) + assert_equal(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_equal(schema, Schema({ + Required('a'): { + Required('b'): { + Required('c'): str + } + } + })) + + +def test_schema_infer_list(): + schema = Schema.infer({ + 'list': ['foo', True, 42, 3.14] + }) + + assert_equal(schema, Schema({ + Required('list'): [str, bool, int, float] + })) + + +def test_schema_infer_scalar(): + assert_equal(Schema.infer('foo'), Schema(str)) + assert_equal(Schema.infer(True), Schema(bool)) + assert_equal(Schema.infer(42), Schema(int)) + assert_equal(Schema.infer(3.14), Schema(float)) + assert_equal(Schema.infer({}), Schema(dict)) + assert_equal(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. From d82601ed51a0d8e3a61bb49164cbe2683eed2edb Mon Sep 17 00:00:00 2001 From: Dan Tao Date: Thu, 7 Dec 2017 01:23:22 -0600 Subject: [PATCH 130/261] make Schema.__eq__ deterministic (#316) * add test for schema equality and fix Schema.__eq__ to deal with dicts properly (fixes #315) * add negative equality tests, implement Schema.__ne__ method to support != operator This fills in missing test coverage to ensure the __eq__ method does not return True in some potentially unexpected cases (these tests would fail before be867c5). Previously only the __eq__ method was implemented, which could lead to surprising behavior e.g.: Schema('foo') == Schema('foo') # True Schema('foo') != Schema('foo') # True This adds the __ne__ method so that these operators are complementary as one might expect. --- voluptuous/schema_builder.py | 10 +++--- voluptuous/tests/tests.py | 65 +++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index f4244c86..724c504c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -242,10 +242,12 @@ def value_to_schema_type(value): return cls(value_to_schema_type(data), **kwargs) def __eq__(self, other): - if str(other) == str(self.schema): - # Because repr is combination mixture of object and schema - return True - return False + 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) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index cc0ed617..25a938c6 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -3,7 +3,7 @@ import os import sys -from nose.tools import assert_equal, assert_raises, assert_true +from nose.tools import assert_equal, assert_false, assert_raises, assert_true from voluptuous import ( Schema, Required, Exclusive, Optional, Extra, Invalid, In, Remove, Literal, @@ -410,6 +410,69 @@ def test_subschema_extension(): assert_equal(extended.schema, {'a': {'b': str, 'c': float, 'e': int}, 'd': str}) +def test_equality(): + assert_equal(Schema('foo'), Schema('foo')) + + assert_equal(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_equal(Schema(dict_a), Schema(dict_b)) + + +def test_equality_negative(): + """Verify that Schema objects are not equal to string representations""" + assert_false(Schema('foo') == 'foo') + + assert_false(Schema(['foo', 'bar']) == "['foo', 'bar']") + assert_false(Schema(['foo', 'bar']) == Schema("['foo', 'bar']")) + + assert_false(Schema({'foo': 1, 'bar': 2}) == "{'foo': 1, 'bar': 2}") + assert_false(Schema({'foo': 1, 'bar': 2}) == Schema("{'foo': 1, 'bar': 2}")) + + +def test_inequality(): + assert_true(Schema('foo') != 'foo') + + assert_true(Schema(['foo', 'bar']) != "['foo', 'bar']") + assert_true(Schema(['foo', 'bar']) != Schema("['foo', 'bar']")) + + assert_true(Schema({'foo': 1, 'bar': 2}) != "{'foo': 1, 'bar': 2}") + assert_true(Schema({'foo': 1, 'bar': 2}) != Schema("{'foo': 1, 'bar': 2}")) + + +def test_inequality_negative(): + assert_false(Schema('foo') != Schema('foo')) + + assert_false(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_false(Schema(dict_a) != Schema(dict_b)) + + def test_repr(): """Verify that __repr__ returns valid Python expressions""" match = Match('a pattern', msg='message') From 1666a68c030e943d5af9c1e3ed028a72db9f3d21 Mon Sep 17 00:00:00 2001 From: Guy Arad Date: Thu, 7 Dec 2017 12:34:12 +0200 Subject: [PATCH 131/261] Adding SomeOf validator (#314) Adding SomeOf validator and corresponding tests. --- voluptuous/error.py | 10 +++++++ voluptuous/tests/tests.py | 39 ++++++++++++++++++++++++- voluptuous/validators.py | 61 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/voluptuous/error.py b/voluptuous/error.py index aa87500a..86c4e0a3 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -187,3 +187,13 @@ class NotInInvalid(Invalid): 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 diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 25a938c6..ee5f2fdb 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -10,7 +10,7 @@ Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, - Contains, Marker, IsDir, IsFile, PathExists) + Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, raises) from voluptuous.humanize import humanize_error from voluptuous.util import u @@ -1031,3 +1031,40 @@ def test_description(): 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('wqs2!#s111') + + 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_SomeOf_on_bounds_assertion(): + with raises(AssertionError, 'when using "SomeOf" you should specify at least one of min_valid and max_valid'): + SomeOf(validators=[]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index fb2197f0..138941a6 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -9,7 +9,8 @@ from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, - DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid) + DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, + TooManyValid) if sys.version_info >= (3,): import urllib.parse as urlparse @@ -933,3 +934,61 @@ def _get_precision_scale(self, number): raise Invalid(self.msg or 'Value must be a number enclosed with string') return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) + + +class SomeOf(object): + """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: a 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 more validations than the given amount is met + + >>> 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, min_valid=None, max_valid=None, **kwargs): + assert min_valid is not None or max_valid is not None, \ + 'when using "%s" you should specify at least one of min_valid and max_valid' % (type(self).__name__,) + self.min_valid = min_valid or 0 + self.max_valid = max_valid or len(validators) + self.validators = validators + self.msg = kwargs.pop('msg', None) + self._schemas = [Schema(val, **kwargs) for val in validators] + + def __call__(self, v): + errors = [] + for schema in self._schemas: + try: + v = schema(v) + except Invalid as e: + errors.append(e) + + passed_count = len(self._schemas) - 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) From 0dad58b6b51fd37e84eff6151cb91d6b3d8ccbdd Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Tue, 26 Dec 2017 12:03:28 +0100 Subject: [PATCH 132/261] Implement nested schema support and validators compilation (#318) * Allow to use nested schema This allows to refer to the current schema using voluptuous.Self and have nested definitions. Fixes #128 * Allow any validator to be compiled This allows any validator to be compiled by implementing the __voluptuous_compile__ method. This avoids having voluptuous.Any and voluptuous.All defining new Schema for sub-validators: they can be compiled recursively using the same parent schema. This solves the recursive Self case. Fixes #18 --- README.md | 15 +++--- voluptuous/schema_builder.py | 8 ++++ voluptuous/tests/tests.py | 71 +++++++++++++++++++++++++++- voluptuous/validators.py | 92 ++++++++++++++++++++++-------------- 4 files changed, 141 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 12fcee28..8b6acac5 100644 --- a/README.md +++ b/README.md @@ -443,18 +443,15 @@ True ``` -### Recursive schema +### Recursive / nested schema -There is no syntax to have a recursive schema. The best way to do it is to have a wrapper like this: +You can use `voluptuous.Self` to define a nested schema: ```pycon ->>> from voluptuous import Schema, Any ->>> def s2(v): -... return s1(v) -... ->>> s1 = Schema({"key": Any(s2, "value")}) ->>> s1({"key": {"key": "value"}}) -{'key': {'key': 'value'}} +>>> from voluptuous import Schema, Self +>>> recursive = Schema({"more": Self, "value": int}) +>>> recursive({"more": {"value": 42}, "value": 41}) == {'more': {'value': 42}, 'value': 41} +True ``` diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 724c504c..dff3e558 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -122,6 +122,10 @@ def __repr__(self): UNDEFINED = Undefined() +def Self(): + raise er.SchemaError('"Self" should never be called') + + def default_factory(value): if value is UNDEFINED or callable(value): return value @@ -270,6 +274,10 @@ def __call__(self, 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.Mapping): diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index ee5f2fdb..98a82ca3 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -10,7 +10,8 @@ Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, - Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, raises) + Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, + raises) from voluptuous.humanize import humanize_error from voluptuous.util import u @@ -1065,6 +1066,74 @@ def test_SomeOf_max_validation(): validator('Aa1') +def test_self_validation(): + schema = Schema({"number": int, + "follow": Self}) + try: + schema({"number": "abc"}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + try: + schema({"follow": {"number": '123456.712'}}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + schema({"follow": {"number": 123456}}) + schema({"follow": {"follow": {"number": 123456}}}) + + +def test_self_any(): + schema = Schema({"number": int, + "follow": Any(Self, "stop")}) + try: + schema({"number": "abc"}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + try: + schema({"follow": {"number": '123456.712'}}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + 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) + try: + schema({"number": "abc"}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + try: + schema({"follow": {"number": '123456.712'}}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + schema({"follow": {"number": 123456}}) + schema({"follow": {"follow": {"number": 123456}}}) + schema({"follow": {"number": 123456, "extra_number": 123}}) + try: + schema({"follow": {"number": 123456, "extra_number": "123"}}) + except MultipleInvalid: + pass + else: + assert False, "Did not raise Invalid" + + 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=[]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 138941a6..af655c33 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -181,7 +181,40 @@ def Boolean(v): return bool(v) -class Any(object): +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, **kwargs): + self.validators = validators + self.msg = kwargs.pop('msg', None) + + def __voluptuous_compile__(self, schema): + self._compiled = [ + schema._compile(v) + for v in self.validators + ] + return self._run + + def _run(self, path, value): + 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 + ) + + +class Any(_WithSubValidators): """Use the first validated value. :param msg: Message to deliver to user if validation fails. @@ -206,16 +239,14 @@ class Any(object): ... validate(4) """ - def __init__(self, *validators, **kwargs): - self.validators = validators - self.msg = kwargs.pop('msg', None) - self._schemas = [Schema(val, **kwargs) for val in validators] - - def __call__(self, v): + def _exec(self, funcs, v, path=None): error = None - for schema in self._schemas: + for func in funcs: try: - return schema(v) + 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 @@ -224,15 +255,12 @@ def __call__(self, v): raise error if self.msg is None else AnyInvalid(self.msg) raise AnyInvalid(self.msg or 'no valid value found') - def __repr__(self): - return 'Any([%s])' % (", ".join(repr(v) for v in self.validators)) - # Convenience alias Or = Any -class All(object): +class All(_WithSubValidators): """Value must pass all validators. The output of each validator is passed as input to the next. @@ -245,25 +273,17 @@ class All(object): 10 """ - def __init__(self, *validators, **kwargs): - self.validators = validators - self.msg = kwargs.pop('msg', None) - self._schemas = [Schema(val, **kwargs) for val in validators] - - def __call__(self, v): + def _exec(self, funcs, v, path=None): try: - for schema in self._schemas: - v = schema(v) + 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) return v - def __repr__(self): - return 'All(%s, msg=%r)' % ( - ", ".join(repr(v) for v in self.validators), - self.msg - ) - # Convenience alias And = All @@ -936,7 +956,7 @@ def _get_precision_scale(self, number): return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) -class SomeOf(object): +class SomeOf(_WithSubValidators): """Value must pass at least some validations, determined by the given parameter. Optionally, number of passed validations can be capped. @@ -965,19 +985,21 @@ def __init__(self, validators, min_valid=None, max_valid=None, **kwargs): '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) - self.validators = validators - self.msg = kwargs.pop('msg', None) - self._schemas = [Schema(val, **kwargs) for val in validators] + super(SomeOf, self).__init__(*validators, **kwargs) - def __call__(self, v): + def _exec(self, funcs, v, path=None): errors = [] - for schema in self._schemas: + funcs = list(funcs) + for func in funcs: try: - v = schema(v) + if path is None: + v = func(v) + else: + v = func(path, v) except Invalid as e: errors.append(e) - passed_count = len(self._schemas) - len(errors) + passed_count = len(funcs) - len(errors) if self.min_valid <= passed_count <= self.max_valid: return v From b99459ffb6c9932abae3c6882c634a108a34f6ab Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Sat, 30 Dec 2017 12:35:14 +0100 Subject: [PATCH 133/261] Replace Maybe class with a function The Any class can be compiled by voluptuous whereas Maybe cannot. Since it's just an alias to Any(None, v), replace it with that directly. --- voluptuous/tests/tests.py | 2 +- voluptuous/validators.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 98a82ca3..2977f354 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -492,7 +492,7 @@ def test_repr(): ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") - assert_equal(repr(maybe_int), "Maybe(%s)" % str(int)) + assert_equal(repr(maybe_int), "Any(None, %s, msg=None)" % str(int)) def test_list_validation_messages(): diff --git a/voluptuous/validators.py b/voluptuous/validators.py index af655c33..e39461c1 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -485,7 +485,7 @@ def PathExists(v): raise PathInvalid("Not a Path") -class Maybe(object): +def Maybe(validator): """Validate that the object matches given validator or is None. :raises Invalid: if the value does not match the given validator and is not @@ -498,16 +498,7 @@ class Maybe(object): ... s("string") """ - - def __init__(self, validator): - self.validator = validator - self.schema = Any(None, validator) - - def __call__(self, v): - return self.schema(v) - - def __repr__(self): - return 'Maybe(%s)' % self.validator + return Any(None, validator) class Range(object): From d27d1ee85d389c8265f026a2cb65aa21b6db4f2d Mon Sep 17 00:00:00 2001 From: Tuukka Mustonen Date: Fri, 12 Jan 2018 11:48:26 +0200 Subject: [PATCH 134/261] Link to pytest-voluptuous in README (#321) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8b6acac5..6521b062 100644 --- a/README.md +++ b/README.md @@ -620,5 +620,9 @@ Voluptuous is heavily inspired by [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. From aa29b08907afc464860d322255557a2ba29fa000 Mon Sep 17 00:00:00 2001 From: Robert Schindler Date: Sat, 3 Feb 2018 10:56:56 +0100 Subject: [PATCH 135/261] Added validation for default values --- CHANGELOG.md | 4 +++- voluptuous/schema_builder.py | 33 +++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45769537..9b97fd2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,11 @@ ## [Unreleased] - [#280](https://github.com/alecthomas/voluptuous/pull/280): Making -`IsDir()`, `IsFile()` and `PathExists()` consistent between different python versions. + `IsDir()`, `IsFile()` and `PathExists()` consistent between different python versions. - [#279](https://github.com/alecthomas/voluptuous/pull/279): Treat Python 2 old-style classes like types when validating. +- [324](https://github.com/alecthomas/voluptuous/pull/324): + Default values MUST now pass validation just as any regular value. ## [0.10.5] diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index dff3e558..88a0203e 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -334,11 +334,25 @@ def _compile_mapping(self, schema, invalid_msg=None): def validate_mapping(path, iterable, out): required_keys = all_required_keys.copy() - # keeps track of all default keys that haven't been filled - default_keys = all_default_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() + error = None errors = [] - for key, value in iterable: + for key, value in key_value_map.items(): key_path = path + [key] remove_key = False @@ -388,12 +402,10 @@ def validate_mapping(path, iterable, out): required_keys.discard(skey) break - # Key and value okay, mark any Required() fields as found. + # Key and value okay, mark as found in case it was + # a Required() field. required_keys.discard(skey) - # No need for a default if it was filled - default_keys.discard(skey) - break else: if remove_key: @@ -405,13 +417,6 @@ def validate_mapping(path, iterable, out): errors.append(er.Invalid('extra keys not allowed', key_path)) # else REMOVE_EXTRA: ignore the key so it's removed from output - # set defaults for any that can have defaults - for key in default_keys: - if not isinstance(key.default, Undefined): # if the user provides a default with the node - out[key.schema] = key.default() - if key in required_keys: - required_keys.discard(key) - # 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' From a2063031a22102e7f22875d4f6a6da0976cb13d4 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 9 Feb 2018 12:04:11 +0000 Subject: [PATCH 136/261] Added note about backward incompatible change The change in pull request #324 can be backward incompatible if a default value does not validate against the schema --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b97fd2b..0ee5487d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - [#279](https://github.com/alecthomas/voluptuous/pull/279): Treat Python 2 old-style classes like types when validating. - [324](https://github.com/alecthomas/voluptuous/pull/324): - Default values MUST now pass validation just as any regular value. + 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. ## [0.10.5] From 2f2c7784f67622bbd9ed590ed7434b7e2cd307d0 Mon Sep 17 00:00:00 2001 From: Mohammed Mohammed Date: Tue, 13 Feb 2018 19:05:18 -0600 Subject: [PATCH 137/261] Modified __lt__ in Marker class. Marker class in /voluptuous/schema_builder.py needed to have the __lt__() function modified in order to compare against regular strings as well. This will allow the usage of Voluptuous Optional objects as keys in dicts alongside strings and int. --- CHANGELOG.md | 2 ++ voluptuous/schema_builder.py | 4 +++- voluptuous/tests/tests.md | 5 +++++ voluptuous/tests/tests.py | 4 ++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee5487d..222b846b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Treat Python 2 old-style classes like types when validating. - [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): + Modified __lt__ in Marker class to allow comparison with non Marker objects, such as str and int ## [0.10.5] diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 88a0203e..9f3a0221 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -953,7 +953,9 @@ def __repr__(self): return repr(self.schema) def __lt__(self, other): - return self.schema < other.schema + if isinstance(other, Marker): + return self.schema < other.schema + return self.schema < other def __hash__(self): return hash(self.schema) diff --git a/voluptuous/tests/tests.md b/voluptuous/tests/tests.md index 18f6fbaf..5ba97ab6 100644 --- a/voluptuous/tests/tests.md +++ b/voluptuous/tests/tests.md @@ -266,3 +266,8 @@ Ensure that subclasses of Invalid of are raised as is. ... 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 diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 2977f354..816958a7 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1137,3 +1137,7 @@ def test_self_all(): 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_true(Optional('Classification') < 'Name') From e6b57f429d42890dbd3d3eab4bf5f4c416f688fc Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Wed, 14 Feb 2018 21:32:29 +0000 Subject: [PATCH 138/261] Updated changelog to include changes merged after 0.10.5 release --- CHANGELOG.md | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 222b846b..5e04e455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,34 @@ ## [Unreleased] -- [#280](https://github.com/alecthomas/voluptuous/pull/280): Making - `IsDir()`, `IsFile()` and `PathExists()` consistent between different python versions. +**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. -- [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): - Modified __lt__ in Marker class to allow comparison with non Marker objects, such as str and int +- [#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] From 47fdfe94576dfe711ee7003359ea5b5ab6839e83 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 15 Feb 2018 20:11:24 +1100 Subject: [PATCH 139/261] 0.11.1 --- CHANGELOG.md | 2 +- setup.py | 2 +- voluptuous/__init__.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e04e455..90d644f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.11.0] **Changes**: diff --git a/setup.py b/setup.py index 25d6cf52..a97b90d0 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ try: import pypandoc - long_description = pypandoc.convert('README.md', 'rst') + long_description = pypandoc.convert('README.md', 'rst').encode('utf-8') with open('README.rst', 'w') as f: f.write(long_description) atexit.register(lambda: os.unlink('README.rst')) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index a12f11c5..3dfb0788 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.10.5' -__author__ = 'tusharmakkar08' +__version__ = '0.11.1' +__author__ = 'alecthomas' From 4c89902700a8716a180d46895eebadfee7ce8dde Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 16 Feb 2018 21:10:27 +0000 Subject: [PATCH 140/261] Use direct link to changelog to make it work on PyPI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6521b062..8ecf45a9 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The documentation is provided [here](http://alecthomas.github.io/voluptuous/). ## Changelog -See [CHANGELOG.md](CHANGELOG.md). +See [CHANGELOG.md](https://github.com/alecthomas/voluptuous/blob/master/CHANGELOG.md). ## Show me an example From 182195346361e9c4342864fe20d6a23b9469b703 Mon Sep 17 00:00:00 2001 From: Felix Yan Date: Thu, 1 Mar 2018 18:24:16 +0800 Subject: [PATCH 141/261] Fix Python 3.x compatibility for setup.py (#332) Without the patch, setup.py fails to run under Python 3.x with pypandoc installed: ``` Traceback (most recent call last): File "setup.py", line 16, in f.write(long_description) TypeError: write() argument must be str, not bytes ``` --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index a97b90d0..0f8fba8b 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,9 @@ try: import pypandoc - long_description = pypandoc.convert('README.md', 'rst').encode('utf-8') - with open('README.rst', 'w') as f: - f.write(long_description) + long_description = pypandoc.convert('README.md', 'rst') + with open('README.rst', 'wb') as f: + f.write(long_description.encode('utf-8')) atexit.register(lambda: os.unlink('README.rst')) except (ImportError, OSError): print('WARNING: Could not locate pandoc, using Markdown long_description.') From 44f5ace320a0e1f38307e6953e29d6bc01ab724d Mon Sep 17 00:00:00 2001 From: thatneat Date: Fri, 22 Jun 2018 13:04:27 -0700 Subject: [PATCH 142/261] Suggested practice for multi-field validation closes #124 --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 8ecf45a9..0ab31ff6 100644 --- a/README.md +++ b/README.md @@ -584,6 +584,48 @@ to the second element in the schema, and succeed: ``` +## 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 + +s=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 +s({'password':'123', 'password_again':'123'}) + +# raises MultipleInvalid: passwords must match +s({'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: + +``` +# raises Invalid because password_again is not a string +# passwords_must_match() will not run because first-pass validation already failed +s({'password':'123', 'password_again': 1337}) +``` + ## Running tests. Voluptuous is using nosetests: From 72411cdf3636c8484fddd5c0ec5126b5eef7005a Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Sun, 24 Jun 2018 04:55:23 +0100 Subject: [PATCH 143/261] Drop support for Python 3.3 (#343) Python 3.3 has reached end-of-life: https://devguide.python.org/#status-of-python-branches (2017-09-29) --- .travis.yml | 1 - setup.py | 1 - tox.ini | 5 +---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65830711..74d12fc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - '2.7' -- '3.3' - '3.4' - '3.5' - '3.6' diff --git a/setup.py b/setup.py index 0f8fba8b..f435962d 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', diff --git a/tox.ini b/tox.ini index 00015687..20c543a4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py33,py34,py35,py36 +envlist = flake8,py27,py34,py35,py36 [flake8] ; E501: line too long (X > 79 characters) @@ -27,9 +27,6 @@ commands = flake8 --doctests setup.py voluptuous [testenv:py27] basepython = python2.7 -[testenv:py33] -basepython = python3.3 - [testenv:py34] basepython = python3.4 From 4b5cb5bed29cd47b5e1968efc3cd84f7697cf12d Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Sun, 24 Jun 2018 17:34:59 +0100 Subject: [PATCH 144/261] Added support for sets and frozensets (#342) --- README.md | 53 +++++++++++++++++++++++++++ voluptuous/schema_builder.py | 42 ++++++++++++++++++++++ voluptuous/tests/tests.py | 70 ++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) diff --git a/README.md b/README.md index 0ab31ff6..46e2288f 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,59 @@ True ``` +### 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 diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 9f3a0221..fa29b348 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -286,6 +286,8 @@ def _compile(self, schema): 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 @@ -675,6 +677,46 @@ def _compile_list(self, schema): """ 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, required=None, extra=None): """Create a new `Schema` by merging this and the provided `schema`. diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 816958a7..2b1ab925 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1141,3 +1141,73 @@ def test_SomeOf_on_bounds_assertion(): def test_comparing_voluptuous_object_to_str(): assert_true(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])) + try: + schema(set(['abc'])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in set") + else: + assert False, "Did not raise Invalid" + + +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])) + try: + schema(frozenset(['abc'])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in frozenset") + else: + assert False, "Did not raise Invalid" + + +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'])) + try: + schema(set([None])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in set") + else: + assert False, "Did not raise Invalid" + + +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'])) + try: + schema(frozenset([None])) + except MultipleInvalid as e: + assert_equal(str(e), "invalid value in frozenset") + else: + assert False, "Did not raise Invalid" From bc429b7168b8ba56c032a097326d2913cd89dd6b Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 5 Jul 2018 17:39:58 +0200 Subject: [PATCH 145/261] Add support for Python 3.7 Also removes support for 3.4 and 3.5 since they are not available on Travis anymore. --- .travis.yml | 7 ++++--- setup.py | 3 +-- tox.ini | 5 ++++- voluptuous/schema_builder.py | 1 - 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74d12fc4..90f17dec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,11 @@ language: python +sudo: true +dist: xenial python: - '2.7' -- '3.4' -- '3.5' - '3.6' -- pypy +- '3.7' +- pypy3.5 install: - pip install coveralls - pip install 'coverage<4' diff --git a/setup.py b/setup.py index f435962d..41db631c 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,7 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ] ) diff --git a/tox.ini b/tox.ini index 20c543a4..7722ed88 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py34,py35,py36 +envlist = flake8,py27,py34,py35,py36,py37 [flake8] ; E501: line too long (X > 79 characters) @@ -35,3 +35,6 @@ basepython = python3.5 [testenv:py36] basepython = python3.6 + +[testenv:py37] +basepython = python3.7 diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index fa29b348..8d7a81a3 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -898,7 +898,6 @@ def _iterate_object(obj): for key in slots: if key != '__dict__': yield (key, getattr(obj, key)) - raise StopIteration() class Msg(object): From 6cd3b1deb6a30a5023ddfecf0aaec3558a7eff1d Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Thu, 5 Jul 2018 17:37:31 +0200 Subject: [PATCH 146/261] Include path in AnyInvalid errors Fixes #347 --- voluptuous/tests/tests.py | 47 +++++++++++++++++++++++++++++++++++++++ voluptuous/validators.py | 8 ++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 2b1ab925..3d195ac8 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1085,6 +1085,53 @@ def test_self_validation(): 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') + }) + try: + s({'q': 'str', 'q2': 'tata'}) + except MultipleInvalid as exc: + assert ( + (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) or + (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) + ) + else: + assert False, "Did not raise AnyInvalid" + + +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'), + }) + try: + s({'q': 'str', 'q2': 12}) + except MultipleInvalid as exc: + assert ( + (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) or + (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) + ) + else: + assert False, "Did not raise AllInvalid" + + +def test_match_error_has_path(): + """https://github.com/alecthomas/voluptuous/issues/347""" + s = Schema({ + Required('q2'): Match("a"), + }) + try: + s({'q2': 12}) + except MultipleInvalid as exc: + assert exc.errors[0].path == ['q2'] + else: + assert False, "Did not raise MatchInvalid" + + def test_self_any(): schema = Schema({"number": int, "follow": Any(Self, "stop")}) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index e39461c1..687d83bb 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -252,8 +252,10 @@ def _exec(self, funcs, v, path=None): error = e else: if error: - raise error if self.msg is None else AnyInvalid(self.msg) - raise AnyInvalid(self.msg or 'no valid value found') + 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 @@ -281,7 +283,7 @@ def _exec(self, funcs, v, path=None): else: v = func(path, v) except Invalid as e: - raise e if self.msg is None else AllInvalid(self.msg) + raise e if self.msg is None else AllInvalid(self.msg, path=path) return v From 7dbbd05bbdffd15354806f5957448946d629d6e2 Mon Sep 17 00:00:00 2001 From: Riccardo Cirimelli Date: Wed, 18 Jul 2018 14:40:34 +0200 Subject: [PATCH 147/261] Fix date validator --- voluptuous/tests/tests.py | 7 ++++++- voluptuous/validators.py | 5 ----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 3d195ac8..fa44fbf7 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -861,10 +861,15 @@ def test_datetime(): def test_date(): schema = Schema({"date": Date()}) schema({"date": "2016-10-24"}) - assert_raises(MultipleInvalid, schema, {"date": "2016-10-2"}) assert_raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) +def test_date_custom_format(): + schema = Schema({"date": Date("%Y%m%d")}) + schema({"date": "20161024"}) + assert_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 diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 687d83bb..d5e3ed59 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -636,15 +636,10 @@ class Date(Datetime): """Validate that the value matches the date format.""" DEFAULT_FORMAT = '%Y-%m-%d' - FORMAT_DESCRIPTION = 'yyyy-mm-dd' def __call__(self, v): try: datetime.datetime.strptime(v, self.format) - if len(v) != len(self.FORMAT_DESCRIPTION): - raise DateInvalid( - self.msg or 'value has invalid length' - ' expected length %d (%s)' % (len(self.FORMAT_DESCRIPTION), self.FORMAT_DESCRIPTION)) except (TypeError, ValueError): raise DateInvalid( self.msg or 'value does not match' From a66c6c40e12b9498c8a4f5793bb65d83a9f45858 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Sat, 28 Jul 2018 15:34:16 +0100 Subject: [PATCH 148/261] Updated changelog with 0.11.3 release --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d644f3..fd1c4dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog -## [0.11.0] +## [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**: From e9589099b6a138c03999da3cf71782da38736371 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Wed, 1 Aug 2018 13:19:14 +0100 Subject: [PATCH 149/261] Updated changelog with 0.11.5 release --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd1c4dff..837071dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [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**: From 55025ddd750b8b68546bbc4eaf8a7f41315183a6 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 27 Jul 2018 18:46:20 +1000 Subject: [PATCH 150/261] 0.11.2 --- voluptuous/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 3dfb0788..4c052c62 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.1' +__version__ = '0.11.2' __author__ = 'alecthomas' From 82a5853d45ba33f45bf33681b146e62f2e050256 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 27 Jul 2018 19:01:31 +1000 Subject: [PATCH 151/261] 0.11.3 --- setup.py | 15 +++++---------- voluptuous/__init__.py | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 41db631c..52e00798 100644 --- a/setup.py +++ b/setup.py @@ -9,16 +9,11 @@ sys.path.insert(0, '.') version = __import__('voluptuous').__version__ -try: - import pypandoc - long_description = pypandoc.convert('README.md', 'rst') - with open('README.rst', 'wb') as f: - f.write(long_description.encode('utf-8')) - atexit.register(lambda: os.unlink('README.rst')) -except (ImportError, OSError): - print('WARNING: Could not locate pandoc, using Markdown long_description.') - with open('README.md') as f: - long_description = f.read() +import pypandoc +long_description = pypandoc.convert('README.md', 'rst') +with open('README.rst', 'wb') as f: + f.write(long_description.encode('utf-8')) +atexit.register(lambda: os.unlink('README.rst')) description = long_description.splitlines()[0].strip() diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 4c052c62..80f5bf7b 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.2' +__version__ = '0.11.3' __author__ = 'alecthomas' From c32bf6a2c9c6e1b006129777595e733be28843c4 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 1 Aug 2018 09:25:34 +1000 Subject: [PATCH 152/261] Use long_description_content_type='text/markdown' yay! --- setup.py | 15 +++++---------- voluptuous/__init__.py | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 52e00798..55954c9f 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,4 @@ -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +from setuptools import setup import sys import os @@ -9,13 +6,10 @@ sys.path.insert(0, '.') version = __import__('voluptuous').__version__ -import pypandoc -long_description = pypandoc.convert('README.md', 'rst') -with open('README.rst', 'wb') as f: - f.write(long_description.encode('utf-8')) -atexit.register(lambda: os.unlink('README.rst')) -description = long_description.splitlines()[0].strip() +with open('README.md', encoding='utf-8') as f: + long_description = f.read() + description = long_description.splitlines()[0].strip() setup( @@ -25,6 +19,7 @@ version=version, description=description, long_description=long_description, + long_description_content_type='text/markdown', license='BSD', platforms=['any'], packages=['voluptuous'], diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 80f5bf7b..22b39121 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.3' +__version__ = '0.11.4' __author__ = 'alecthomas' From e72fd3bfb24f89888f51825313de98c397641674 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 1 Aug 2018 13:14:20 +1000 Subject: [PATCH 153/261] Fix open bug. Python is annoying. --- setup.py | 3 ++- voluptuous/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 55954c9f..6408f8ba 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,14 @@ from setuptools import setup import sys +import io import os import atexit sys.path.insert(0, '.') version = __import__('voluptuous').__version__ -with open('README.md', encoding='utf-8') as f: +with io.open('README.md', encoding='utf-8') as f: long_description = f.read() description = long_description.splitlines()[0].strip() diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 22b39121..10236d5a 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.4' +__version__ = '0.11.5' __author__ = 'alecthomas' From 360d50a8d55761960d24a92af1059a091dc2301d Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Thu, 4 Oct 2018 12:21:51 +0100 Subject: [PATCH 154/261] Updated Inclusive to accept description as well This makes the implementation consistent with Exclusive which is also a subclass of Optional and which also accepts description --- voluptuous/schema_builder.py | 5 +++-- voluptuous/tests/tests.py | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8d7a81a3..8a188218 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1121,8 +1121,9 @@ class Inclusive(Optional): True """ - def __init__(self, schema, group_of_inclusion, msg=None): - super(Inclusive, self).__init__(schema, msg=msg) + def __init__(self, schema, group_of_inclusion, msg=None, description=None): + super(Inclusive, self).__init__(schema, msg=msg, + description=description) self.group_of_inclusion = group_of_inclusion diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index fa44fbf7..6464d378 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,8 +6,8 @@ from nose.tools import assert_equal, assert_false, assert_raises, assert_true from voluptuous import ( - Schema, Required, Exclusive, Optional, Extra, Invalid, In, Remove, Literal, - Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, + Schema, Required, Exclusive, Inclusive, Optional, Extra, Invalid, In, Remove, + Literal, Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, @@ -1035,6 +1035,9 @@ def test_description(): 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' From e6ef8afbbca2f3e777df4ef5eef528709ab697ed Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 14 Dec 2018 19:24:31 +0000 Subject: [PATCH 155/261] Fix DeprecationWarning related to collections.Mapping This warning got reported in https://github.com/alecthomas/voluptuous/issues/367 --- voluptuous/schema_builder.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 8d7a81a3..685a90f3 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -22,6 +22,11 @@ def iteritems(d): def iteritems(d): return d.iteritems() +if sys.version_info >= (3, 3): + _Mapping = collections.abc.Mapping +else: + _Mapping = collections.Mapping + """Schema validation for Python data structures. Given eg. a nested data structure like this: @@ -280,7 +285,7 @@ def _compile(self, schema): return schema.__voluptuous_compile__(self) if isinstance(schema, Object): return self._compile_object(schema) - if isinstance(schema, collections.Mapping): + if isinstance(schema, _Mapping): return self._compile_dict(schema) elif isinstance(schema, list): return self._compile_list(schema) From 43ea94b161cf709a3367be07ed5c27888344986d Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 14 Dec 2018 20:52:32 +0000 Subject: [PATCH 156/261] Allow msg to be specified for Maybe This makes it possible to supply a good error message to the user. This got reported in https://github.com/alecthomas/voluptuous/issues/369 --- voluptuous/tests/tests.py | 6 ++++++ voluptuous/validators.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index fa44fbf7..06778821 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -624,6 +624,12 @@ def test_maybe(): assert_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_empty_list_as_exact(): s = Schema([]) assert_raises(Invalid, s, [1]) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index d5e3ed59..8c6a86d5 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -487,7 +487,7 @@ def PathExists(v): raise PathInvalid("Not a Path") -def Maybe(validator): +def Maybe(validator, msg=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 @@ -500,7 +500,7 @@ def Maybe(validator): ... s("string") """ - return Any(None, validator) + return Any(None, validator, msg=msg) class Range(object): From 54c7f343ec9132b5e760da445c0e3597524a0fcc Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 14 Dec 2018 19:32:01 +0000 Subject: [PATCH 157/261] Ignore .pytest_cache in .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4a7bc002..425e98ce 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ venv docs/_build .vscode/ .venv +.pytest_cache From c2e1cfd3e1d01f07dfcc8b4ffeec3e06e04a4fa6 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Sun, 23 Dec 2018 21:47:41 +0100 Subject: [PATCH 158/261] Added htmlcov directory to .gitignore (#375) This directory should be ignored in case pytest-cov is used. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 425e98ce..331b5d84 100755 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ docs/_build .vscode/ .venv .pytest_cache +htmlcov From e11b20ac3973523773381b7292746719c7cbd10b Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 28 Dec 2018 21:00:00 +0000 Subject: [PATCH 159/261] Allow extend to return a subclass of Schema This updates the extend method to return the same type in case a subclass of Schema has been constructed. This is a change related to issue #370. --- voluptuous/schema_builder.py | 3 ++- voluptuous/tests/tests.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index dd1b6538..c98f20dd 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -776,9 +776,10 @@ def key_literal(key): 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 Schema(result, required=result_required, extra=result_extra) + return result_cls(result, required=result_required, extra=result_extra) def _compile_scalar(schema): diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index f847bc98..779d0c46 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -372,6 +372,7 @@ def test_schema_extend(): 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(): @@ -411,6 +412,20 @@ def test_subschema_extension(): assert_equal(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_equal(Schema('foo'), Schema('foo')) From 2f8412a3d8cfd9ce8df5d46066415d8de2388949 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Fri, 28 Dec 2018 15:48:15 +0000 Subject: [PATCH 160/261] Preserve Unicode strings when passed to utility functions This updates the string utility functions to return a Unicode string when a Unicode string is passed in. In Python 2 this would raise an error due to the use of str(v). This is a change related to issue #374. The tests also check for other types (int) to preserve existing functionality. --- voluptuous/tests/tests.py | 37 ++++++++++++++++++++++++++++++++++++- voluptuous/util.py | 18 +++++++++++++----- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index f847bc98..0335892a 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -13,7 +13,7 @@ Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, raises) from voluptuous.humanize import humanize_error -from voluptuous.util import u +from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper def test_exact_sequence(): @@ -1272,3 +1272,38 @@ def test_frozenset_of_integers_and_strings(): assert_equal(str(e), "invalid value in frozenset") else: assert False, "Did not raise Invalid" + + +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" diff --git a/voluptuous/util.py b/voluptuous/util.py index 434c360c..e0ff43f8 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -7,6 +7,14 @@ __author__ = 'tusharmakkar08' +def _fix_str(v): + if sys.version_info[0] == 2 and isinstance(v, unicode): + s = v + else: + s = str(v) + return s + + def Lower(v): """Transform a string to lower case. @@ -14,7 +22,7 @@ def Lower(v): >>> s('HI') 'hi' """ - return str(v).lower() + return _fix_str(v).lower() def Upper(v): @@ -24,7 +32,7 @@ def Upper(v): >>> s('hi') 'HI' """ - return str(v).upper() + return _fix_str(v).upper() def Capitalize(v): @@ -34,7 +42,7 @@ def Capitalize(v): >>> s('hello world') 'Hello world' """ - return str(v).capitalize() + return _fix_str(v).capitalize() def Title(v): @@ -44,7 +52,7 @@ def Title(v): >>> s('hello world') 'Hello World' """ - return str(v).title() + return _fix_str(v).title() def Strip(v): @@ -54,7 +62,7 @@ def Strip(v): >>> s(' hello world ') 'hello world' """ - return str(v).strip() + return _fix_str(v).strip() class DefaultTo(object): From 7b4efb403b6bed94e306d58d487d2a7e21194af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Cabessa?= Date: Wed, 9 Jan 2019 11:06:56 +0100 Subject: [PATCH 161/261] Fix #379: Regression with Any and required In 0.9.3, the `Schema`s associated to the `Any` validators where setup with the `required` argument given to `Any` https://github.com/alecthomas/voluptuous/blob/0.9.3/voluptuous/validators.py#L218 This allow us to write something like: ``` Schema(Any({'a': int}, {'b': str}, required=True)) ``` In recent version, the `required` keyword is ignored. This patch pass the required argument to the `Schema`s associated with the `SubValidator`. --- voluptuous/tests/tests.py | 27 +++++++++++++++++++++++++++ voluptuous/validators.py | 9 +++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index bc16a122..f2db8da7 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1322,3 +1322,30 @@ def test_strip_util_handles_various_inputs(): 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)) + + try: + schema({}) + except MultipleInvalid as e: + assert_equal(str(e), + "required key not provided @ data['a']") + else: + assert False, "Did not raise Invalid for MultipleInvalid" + + +def test_any_required_with_subschema(): + schema = Schema(Any({'a': Any(float, int)}, + {'b': int}, + {'c': {'aa': int}}, + required=True)) + + try: + schema({}) + except MultipleInvalid as e: + assert_equal(str(e), + "required key not provided @ data['a']") + else: + assert False, "Did not raise Invalid for MultipleInvalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 8c6a86d5..81566b9e 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -192,12 +192,13 @@ class _WithSubValidators(object): def __init__(self, *validators, **kwargs): self.validators = validators self.msg = kwargs.pop('msg', None) + self.required = kwargs.pop('required', False) def __voluptuous_compile__(self, schema): - self._compiled = [ - schema._compile(v) - for v in self.validators - ] + self._compiled = [] + for v in self.validators: + schema.required = self.required + self._compiled.append(schema._compile(v)) return self._run def _run(self, path, value): From b3f44e9ecab8933077f8f0043d8b2739e90f7035 Mon Sep 17 00:00:00 2001 From: David Beitey Date: Sat, 16 Feb 2019 01:19:12 +0000 Subject: [PATCH 162/261] Minor grammar fix in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46e2288f..fb6c1396 100644 --- a/README.md +++ b/README.md @@ -189,9 +189,9 @@ True ``` -### URL's +### URLs -URL's in the schema are matched by using `urlparse` library. +URLs in the schema are matched by using `urlparse` library. ```pycon >>> from voluptuous import Url From 50c1243c886e1eed5fb52d37b9eba795c58b0b7d Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sun, 10 Feb 2019 09:23:25 -0700 Subject: [PATCH 163/261] Added support for default values in Inclusive schemas --- voluptuous/schema_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index c98f20dd..4bbe70fd 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1127,8 +1127,10 @@ class Inclusive(Optional): True """ - def __init__(self, schema, group_of_inclusion, msg=None, description=None): + def __init__(self, schema, group_of_inclusion, + msg=None, default=UNDEFINED, description=None): super(Inclusive, self).__init__(schema, msg=msg, + default=default, description=description) self.group_of_inclusion = group_of_inclusion From fb8427af9a3a116c3772bfebc5416dc980a6e9fd Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Tue, 12 Feb 2019 10:50:25 -0700 Subject: [PATCH 164/261] Added new test cases for Inclusive and Exclusive schema entries. --- voluptuous/tests/tests.py | 57 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index f2db8da7..1c144eb5 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1349,3 +1349,60 @@ def test_any_required_with_subschema(): "required key not provided @ data['a']") else: assert False, "Did not raise Invalid for MultipleInvalid" + +def test_inclusive(): + schema = Schema({ + Inclusive('x', 'stuff'): int, + Inclusive('y', 'stuff'): int, + }) + + r = schema({}) + assert_equal(r, {}) + + r = schema({'x':1, 'y':2}) + assert_equal(r, {'x':1, 'y':2}) + + try: + r = schema({'x':1}) + except MultipleInvalid as e: + assert_equal(str(e), + "some but not all values in the same group of inclusion 'stuff' @ data[]") + else: + assert False, "Did not raise Invalid for incomplete Inclusive group" + +def test_inclusive_defaults(): + schema = Schema({ + Inclusive('x', 'stuff', default=3): int, + Inclusive('y', 'stuff', default=4): int, + }) + + r = schema({}) + assert_equal(r, {'x':3, 'y':4}) + + try: + r = schema({'x':1}) + except MultipleInvalid as e: + assert_equal(str(e), + "some but not all values in the same group of inclusion 'stuff' @ data[]") + else: + assert False, "Did not raise Invalid for incomplete Inclusive group with defaults" + +def test_exclusive(): + schema = Schema({ + Exclusive('x', 'stuff'): int, + Exclusive('y', 'stuff'): int, + }) + + r = schema({}) + assert_equal(r, {}) + + r = schema({'x':1}) + assert_equal(r, {'x':1}) + + try: + r = schema({'x':1, 'y': 2}) + except MultipleInvalid as e: + assert_equal(str(e), + "two or more values in the same group of exclusion 'stuff' @ data[]") + else: + assert False, "Did not raise Invalid for multiple values in Exclusive group" From bb4e4bfcaff21e2370e7a2fc74a916ed4c19e72a Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 16 Feb 2019 07:17:32 -0700 Subject: [PATCH 165/261] Moved additional parameter to the end of the arguments list. --- voluptuous/schema_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 4bbe70fd..89745b6c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1128,7 +1128,7 @@ class Inclusive(Optional): """ def __init__(self, schema, group_of_inclusion, - msg=None, default=UNDEFINED, description=None): + msg=None, description=None, default=UNDEFINED): super(Inclusive, self).__init__(schema, msg=msg, default=default, description=description) From 36c8c11e2b7eb402c24866fa558473661ede9403 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Tue, 12 Mar 2019 02:07:13 +0300 Subject: [PATCH 166/261] Updates README.md with svg badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fb6c1396..7955ecd2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Voluptuous is a Python data validation library -[![Build Status](https://travis-ci.org/alecthomas/voluptuous.png)](https://travis-ci.org/alecthomas/voluptuous) -[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) [![Gitter chat](https://badges.gitter.im/alecthomas.png)](https://gitter.im/alecthomas/Lobby) +[![Build Status](https://travis-ci.org/alecthomas/voluptuous.svg)](https://travis-ci.org/alecthomas/voluptuous) +[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) [![Gitter chat](https://badges.gitter.im/alecthomas.svg)](https://gitter.im/alecthomas/Lobby) Voluptuous, *despite* the name, is a Python data validation library. It is primarily intended for validating data coming into Python as JSON, From 953cab62aab68c5f1e3aafda3d8b37ae9c027b73 Mon Sep 17 00:00:00 2001 From: Andre Nakkurt Date: Tue, 18 Jun 2019 15:30:47 -0700 Subject: [PATCH 167/261] Fix datetime namespace typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7955ecd2..724d5383 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ validator: ```pycon >>> schema = Schema(Date()) >>> schema('2013-03-03') -datetime.datetime(2013, 3, 3, 0, 0) +datetime(2013, 3, 3, 0, 0) >>> try: ... schema('2013-03') ... raise AssertionError('MultipleInvalid not raised') From 2e557f71db6260e3ab40a6848a6bf4705d434f2d Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 19 Jun 2019 09:09:00 +1000 Subject: [PATCH 168/261] Revert "Fix datetime namespace typo" This reverts commit 953cab62aab68c5f1e3aafda3d8b37ae9c027b73. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 724d5383..7955ecd2 100644 --- a/README.md +++ b/README.md @@ -322,7 +322,7 @@ validator: ```pycon >>> schema = Schema(Date()) >>> schema('2013-03-03') -datetime(2013, 3, 3, 0, 0) +datetime.datetime(2013, 3, 3, 0, 0) >>> try: ... schema('2013-03') ... raise AssertionError('MultipleInvalid not raised') From fe04e896d0b8ca3345d63b685794972807870513 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 13 Aug 2019 12:11:23 +1000 Subject: [PATCH 169/261] 0.11.6 release. --- voluptuous/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 10236d5a..68e8e812 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.5' +__version__ = '0.11.6' __author__ = 'alecthomas' From ce8227f74b0c14d4f85e6f58f071066ebf619134 Mon Sep 17 00:00:00 2001 From: Simeon Visser Date: Wed, 10 Jul 2019 10:36:54 +0100 Subject: [PATCH 170/261] Updated changelog with latest changes since release of 0.11.5 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 837071dd..c27a4240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## Upcoming + +**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`. From 961f85fd809261cbe80ac037105a317db484cbe4 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 13 Aug 2019 12:17:59 +1000 Subject: [PATCH 171/261] 0.11.7 including changelog. --- CHANGELOG.md | 2 +- voluptuous/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c27a4240..77aeea99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Upcoming +## [0.11.7] **Changes** diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 68e8e812..fef10010 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.6' +__version__ = '0.11.7' __author__ = 'alecthomas' From f5e64dd7e26fb2de0ca4baec42c426c60cb061c2 Mon Sep 17 00:00:00 2001 From: Yashvardhan Didwania Date: Thu, 15 Aug 2019 18:30:24 +0530 Subject: [PATCH 172/261] Allow a discrminant field in validators (#368) --- voluptuous/tests/tests.py | 26 +++++++++++++++++++- voluptuous/validators.py | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 1c144eb5..3ff71e4a 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -11,7 +11,7 @@ Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, - raises) + raises, Union) from voluptuous.humanize import humanize_error from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper @@ -1406,3 +1406,27 @@ def test_exclusive(): "two or more values in the same group of exclusion 'stuff' @ data[]") else: assert False, "Did not raise Invalid for multiple values in Exclusive group" + +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)) + }) + try: + schema({ + 'implementation': { + 'type': 'C', + 'c-value': None,} + }) + except MultipleInvalid as e: + assert_equal(str(e),'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') + else: + assert False, "Did not raise correct Invalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 81566b9e..328e2bfa 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -193,15 +193,23 @@ def __init__(self, *validators, **kwargs): self.validators = validators self.msg = kwargs.pop('msg', None) self.required = kwargs.pop('required', False) + self.discriminant = kwargs.pop('discriminant', None) def __voluptuous_compile__(self, schema): self._compiled = [] + self.schema = schema for v in self.validators: schema.required = self.required self._compiled.append(schema._compile(v)) return self._run def _run(self, path, 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): @@ -262,6 +270,49 @@ def _exec(self, funcs, v, path=None): # Convenience alias Or = Any +class Union(_WithSubValidators): + """Use the first validated value among those selected by discrminant. + + :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. From 858ceee119643bca099077e5a3ccbcb176292d1c Mon Sep 17 00:00:00 2001 From: monopolis Date: Thu, 28 Nov 2019 01:13:16 +0100 Subject: [PATCH 173/261] Handle incomparable values in Range (#414) In Python3 some values that are comparable in Python2 are no longer comparable. One instance of this is None, which in Python2 is always less than any other object. In Python3, however, a TypeError is raised if it is used in a comparison. This commit handles said TypeError and issues a RangeInvalid exception instead. --- voluptuous/tests/tests.py | 13 +++++++++++++ voluptuous/validators.py | 41 +++++++++++++++++++++++---------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 3ff71e4a..acd6642a 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -583,6 +583,19 @@ def test_range_exlcudes_nan(): assert_raises(MultipleInvalid, s, float('nan')) +def test_range_excludes_none(): + s = Schema(Range(min=0, max=10)) + assert_raises(MultipleInvalid, s, None) + + +def test_range_excludes_unordered_object(): + class MyObject(object): + pass + + s = Schema(Range(min=0, max=10)) + assert_raises(MultipleInvalid, s, MyObject()) + + def test_equal(): s = Schema(Equal(1)) s(1) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 328e2bfa..0e0e1fc6 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -585,23 +585,30 @@ def __init__(self, min=None, max=None, min_included=True, self.msg = msg def __call__(self, v): - 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 + 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 will raise TypeError + except TypeError: + raise RangeInvalid( + self.msg or 'value must have a partial ordering') def __repr__(self): return ('Range(min=%r, max=%r, min_included=%r,' From 49160497ca0a4af901977609489be695382d37e0 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sat, 7 Dec 2019 08:29:39 +1100 Subject: [PATCH 174/261] Fix simple typo: arugment -> argument Closes #415 --- voluptuous/schema_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 89745b6c..4c232273 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -740,7 +740,7 @@ def extend(self, schema, required=None, extra=None): result = self.schema.copy() - # returns the key that may have been passed as arugment to Marker constructor + # returns the key that may have been passed as argument to Marker constructor def key_literal(key): return (key.schema if isinstance(key, Marker) else key) From 45ba818fe718d5d3de58885a54fd5a2e4664d835 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sat, 7 Dec 2019 08:35:47 +1100 Subject: [PATCH 175/261] Update schema_builder.py --- voluptuous/schema_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 4c232273..84c17381 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -740,7 +740,7 @@ def extend(self, schema, required=None, extra=None): result = self.schema.copy() - # returns the key that may have been passed as argument to Marker constructor + # 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) From 095cda05255a636a88b0c6583ebfe9511659f9df Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 21 Jan 2020 07:57:40 +1100 Subject: [PATCH 176/261] Switch to CONTRIBUTIONS ONLY --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 7955ecd2..e77856f4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ + +# CONTRIBUTIONS ONLY + +**What does this mean?** I do not have time to fix issues myself. The only way fixes or new features will be added is by people submitting PRs. + +**Current status.** Voluptuous is largely feature stable. There hasn't been a need to add new features in a while, but there are some bugs that should be fixed. + +**Why?** I no longer use Voluptuous personally (in fact I no longer regularly write Python code). Rather than leave the project in a limbo of people filing issues and wondering why they're note being worked on, I believe this notice will more clearly set expectations. + # Voluptuous is a Python data validation library [![Build Status](https://travis-ci.org/alecthomas/voluptuous.svg)](https://travis-ci.org/alecthomas/voluptuous) From 82de2ef4afe1f168fa16160f9da7c757aba0d6cc Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 21 Jan 2020 11:35:02 +1100 Subject: [PATCH 177/261] Update issue templates --- .github/ISSUE_TEMPLATE/contributions-only.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/contributions-only.md diff --git a/.github/ISSUE_TEMPLATE/contributions-only.md b/.github/ISSUE_TEMPLATE/contributions-only.md new file mode 100644 index 00000000..cca06c35 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/contributions-only.md @@ -0,0 +1,16 @@ +--- +name: Contributions only +about: Contributions only +title: "(Contributions only)" +labels: '' +assignees: '' + +--- + +# CONTRIBUTIONS ONLY + +**What does this mean?** I do not have time to fix issues myself. The only way fixes or new features will be added is by people submitting PRs. + +**Current status.** Voluptuous is largely feature stable. There hasn't been a need to add new features in a while, but there are some bugs that should be fixed. + +**Why?** I no longer use Voluptuous personally (in fact I no longer regularly write Python code). Rather than leave the project in a limbo of people filing issues and wondering why they're note being worked on, I believe this notice will more clearly set expectations. From 7fa045e72cd4019ae5c384deee9d708a8e398bae Mon Sep 17 00:00:00 2001 From: EarQuack <62698102+EarQuack@users.noreply.github.com> Date: Wed, 1 Apr 2020 07:09:36 +0100 Subject: [PATCH 178/261] Fixed issue with 'required' not being set properly and added test (#420) A fix for issue #419 - schema required flag isn't set properly in all cases --- voluptuous/tests/tests.py | 7 +++++++ voluptuous/validators.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index acd6642a..aaa260d5 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -16,6 +16,13 @@ from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper +def test_new_required_test(): + schema = Schema({ + 'my_key': All(int, Range(1, 20)), + }, required=True) + assert_true(schema.required) + + def test_exact_sequence(): schema = Schema(ExactSequence([int, int])) try: diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 0e0e1fc6..9b0e0309 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -197,10 +197,12 @@ def __init__(self, *validators, **kwargs): def __voluptuous_compile__(self, schema): 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, value): From 580e7305ae0de6d4ba22c4feee688df64bfbb877 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Mon, 24 Aug 2020 14:28:53 +1000 Subject: [PATCH 179/261] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e77856f4..5801ece7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Current status.** Voluptuous is largely feature stable. There hasn't been a need to add new features in a while, but there are some bugs that should be fixed. -**Why?** I no longer use Voluptuous personally (in fact I no longer regularly write Python code). Rather than leave the project in a limbo of people filing issues and wondering why they're note being worked on, I believe this notice will more clearly set expectations. +**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 From 9fa00090a489b3d0c28b513aa9d581a0f559005f Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 20 Sep 2020 10:57:09 +0200 Subject: [PATCH 180/261] Added additional tests for Range, Clamp and Length + catch exceptions (#427) --- voluptuous/tests/tests.py | 77 ++++++++++++++++++++++++++++++++++++++- voluptuous/validators.py | 40 +++++++++++++------- 2 files changed, 101 insertions(+), 16 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index aaa260d5..3bf52b45 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -11,7 +11,7 @@ Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, - raises, Union) + raises, Union, Clamp) from voluptuous.humanize import humanize_error from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper @@ -585,7 +585,23 @@ def test_fix_157(): assert_raises(MultipleInvalid, s, ['four']) -def test_range_exlcudes_nan(): +def test_range_inside(): + s = Schema(Range(min=0, max=10)) + assert_equal(5, s(5)) + + +def test_range_outside(): + s = Schema(Range(min=0, max=10)) + assert_raises(MultipleInvalid, s, 12) + assert_raises(MultipleInvalid, s, -1) + + +def test_range_no_upper_limit(): + s = Schema(Range(min=0)) + assert_equal(123, s(123)) + + +def test_range_excludes_nan(): s = Schema(Range(min=0, max=10)) assert_raises(MultipleInvalid, s, float('nan')) @@ -595,6 +611,11 @@ def test_range_excludes_none(): assert_raises(MultipleInvalid, s, None) +def test_range_excludes_string(): + s = Schema(Range(min=0, max=10)) + assert_raises(MultipleInvalid, s, "abc") + + def test_range_excludes_unordered_object(): class MyObject(object): pass @@ -603,6 +624,58 @@ class MyObject(object): assert_raises(MultipleInvalid, s, MyObject()) +def test_clamp_inside(): + s = Schema(Clamp(min=1, max=10)) + assert_equal(5, s(5)) + + +def test_clamp_above(): + s = Schema(Clamp(min=1, max=10)) + assert_equal(10, s(12)) + + +def test_clamp_below(): + s = Schema(Clamp(min=1, max=10)) + assert_equal(1, s(-3)) + + +def test_clamp_invalid(): + s = Schema(Clamp(min=1, max=10)) + if sys.version_info.major >= 3: + assert_raises(MultipleInvalid, s, None) + assert_raises(MultipleInvalid, s, "abc") + else: + assert_equal(1, s(None)) + + +def test_length_ok(): + v1 = ['a', 'b', 'c'] + s = Schema(Length(min=1, max=10)) + assert_equal(v1, s(v1)) + v2 = "abcde" + assert_equal(v2, s(v2)) + + +def test_length_too_short(): + v1 = [] + s = Schema(Length(min=1, max=10)) + assert_raises(MultipleInvalid, s, v1) + v2 = '' + assert_raises(MultipleInvalid, s, v2) + + +def test_length_too_long(): + v = ['a', 'b', 'c'] + s = Schema(Length(min=0, max=2)) + assert_raises(MultipleInvalid, s, v) + + +def test_length_invalid(): + v = None + s = Schema(Length(min=0, max=2)) + assert_raises(MultipleInvalid, s, v) + + def test_equal(): s = Schema(Equal(1)) s(1) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 9b0e0309..ce1567e7 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -607,10 +607,10 @@ def __call__(self, v): return v - # Objects that lack a partial ordering, e.g. None will raise TypeError + # Objects that lack a partial ordering, e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid( - self.msg or 'value must have a partial ordering') + self.msg or 'invalid value or type (must have a partial ordering)') def __repr__(self): return ('Range(min=%r, max=%r, min_included=%r,' @@ -640,11 +640,17 @@ def __init__(self, min=None, max=None, msg=None): self.msg = msg def __call__(self, v): - 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 + 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) @@ -659,13 +665,19 @@ def __init__(self, min=None, max=None, msg=None): self.msg = msg def __call__(self, v): - 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 + 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 havbe 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) From f8775a70cd10dc56775f74978840e4611900cff8 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 21 Sep 2020 12:24:21 +0200 Subject: [PATCH 181/261] Fix most important flake8 errors/warnings --- voluptuous/schema_builder.py | 2 +- voluptuous/tests/tests.py | 37 ++++++++++++++++++++---------------- voluptuous/validators.py | 5 +++-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 84c17381..64da5b2c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -815,7 +815,7 @@ def validate_instance(path, data): def validate_callable(path, data): try: return schema(data) - except ValueError as e: + except ValueError: raise er.ValueInvalid('not a valid value', path) except er.Invalid as e: e.prepend(path) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 3bf52b45..61729353 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -631,12 +631,12 @@ def test_clamp_inside(): def test_clamp_above(): s = Schema(Clamp(min=1, max=10)) - assert_equal(10, s(12)) + assert_equal(10, s(12)) def test_clamp_below(): s = Schema(Clamp(min=1, max=10)) - assert_equal(1, s(-3)) + assert_equal(1, s(-3)) def test_clamp_invalid(): @@ -667,13 +667,13 @@ def test_length_too_short(): def test_length_too_long(): v = ['a', 'b', 'c'] s = Schema(Length(min=0, max=2)) - assert_raises(MultipleInvalid, s, v) + assert_raises(MultipleInvalid, s, v) def test_length_invalid(): v = None s = Schema(Length(min=0, max=2)) - assert_raises(MultipleInvalid, s, v) + assert_raises(MultipleInvalid, s, v) def test_equal(): @@ -1443,6 +1443,7 @@ def test_any_required_with_subschema(): else: assert False, "Did not raise Invalid for MultipleInvalid" + def test_inclusive(): schema = Schema({ Inclusive('x', 'stuff'): int, @@ -1452,17 +1453,18 @@ def test_inclusive(): r = schema({}) assert_equal(r, {}) - r = schema({'x':1, 'y':2}) - assert_equal(r, {'x':1, 'y':2}) + r = schema({'x': 1, 'y': 2}) + assert_equal(r, {'x': 1, 'y': 2}) try: - r = schema({'x':1}) + r = schema({'x': 1}) except MultipleInvalid as e: assert_equal(str(e), "some but not all values in the same group of inclusion 'stuff' @ data[]") else: assert False, "Did not raise Invalid for incomplete Inclusive group" + def test_inclusive_defaults(): schema = Schema({ Inclusive('x', 'stuff', default=3): int, @@ -1470,16 +1472,17 @@ def test_inclusive_defaults(): }) r = schema({}) - assert_equal(r, {'x':3, 'y':4}) + assert_equal(r, {'x': 3, 'y': 4}) try: - r = schema({'x':1}) + r = schema({'x': 1}) except MultipleInvalid as e: assert_equal(str(e), "some but not all values in the same group of inclusion 'stuff' @ data[]") else: assert False, "Did not raise Invalid for incomplete Inclusive group with defaults" + def test_exclusive(): schema = Schema({ Exclusive('x', 'stuff'): int, @@ -1489,17 +1492,18 @@ def test_exclusive(): r = schema({}) assert_equal(r, {}) - r = schema({'x':1}) - assert_equal(r, {'x':1}) + r = schema({'x': 1}) + assert_equal(r, {'x': 1}) try: - r = schema({'x':1, 'y': 2}) + r = schema({'x': 1, 'y': 2}) except MultipleInvalid as e: assert_equal(str(e), "two or more values in the same group of exclusion 'stuff' @ data[]") else: assert False, "Did not raise Invalid for multiple values in Exclusive group" + def test_any_with_discriminant(): schema = Schema({ 'implementation': Union({ @@ -1511,15 +1515,16 @@ def test_any_with_discriminant(): }, { 'type': 'C', 'c-value': bool, - }, discriminant=lambda value, alternatives: filter(lambda v : v['type'] == value['type'], alternatives)) + }, discriminant=lambda value, alternatives: filter(lambda v: v['type'] == value['type'], alternatives)) }) try: schema({ 'implementation': { - 'type': 'C', - 'c-value': None,} + 'type': 'C', + 'c-value': None + } }) except MultipleInvalid as e: - assert_equal(str(e),'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') + assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') else: assert False, "Did not raise correct Invalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index ce1567e7..21a3a65f 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -272,6 +272,7 @@ def _exec(self, funcs, v, path=None): # Convenience alias Or = Any + class Union(_WithSubValidators): """Use the first validated value among those selected by discrminant. @@ -650,7 +651,7 @@ def __call__(self, v): # Objects that lack a partial ordering, e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid( - self.msg or 'invalid value or type (must have a partial ordering)') + self.msg or 'invalid value or type (must have a partial ordering)') def __repr__(self): return 'Clamp(min=%s, max=%s)' % (self.min, self.max) @@ -677,7 +678,7 @@ def __call__(self, v): # Objects that havbe no length e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid( - self.msg or 'invalid value or type') + self.msg or 'invalid value or type') def __repr__(self): return 'Length(min=%s, max=%s)' % (self.min, self.max) From 5f14afc580bc25cc2055aac279f63c98c47948fb Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 21 Sep 2020 14:07:31 +0200 Subject: [PATCH 182/261] Updated README to include shields from pypi --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5801ece7..00ec911d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ # Voluptuous is a Python data validation library +[![image](https://img.shields.io/pypi/v/voluptuous.svg)](https://python.org/pypi/voluptuous) +[![image](https://img.shields.io/pypi/l/voluptuous.svg)](https://python.org/pypi/voluptuous) +[![image](https://img.shields.io/pypi/pyversions/voluptuous.svg)](https://python.org/pypi/voluptuous) [![Build Status](https://travis-ci.org/alecthomas/voluptuous.svg)](https://travis-ci.org/alecthomas/voluptuous) [![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) [![Gitter chat](https://badges.gitter.im/alecthomas.svg)](https://gitter.im/alecthomas/Lobby) From d8d863d0bd8526c58a3e78506d17de8c9febcf30 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 21 Sep 2020 12:36:47 +0200 Subject: [PATCH 183/261] Cleanup python versions (remove unsupported + add 3.8) --- .travis.yml | 2 +- setup.py | 3 +-- tox.ini | 17 +---------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 90f17dec..51b64e08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ python: - '2.7' - '3.6' - '3.7' -- pypy3.5 +- '3.8' install: - pip install coveralls - pip install 'coverage<4' diff --git a/setup.py b/setup.py index 6408f8ba..fe6cb67b 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,6 @@ import sys import io -import os -import atexit sys.path.insert(0, '.') version = __import__('voluptuous').__version__ @@ -36,5 +34,6 @@ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ] ) diff --git a/tox.ini b/tox.ini index 7722ed88..adfe8932 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py34,py35,py36,py37 +envlist = flake8,py27,py36,py37,py38 [flake8] ; E501: line too long (X > 79 characters) @@ -23,18 +23,3 @@ commands = [testenv:flake8] deps = flake8 commands = flake8 --doctests setup.py voluptuous - -[testenv:py27] -basepython = python2.7 - -[testenv:py34] -basepython = python3.4 - -[testenv:py35] -basepython = python3.5 - -[testenv:py36] -basepython = python3.6 - -[testenv:py37] -basepython = python3.7 From da9687dfed27f5f8bfb8c2a76b737c3cc319fd85 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 21 Sep 2020 12:49:46 +0200 Subject: [PATCH 184/261] Newer coverage version required for python-3.8 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 51b64e08..12cee006 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ python: - '3.8' install: - pip install coveralls -- pip install 'coverage<4' +- pip install coverage script: nosetests --with-coverage --cover-package=voluptuous after_success: - coveralls From f1b04199e57b0ad986d53e2ff9be4a4d68c97eb5 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 21 Sep 2020 11:28:08 +0200 Subject: [PATCH 185/261] Update changelog for 0.12.0 --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77aeea99..30b5f87e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Changelog +## [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** +**Changes**: - [#378](https://github.com/alecthomas/voluptuous/pull/378): Allow `extend()` of a `Schema` to return a subclass of a `Schema` as well. From a62e5391a66804e67d7365d02fc06802bdaffb28 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Tue, 22 Sep 2020 10:52:13 +0200 Subject: [PATCH 186/261] Fixed typos + made spelling more consistent (#431) * Fixed typos + made spelling more consistent * Adjustments --- README.md | 48 +++++++++++++++++----------------- voluptuous/error.py | 4 +-- voluptuous/tests/tests.py | 50 ++++++++++++++++++------------------ voluptuous/validators.py | 54 +++++++++++++++++++-------------------- 4 files changed, 78 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 00ec911d..a6900586 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **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. +**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. @@ -44,6 +44,28 @@ The documentation is provided [here](http://alecthomas.github.io/voluptuous/). 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 @@ -691,35 +713,13 @@ cross-field validator will not run: s({'password':'123', 'password_again': 1337}) ``` -## Running tests. +## Running tests Voluptuous is using nosetests: $ nosetests -## Why use Voluptuous over another validation library? - -**Validators are simple callables** -: No need to subclass anything, just use a function. - -**Errors are simple exceptions.** -: A validator can just `raise Invalid(msg)` and expect the user to get -useful messages. - -**Schemas are basic Python data structures.** -: Should your data be a dictionary of integer keys to strings? -`{int: str}` does what you expect. List of integers, floats or -strings? `[int, float, str]`. - -**Designed from the ground up for validating more than just forms.** -: Nested data structures are treated in the same way as any other -type. Need a list of dictionaries? `[{}]` - -**Consistency.** -: Types in the schema are checked as types. Values are compared as -values. Callables are called to validate. Simple. - ## Other libraries and inspirations Voluptuous is heavily inspired by diff --git a/voluptuous/error.py b/voluptuous/error.py index 86c4e0a3..97f37d2c 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -142,11 +142,11 @@ class BooleanInvalid(Invalid): class UrlInvalid(Invalid): - """The value is not a url.""" + """The value is not a URL.""" class EmailInvalid(Invalid): - """The value is not a email.""" + """The value is not an email address.""" class FileInvalid(Invalid): diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 61729353..01ed91bf 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -197,7 +197,7 @@ class C2: def test_email_validation(): - """ test with valid email """ + """ test with valid email address """ schema = Schema({"email": Email()}) out_ = schema({"email": "example@example.com"}) @@ -205,43 +205,43 @@ def test_email_validation(): def test_email_validation_with_none(): - """ test with invalid None Email""" + """ test with invalid None email address """ schema = Schema({"email": Email()}) try: schema({"email": None}) except MultipleInvalid as e: assert_equal(str(e), - "expected an Email for dictionary value @ data['email']") + "expected an email address for dictionary value @ data['email']") else: - assert False, "Did not raise Invalid for None url" + assert False, "Did not raise Invalid for None URL" def test_email_validation_with_empty_string(): - """ test with empty string Email""" + """ test with empty string email address""" schema = Schema({"email": Email()}) try: schema({"email": ''}) except MultipleInvalid as e: assert_equal(str(e), - "expected an Email for dictionary value @ data['email']") + "expected an email address for dictionary value @ data['email']") else: - assert False, "Did not raise Invalid for empty string url" + assert False, "Did not raise Invalid for empty string URL" def test_email_validation_without_host(): - """ test with empty host name in email """ + """ test with empty host name in email address """ schema = Schema({"email": Email()}) try: schema({"email": 'a@.com'}) except MultipleInvalid as e: assert_equal(str(e), - "expected an Email for dictionary value @ data['email']") + "expected an email address for dictionary value @ data['email']") else: - assert False, "Did not raise Invalid for empty string url" + assert False, "Did not raise Invalid for empty string URL" def test_fqdn_url_validation(): - """ test with valid fully qualified domain name url """ + """ test with valid fully qualified domain name URL """ schema = Schema({"url": FqdnUrl()}) out_ = schema({"url": "http://example.com/"}) @@ -249,27 +249,27 @@ def test_fqdn_url_validation(): def test_fqdn_url_without_domain_name(): - """ test with invalid fully qualified domain name url """ + """ test with invalid fully qualified domain name URL """ schema = Schema({"url": FqdnUrl()}) try: schema({"url": "http://localhost/"}) except MultipleInvalid as e: assert_equal(str(e), - "expected a Fully qualified domain name URL for dictionary value @ data['url']") + "expected a fully qualified domain name URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for None url" + assert False, "Did not raise Invalid for None URL" def test_fqdnurl_validation_with_none(): - """ test with invalid None FQDN url""" + """ test with invalid None FQDN URL """ schema = Schema({"url": FqdnUrl()}) try: schema({"url": None}) except MultipleInvalid as e: assert_equal(str(e), - "expected a Fully qualified domain name URL for dictionary value @ data['url']") + "expected a fully qualified domain name URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for None url" + assert False, "Did not raise Invalid for None URL" def test_fqdnurl_validation_with_empty_string(): @@ -279,9 +279,9 @@ def test_fqdnurl_validation_with_empty_string(): schema({"url": ''}) except MultipleInvalid as e: assert_equal(str(e), - "expected a Fully qualified domain name URL for dictionary value @ data['url']") + "expected a fully qualified domain name URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for empty string url" + assert False, "Did not raise Invalid for empty string URL" def test_fqdnurl_validation_without_host(): @@ -291,9 +291,9 @@ def test_fqdnurl_validation_without_host(): schema({"url": 'http://'}) except MultipleInvalid as e: assert_equal(str(e), - "expected a Fully qualified domain name URL for dictionary value @ data['url']") + "expected a fully qualified domain name URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for empty string url" + assert False, "Did not raise Invalid for empty string URL" def test_url_validation(): @@ -305,7 +305,7 @@ def test_url_validation(): def test_url_validation_with_none(): - """ test with invalid None url""" + """ test with invalid None URL""" schema = Schema({"url": Url()}) try: schema({"url": None}) @@ -313,7 +313,7 @@ def test_url_validation_with_none(): assert_equal(str(e), "expected a URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for None url" + assert False, "Did not raise Invalid for None URL" def test_url_validation_with_empty_string(): @@ -325,7 +325,7 @@ def test_url_validation_with_empty_string(): assert_equal(str(e), "expected a URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for empty string url" + assert False, "Did not raise Invalid for empty string URL" def test_url_validation_without_host(): @@ -337,7 +337,7 @@ def test_url_validation_without_host(): assert_equal(str(e), "expected a URL for dictionary value @ data['url']") else: - assert False, "Did not raise Invalid for empty string url" + assert False, "Did not raise Invalid for empty string URL" def test_copy_dict_undefined(): diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 21a3a65f..2add5d1b 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -229,7 +229,7 @@ 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. + :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', @@ -274,11 +274,11 @@ def _exec(self, funcs, v, path=None): class Union(_WithSubValidators): - """Use the first validated value among those selected by discrminant. + """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. + :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'}, @@ -323,7 +323,7 @@ class All(_WithSubValidators): 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. + :param kwargs: All other keyword arguments are passed to the sub-schema constructors. >>> validate = Schema(All('10', Coerce(int))) >>> validate('10') @@ -358,7 +358,7 @@ class Match(object): >>> with raises(MultipleInvalid, 'expected string or buffer'): ... validate(123) - Pattern may also be a _compiled regular expression: + Pattern may also be a compiled regular expression: >>> validate = Schema(Match(re.compile(r'0x[A-F0-9]+', re.I))) >>> validate('0x123ef4') @@ -416,38 +416,38 @@ def _url_validation(v): return parsed -@message('expected an Email', cls=EmailInvalid) +@message('expected an email address', cls=EmailInvalid) def Email(v): - """Verify that the value is an Email or not. + """Verify that the value is an email address or not. >>> s = Schema(Email()) - >>> with raises(MultipleInvalid, 'expected an Email'): + >>> with raises(MultipleInvalid, 'expected an email address'): ... s("a.com") - >>> with raises(MultipleInvalid, 'expected an Email'): + >>> with raises(MultipleInvalid, 'expected an email address'): ... s("a@.com") - >>> with raises(MultipleInvalid, 'expected an Email'): + >>> 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") + 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") + raise EmailInvalid("Invalid email address") return v except: raise ValueError -@message('expected a Fully qualified domain name URL', cls=UrlInvalid) +@message('expected a fully qualified domain name URL', cls=UrlInvalid) def FqdnUrl(v): - """Verify that the value is a Fully qualified domain name URL. + """Verify that the value is a fully qualified domain name URL. >>> s = Schema(FqdnUrl()) - >>> with raises(MultipleInvalid, 'expected a Fully qualified domain name URL'): + >>> with raises(MultipleInvalid, 'expected a fully qualified domain name URL'): ... s("http://localhost/") >>> s('http://w3.org') 'http://w3.org' @@ -478,14 +478,14 @@ def Url(v): raise ValueError -@message('not a file', cls=FileInvalid) +@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'): + >>> with raises(FileInvalid, 'Not a file'): ... IsFile()("random_filename_goes_here.py") >>> with raises(FileInvalid, 'Not a file'): ... IsFile()(None) @@ -500,7 +500,7 @@ def IsFile(v): raise FileInvalid('Not a file') -@message('not a directory', cls=DirInvalid) +@message('Not a directory', cls=DirInvalid) @truth def IsDir(v): """Verify the directory exists. @@ -545,8 +545,8 @@ def PathExists(v): def Maybe(validator, msg=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 + :raises Invalid: If the value does not match the given validator and is not + None. >>> s = Schema(Maybe(int)) >>> s(10) @@ -796,7 +796,7 @@ class ExactSequence(object): the validators. :param msg: Message to deliver to user if validation fails. - :param kwargs: All other keyword arguments are passed to the sub-Schema + :param kwargs: All other keyword arguments are passed to the sub-schema constructors. >>> from voluptuous import Schema, ExactSequence @@ -961,7 +961,7 @@ def __repr__(self): class Number(object): """ Verify the number of digits that are present in the number(Precision), - and the decimal places(Scale) + and the decimal places(Scale). :raises Invalid: If the value does not match the provided Precision and Scale. @@ -1025,13 +1025,13 @@ class SomeOf(_WithSubValidators): The output of each validator is passed as input to the next. :param min_valid: Minimum number of valid schemas. - :param validators: a list of schemas or validators to match input against + :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. + :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 more validations than the given amount is met + :raises NotEnoughValid: If the minimum number of validations isn't met. + :raises TooManyValid: If the more validations than the given amount is met. >>> validate = Schema(SomeOf(min_valid=2, validators=[Range(1, 5), Any(float, int), 6.6])) >>> validate(6.6) From 6b0fdc3e178af7267d97827787669da16c19dd01 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 7 Oct 2020 19:13:30 +0200 Subject: [PATCH 187/261] Add Python 3.9 support (#433) --- .travis.yml | 1 + setup.py | 1 + tox.ini | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 12cee006..e6d6c6e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ python: - '3.6' - '3.7' - '3.8' +- '3.9' install: - pip install coveralls - pip install coverage diff --git a/setup.py b/setup.py index fe6cb67b..05791619 100644 --- a/setup.py +++ b/setup.py @@ -35,5 +35,6 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ] ) diff --git a/tox.ini b/tox.ini index adfe8932..8d4c8ff7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py36,py37,py38 +envlist = flake8,py27,py36,py37,py38,py39 [flake8] ; E501: line too long (X > 79 characters) From 7151438f2957debb04019400e6552965fd04c867 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 7 Oct 2020 20:05:42 +0200 Subject: [PATCH 188/261] Use "py3.9-dev" in travis-ci for now The python 3.9 binary from travis-ci is not yet available although Python 3.9 itself has been officially released. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e6d6c6e3..70bf0826 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - '3.6' - '3.7' - '3.8' -- '3.9' +- '3.9-dev' install: - pip install coveralls - pip install coverage From 67adedd627054e151101575c8c5a7d65f72f827a Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 18 Nov 2020 19:40:30 +0100 Subject: [PATCH 189/261] Extended a few tests (#435) --- voluptuous/tests/tests.py | 68 ++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 01ed91bf..9d784bd0 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -36,9 +36,8 @@ def test_exact_sequence(): def test_required(): """Verify that Required works.""" - schema = Schema({Required('q'): 1}) - # Can't use nose's raises (because we need to access the raised - # exception, nor assert_raises which fails with Python 2.6.9. + schema = Schema({Required('q'): int}) + schema({"q": 123}) try: schema({}) except Invalid as e: @@ -70,6 +69,12 @@ def test_in(): """Verify that In works.""" schema = Schema({"color": In(frozenset(["blue", "red", "yellow"]))}) schema({"color": "blue"}) + try: + schema({"color": "orange"}) + except Invalid as e: + assert_equal(str(e), "value is not allowed for dictionary value @ data['color']") + else: + assert False, "Did not raise InInvalid" def test_not_in(): @@ -138,7 +143,7 @@ def test_extra_empty_errors(): def test_literal(): - """ test with Literal """ + """ Test with Literal """ schema = Schema([Literal({"a": 1}), Literal({"b": 1})]) schema([{"a": 1}]) @@ -197,7 +202,7 @@ class C2: def test_email_validation(): - """ test with valid email address """ + """ Test with valid email address """ schema = Schema({"email": Email()}) out_ = schema({"email": "example@example.com"}) @@ -205,7 +210,7 @@ def test_email_validation(): def test_email_validation_with_none(): - """ test with invalid None email address """ + """ Test with invalid None email address """ schema = Schema({"email": Email()}) try: schema({"email": None}) @@ -217,7 +222,7 @@ def test_email_validation_with_none(): def test_email_validation_with_empty_string(): - """ test with empty string email address""" + """ Test with empty string email address""" schema = Schema({"email": Email()}) try: schema({"email": ''}) @@ -229,7 +234,7 @@ def test_email_validation_with_empty_string(): def test_email_validation_without_host(): - """ test with empty host name in email address """ + """ Test with empty host name in email address """ schema = Schema({"email": Email()}) try: schema({"email": 'a@.com'}) @@ -241,7 +246,7 @@ def test_email_validation_without_host(): def test_fqdn_url_validation(): - """ test with valid fully qualified domain name URL """ + """ Test with valid fully qualified domain name URL """ schema = Schema({"url": FqdnUrl()}) out_ = schema({"url": "http://example.com/"}) @@ -249,7 +254,7 @@ def test_fqdn_url_validation(): def test_fqdn_url_without_domain_name(): - """ test with invalid fully qualified domain name URL """ + """ Test with invalid fully qualified domain name URL """ schema = Schema({"url": FqdnUrl()}) try: schema({"url": "http://localhost/"}) @@ -261,7 +266,7 @@ def test_fqdn_url_without_domain_name(): def test_fqdnurl_validation_with_none(): - """ test with invalid None FQDN URL """ + """ Test with invalid None FQDN URL """ schema = Schema({"url": FqdnUrl()}) try: schema({"url": None}) @@ -273,7 +278,7 @@ def test_fqdnurl_validation_with_none(): def test_fqdnurl_validation_with_empty_string(): - """ test with empty string FQDN URL """ + """ Test with empty string FQDN URL """ schema = Schema({"url": FqdnUrl()}) try: schema({"url": ''}) @@ -285,7 +290,7 @@ def test_fqdnurl_validation_with_empty_string(): def test_fqdnurl_validation_without_host(): - """ test with empty host FQDN URL """ + """ Test with empty host FQDN URL """ schema = Schema({"url": FqdnUrl()}) try: schema({"url": 'http://'}) @@ -297,7 +302,7 @@ def test_fqdnurl_validation_without_host(): def test_url_validation(): - """ test with valid URL """ + """ Test with valid URL """ schema = Schema({"url": Url()}) out_ = schema({"url": "http://example.com/"}) @@ -305,7 +310,7 @@ def test_url_validation(): def test_url_validation_with_none(): - """ test with invalid None URL""" + """ Test with invalid None URL""" schema = Schema({"url": Url()}) try: schema({"url": None}) @@ -317,7 +322,7 @@ def test_url_validation_with_none(): def test_url_validation_with_empty_string(): - """ test with empty string URL """ + """ Test with empty string URL """ schema = Schema({"url": Url()}) try: schema({"url": ''}) @@ -329,7 +334,7 @@ def test_url_validation_with_empty_string(): def test_url_validation_without_host(): - """ test with empty host URL """ + """ Test with empty host URL """ schema = Schema({"url": Url()}) try: schema({"url": 'http://'}) @@ -341,7 +346,7 @@ def test_url_validation_without_host(): def test_copy_dict_undefined(): - """ test with a copied dictionary """ + """ Test with a copied dictionary """ fields = { Required("foo"): int } @@ -384,7 +389,6 @@ def test_schema_extend(): 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) @@ -396,7 +400,6 @@ def test_schema_extend_overrides(): 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) @@ -409,7 +412,6 @@ def test_schema_extend_key_swap(): 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) @@ -866,7 +868,7 @@ def test_unicode_as_key(): def test_number_validation_with_string(): - """ test with Number with string""" + """ Test with Number with string""" schema = Schema({"number": Number(precision=6, scale=2)}) try: schema({"number": 'teststr'}) @@ -878,7 +880,7 @@ def test_number_validation_with_string(): def test_number_validation_with_invalid_precision_invalid_scale(): - """ test with Number with invalid precision and scale""" + """ Test with Number with invalid precision and scale""" schema = Schema({"number": Number(precision=6, scale=2)}) try: schema({"number": '123456.712'}) @@ -890,35 +892,35 @@ def test_number_validation_with_invalid_precision_invalid_scale(): def test_number_validation_with_valid_precision_scale_yield_decimal_true(): - """ test with Number with valid precision and scale""" + """ Test with Number with valid precision and scale""" schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=True)}) out_ = schema({"number": '1234.00'}) assert_equal(float(out_.get("number")), 1234.00) def test_number_when_precision_scale_none_yield_decimal_true(): - """ test with Number with no precision and scale""" + """ Test with Number with no precision and scale""" schema = Schema({"number": Number(yield_decimal=True)}) out_ = schema({"number": '12345678901234'}) assert_equal(out_.get("number"), 12345678901234) def test_number_when_precision_none_n_valid_scale_case1_yield_decimal_true(): - """ test with Number with no precision and valid scale case 1""" + """ Test with Number with no precision and valid scale case 1""" schema = Schema({"number": Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789.34'}) assert_equal(float(out_.get("number")), 123456789.34) def test_number_when_precision_none_n_valid_scale_case2_yield_decimal_true(): - """ test with Number with no precision and valid scale case 2 with zero in decimal part""" + """ Test with Number with no precision and valid scale case 2 with zero in decimal part""" schema = Schema({"number": Number(scale=2, yield_decimal=True)}) out_ = schema({"number": '123456789012.00'}) assert_equal(float(out_.get("number")), 123456789012.00) def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): - """ test with Number with no precision and invalid scale""" + """ Test with Number with no precision and invalid scale""" schema = Schema({"number": Number(scale=2, yield_decimal=True)}) try: schema({"number": '12345678901.234'}) @@ -930,14 +932,14 @@ def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): - """ test with Number with no precision and valid scale""" + """ Test with Number with no precision and valid scale""" schema = Schema({"number": Number(precision=14, yield_decimal=True)}) out_ = schema({"number": '1234567.8901234'}) assert_equal(float(out_.get("number")), 1234567.8901234) def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): - """ test with Number with no precision and invalid scale""" + """ Test with Number with no precision and invalid scale""" schema = Schema({"number": Number(precision=14, yield_decimal=True)}) try: schema({"number": '12345674.8901234'}) @@ -949,7 +951,7 @@ def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): def test_number_validation_with_valid_precision_scale_yield_decimal_false(): - """ test with Number with valid precision, scale and no yield_decimal""" + """ Test with Number with valid precision, scale and no yield_decimal""" schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=False)}) out_ = schema({"number": '1234.00'}) assert_equal(out_.get("number"), '1234.00') @@ -1087,9 +1089,9 @@ def test_schema_infer_accepts_kwargs(): 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. + 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 + Prior to issue https://github.com/alecthomas/voluptuous/issues/259 this was exponential. """ num_of_keys = 1000 From ffa09b4b0668fdc7dba012d8c13ffb35bc83ff79 Mon Sep 17 00:00:00 2001 From: vmaillol-altair <55581329+vmaillol-altair@users.noreply.github.com> Date: Wed, 18 Nov 2020 20:13:54 +0100 Subject: [PATCH 190/261] Improve error message for In() and NotIn() (#425) * Update validators.py * Update In and NotIn validator error message --- voluptuous/validators.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 2add5d1b..fd0aa901 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -737,7 +737,8 @@ def __call__(self, v): except TypeError: check = True if check: - raise InInvalid(self.msg or 'value is not allowed') + raise InInvalid(self.msg or + 'value must be one of {}'.format(self.container)) return v def __repr__(self): @@ -757,7 +758,8 @@ def __call__(self, v): except TypeError: check = True if check: - raise NotInInvalid(self.msg or 'value is not allowed') + raise NotInInvalid(self.msg or + 'value must not be one of {}'.format(self.container)) return v def __repr__(self): From 997c8dbba92fec3f58b081141c9a4a90e046ada8 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 18 Nov 2020 20:17:20 +0100 Subject: [PATCH 191/261] Add sorted() for In and NotIn + fix tests (#436) --- voluptuous/tests/tests.py | 8 ++++---- voluptuous/validators.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 9d784bd0..28598f70 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -67,24 +67,24 @@ def test_iterate_candidates(): def test_in(): """Verify that In works.""" - schema = Schema({"color": In(frozenset(["blue", "red", "yellow"]))}) + schema = Schema({"color": In(frozenset(["red", "blue", "yellow"]))}) schema({"color": "blue"}) try: schema({"color": "orange"}) except Invalid as e: - assert_equal(str(e), "value is not allowed for dictionary value @ data['color']") + assert_equal(str(e), "value must be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']") else: assert False, "Did not raise InInvalid" def test_not_in(): """Verify that NotIn works.""" - schema = Schema({"color": NotIn(frozenset(["blue", "red", "yellow"]))}) + schema = Schema({"color": NotIn(frozenset(["red", "blue", "yellow"]))}) schema({"color": "orange"}) try: schema({"color": "blue"}) except Invalid as e: - assert_equal(str(e), "value is not allowed for dictionary value @ data['color']") + assert_equal(str(e), "value must not be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']") else: assert False, "Did not raise NotInInvalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index fd0aa901..5e9a932f 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -738,7 +738,7 @@ def __call__(self, v): check = True if check: raise InInvalid(self.msg or - 'value must be one of {}'.format(self.container)) + 'value must be one of {}'.format(sorted(self.container))) return v def __repr__(self): @@ -759,7 +759,7 @@ def __call__(self, v): check = True if check: raise NotInInvalid(self.msg or - 'value must not be one of {}'.format(self.container)) + 'value must not be one of {}'.format(sorted(self.container))) return v def __repr__(self): From de695d855949b44ec1cce41f243ad9c6a39034fd Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 19 Nov 2020 07:08:12 +0100 Subject: [PATCH 192/261] Enable stable travis-ci python 3.9 build --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 70bf0826..e6d6c6e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - '3.6' - '3.7' - '3.8' -- '3.9-dev' +- '3.9' install: - pip install coveralls - pip install coverage From da3cc967f7b16f18bf8c1feacfeced8f10ce9e40 Mon Sep 17 00:00:00 2001 From: monopolis Date: Sun, 6 Dec 2020 21:28:35 +0100 Subject: [PATCH 193/261] Ensure `Maybe` propagates error information (#411) Due to Any evaluating all options, and raising the exception from the first encountered error, Maybe would discard the expected error raised by the provided validator and only raise a invalid value error (due to values not being None). This commit changes the order of the validators in Maybe so that the first evaluated error, and thus returned is that of the provided validator. --- voluptuous/tests/tests.py | 18 +++++++++++++++++- voluptuous/validators.py | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 28598f70..428b157d 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -516,7 +516,7 @@ def test_repr(): ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") - assert_equal(repr(maybe_int), "Any(None, %s, msg=None)" % str(int)) + assert_equal(repr(maybe_int), "Any(%s, None, msg=None)" % str(int)) def test_list_validation_messages(): @@ -1530,3 +1530,19 @@ def test_any_with_discriminant(): assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') else: assert False, "Did not raise correct Invalid" + +def test_maybe_returns_subvalidator_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_equal(str(e), "value must be at most 2") + else: + assert False, "Did not raise correct Invalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 5e9a932f..7b2536c4 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -555,7 +555,7 @@ def Maybe(validator, msg=None): ... s("string") """ - return Any(None, validator, msg=msg) + return Any(validator, None, msg=msg) class Range(object): From 0639df345ee0c06690229b98a561a2c17981332a Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 6 Dec 2020 21:51:40 +0100 Subject: [PATCH 194/261] Grouped `Maybe` tests plus added a `Range` test (#437) --- voluptuous/tests/tests.py | 40 +++++++++++++++++++++++---------------- voluptuous/validators.py | 2 +- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 428b157d..50bb0698 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -601,6 +601,13 @@ def test_range_outside(): def test_range_no_upper_limit(): s = Schema(Range(min=0)) assert_equal(123, s(123)) + assert_raises(MultipleInvalid, s, -1) + + +def test_range_no_lower_limit(): + s = Schema(Range(max=10)) + assert_equal(-1, s(-1)) + assert_raises(MultipleInvalid, s, 123) def test_range_excludes_nan(): @@ -740,6 +747,23 @@ def test_maybe_accepts_msg(): assert s([]) +def test_maybe_returns_subvalidator_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_equal(str(e), "value must be at most 2") + else: + assert False, "Did not raise correct Invalid" + + def test_empty_list_as_exact(): s = Schema([]) assert_raises(Invalid, s, [1]) @@ -1530,19 +1554,3 @@ def test_any_with_discriminant(): assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') else: assert False, "Did not raise correct Invalid" - -def test_maybe_returns_subvalidator_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_equal(str(e), "value must be at most 2") - else: - assert False, "Did not raise correct Invalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 7b2536c4..fac9cc77 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -1033,7 +1033,7 @@ class SomeOf(_WithSubValidators): :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 more validations than the given amount is 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) From 1a7cb819b4f7e3998ee087ddddc46ba87dec63ef Mon Sep 17 00:00:00 2001 From: monopolis Date: Sun, 6 Dec 2020 23:08:04 +0100 Subject: [PATCH 195/261] Remove value enumeration when validating empty list (#434) * Add ability to use custom flags when running tox .. so that you can e.g. run `tox -- --pdb` to get a debugger on test failures. * Remove value enumeration when validating empty list (#397) The behaviour seems to have been introduced in commit 95489bd443e9a654 that modified the schema of an empty list in order to provide a consistent behaviour between Schema({}) and Schema([]). The commit introduced an enumeration of the incorrect values rather than the expected behaviour of showing the key with the incorrect value. This commit provides the expected behaviour, and is also consistent with the 'pathless' behaviour currently implemented, meaning that - Schema([])([1]) -> 'not a valid value @ data[1]' - Schema({'key': []})({'key': [1]) -> 'not a valid dictionary value @ data['key']. Finally, as new unit test is provided to ensure that the new behaviour remains intact. --- tox.ini | 2 +- voluptuous/schema_builder.py | 4 ++-- voluptuous/tests/tests.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 8d4c8ff7..62a29e7a 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ commands = --with-coverage3 \ --cover3-package=voluptuous \ --cover3-branch \ - --verbose + --verbose {posargs} [testenv:flake8] deps = flake8 diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 64da5b2c..df19c8da 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -615,11 +615,11 @@ def validate_sequence(path, data): if not isinstance(data, seq_type): raise er.SequenceTypeInvalid('expected a %s' % seq_type_name, path) - # Empty seq schema, allow any data. + # Empty seq schema, reject any data. if not schema: if data: raise er.MultipleInvalid([ - er.ValueInvalid('not a valid value', [value]) for value in data + er.ValueInvalid('not a valid value', path if path else data) ]) return data diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 50bb0698..eb690fc2 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1554,3 +1554,17 @@ def test_any_with_discriminant(): assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') else: assert False, "Did not raise correct Invalid" + + +def test_empty_list_raises_error_of_key_not_values(): + """ https://github.com/alecthomas/voluptuous/issues/397 """ + schema = Schema({ + Required('variables', default=[]): [] + }) + + try: + schema({'variables': ['x']}) + except MultipleInvalid as e: + assert_equal(str(e), "not a valid value for dictionary value @ data['variables']") + else: + assert False, "Did not raise correct Invalid" From c82e5d575ba7b71be8407bdcccec0c6cf4b15e0f Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 6 Dec 2020 23:33:16 +0100 Subject: [PATCH 196/261] Extend tests for `Schema` with empty list or dict (#438) --- voluptuous/tests/tests.py | 63 +++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index eb690fc2..54dcc7fc 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -764,11 +764,56 @@ def test_maybe_returns_subvalidator_error(): assert False, "Did not raise correct Invalid" -def test_empty_list_as_exact(): +def test_schema_empty_list(): s = Schema([]) - assert_raises(Invalid, s, [1]) s([]) + try: + s([123]) + except MultipleInvalid as e: + assert_equal(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_equal(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_equal(str(e), "extra keys not allowed @ data['var']") + else: + assert False, "Did not raise correct Invalid" + + try: + s([123]) + except MultipleInvalid as e: + assert_equal(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_equal(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) @@ -1554,17 +1599,3 @@ def test_any_with_discriminant(): assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') else: assert False, "Did not raise correct Invalid" - - -def test_empty_list_raises_error_of_key_not_values(): - """ https://github.com/alecthomas/voluptuous/issues/397 """ - schema = Schema({ - Required('variables', default=[]): [] - }) - - try: - schema({'variables': ['x']}) - except MultipleInvalid as e: - assert_equal(str(e), "not a valid value for dictionary value @ data['variables']") - else: - assert False, "Did not raise correct Invalid" From 5419cff1fae104690d553dec967ce99c546473bd Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 6 Dec 2020 23:50:58 +0100 Subject: [PATCH 197/261] Release 0.12.1 --- CHANGELOG.md | 19 ++++++++++++++++++- voluptuous/__init__.py | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b5f87e..82062600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [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**: @@ -157,4 +174,4 @@ translation to python 2 issue fixed. ## 0.9.3 (2016-08-03) -Changelog not kept for 0.9.3 and earlier releases. +Changelog not kept for 0.9.3 and earlier releases. \ No newline at end of file diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index fef10010..4d09fe67 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.11.7' +__version__ = '0.12.1' __author__ = 'alecthomas' From 11b4a2fa6987ba957c1fdcc1b217f4d6d6c9037d Mon Sep 17 00:00:00 2001 From: timski Date: Wed, 27 Jan 2021 15:33:04 -0500 Subject: [PATCH 198/261] Include the regular expression in the error message --- voluptuous/validators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index fac9cc77..678e3d2f 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -352,7 +352,7 @@ class Match(object): >>> validate = Schema(Match(r'^0x[A-F0-9]+$')) >>> validate('0x123EF4') '0x123EF4' - >>> with raises(MultipleInvalid, "does not match regular expression"): + >>> with raises(MultipleInvalid, 'does not match regular expression ^0x[A-F0-9]+$'): ... validate('123EF4') >>> with raises(MultipleInvalid, 'expected string or buffer'): @@ -377,7 +377,7 @@ def __call__(self, v): except TypeError: raise MatchInvalid("expected string or buffer") if not match: - raise MatchInvalid(self.msg or 'does not match regular expression') + raise MatchInvalid(self.msg or 'does not match regular expression {}'.format(self.pattern.pattern)) return v def __repr__(self): From ef32f11f159f856d88b766837ace1ed0fdefdb23 Mon Sep 17 00:00:00 2001 From: Adam Tankanow Date: Mon, 7 Dec 2020 12:08:56 -0500 Subject: [PATCH 199/261] Revert "Ensure `Maybe` propagates error information (#411)" This reverts commit da3cc967f7b16f18bf8c1feacfeced8f10ce9e40. --- voluptuous/tests/tests.py | 2 +- voluptuous/validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 54dcc7fc..448abc3b 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -516,7 +516,7 @@ def test_repr(): ) assert_equal(repr(coerce_), "Coerce(int, msg='moo')") assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") - assert_equal(repr(maybe_int), "Any(%s, None, msg=None)" % str(int)) + assert_equal(repr(maybe_int), "Any(None, %s, msg=None)" % str(int)) def test_list_validation_messages(): diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 678e3d2f..e8f8a8b1 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -555,7 +555,7 @@ def Maybe(validator, msg=None): ... s("string") """ - return Any(validator, None, msg=msg) + return Any(None, validator, msg=msg) class Range(object): From 1720439e4848d966707002707e825b448ab40342 Mon Sep 17 00:00:00 2001 From: Adam Tankanow Date: Tue, 8 Dec 2020 16:00:29 -0500 Subject: [PATCH 200/261] ISSUE-439: Fix broken test --- voluptuous/tests/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 448abc3b..cfac3e71 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -747,7 +747,7 @@ def test_maybe_accepts_msg(): assert s([]) -def test_maybe_returns_subvalidator_error(): +def test_maybe_returns_default_error(): schema = Schema(Maybe(Range(1, 2))) # The following should be valid @@ -759,7 +759,7 @@ def test_maybe_returns_subvalidator_error(): # Should trigger a MultipleInvalid exception schema(3) except MultipleInvalid as e: - assert_equal(str(e), "value must be at most 2") + assert_equal(str(e), "not a valid value") else: assert False, "Did not raise correct Invalid" From e5e3b51fe86523442f09446916f834c16c7d15d6 Mon Sep 17 00:00:00 2001 From: leonidguadalupe Date: Tue, 31 Aug 2021 13:50:34 +0800 Subject: [PATCH 201/261] Change email regex match to fullmatch This PR is about the current method used to conditionally match user and domain regex to email. currently, it's using match which wrongfully accepts entries if domain has special characters after .com it currently accepts, john@voluptuous.com> or john!@voluptuous.org!@($*! thus, we need to fullmatch the domain or user to avoid such entries and validate them properly. --- voluptuous/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index e8f8a8b1..5f72cf6f 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -435,7 +435,7 @@ def Email(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)): + if not (USER_REGEX.fullmatch(user_part) and DOMAIN_REGEX.fullmatch(domain_part)): raise EmailInvalid("Invalid email address") return v except: From 980a55f57c0ecdd4fd3fec687dbbabf132f9e022 Mon Sep 17 00:00:00 2001 From: Adam Tankanow Date: Mon, 20 Sep 2021 09:39:29 -0400 Subject: [PATCH 202/261] Release 0.12.2 --- CHANGELOG.md | 8 +++++++- voluptuous/__init__.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82062600..db73e440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [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**: @@ -174,4 +180,4 @@ translation to python 2 issue fixed. ## 0.9.3 (2016-08-03) -Changelog not kept for 0.9.3 and earlier releases. \ No newline at end of file +Changelog not kept for 0.9.3 and earlier releases. diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 4d09fe67..4d68fdd5 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.12.1' +__version__ = '0.12.2' __author__ = 'alecthomas' From 6cf017e46df42cca001e8513ba64c1033c77fe98 Mon Sep 17 00:00:00 2001 From: epenet Date: Thu, 10 Mar 2022 14:05:55 +0100 Subject: [PATCH 203/261] Display valid Enum values in Coerce --- voluptuous/tests/tests.py | 37 +++++++++++++++++++++++++++++++++++++ voluptuous/validators.py | 3 +++ 2 files changed, 40 insertions(+) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index cfac3e71..d87f301f 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,5 +1,6 @@ import copy import collections +from enum import Enum import os import sys @@ -1599,3 +1600,39 @@ def test_any_with_discriminant(): assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') else: assert False, "Did not raise correct Invalid" + +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 + try: + schema(4) + except Invalid as e: + assert_equal(str(e), + "expected Choice or one of 1, 2, 3") + else: + assert False, "Did not raise Invalid for String" + + try: + string_schema("hello") + except Invalid as e: + assert_equal(str(e), + "expected StringChoice or one of 'easy', 'medium', 'hard'") + else: + assert False, "Did not raise Invalid for String" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 5f72cf6f..972fdde9 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -4,6 +4,7 @@ import sys from functools import wraps from decimal import Decimal, InvalidOperation +from enum import Enum from voluptuous.schema_builder import Schema, raises, message from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, @@ -95,6 +96,8 @@ def __call__(self, v): return self.type(v) except (ValueError, TypeError, InvalidOperation): msg = self.msg or ('expected %s' % self.type_name) + if not self.msg 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): From c3d61efc75429bdd30213878b416722ca164c828 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 30 Mar 2022 13:22:35 +0200 Subject: [PATCH 204/261] Release 0.13.0 --- CHANGELOG.md | 6 ++++++ voluptuous/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db73e440..ab925f46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.13.0] + +**Changes**: + +- [#450](https://github.com/alecthomas/voluptuous/pull/450): Display valid Enum values in Coerce + ## [0.12.2] **Fixes**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 4d68fdd5..6f9e20b1 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.12.2' +__version__ = '0.13.0' __author__ = 'alecthomas' From 1d6d41a5e44a7abfc11ced0601a8fb788e655b03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Apr 2022 09:01:52 +0200 Subject: [PATCH 205/261] Fix email regex match for Python 2.7 (#456) --- voluptuous/tests/tests.py | 12 ++++++++++++ voluptuous/validators.py | 14 +++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index d87f301f..049221be 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -245,6 +245,18 @@ def test_email_validation_without_host(): else: assert False, "Did not raise Invalid for empty string URL" +def test_email_validation_with_bad_data(): + """ Test with bad data in email address """ + schema = Schema({"email": Email()}) + for email in ('john@voluptuous.com>', 'john!@voluptuous.org!@($*!'): + try: + schema({"email": 'john@voluptuous.com>'}) + except MultipleInvalid as e: + assert_equal(str(e), + "expected an email address for dictionary value @ data['email']") + else: + assert False, "Did not raise Invalid for bad email " + email + def test_fqdn_url_validation(): """ Test with valid fully qualified domain name URL """ diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 972fdde9..109342df 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -22,21 +22,29 @@ # Taken from https://github.com/kvesteri/validators/blob/master/validators/email.py 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])*"$)""", + 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])?\.)+' 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}\]$', + 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) __author__ = 'tusharmakkar08' @@ -438,7 +446,7 @@ def Email(v): raise EmailInvalid("Invalid email address") user_part, domain_part = v.rsplit('@', 1) - if not (USER_REGEX.fullmatch(user_part) and DOMAIN_REGEX.fullmatch(domain_part)): + if not (USER_REGEX.match(user_part) and DOMAIN_REGEX.match(domain_part)): raise EmailInvalid("Invalid email address") return v except: From 2e5d6540e81ae015b5e39503c4f7ed3e2fba93b9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 2 Apr 2022 12:59:22 +0200 Subject: [PATCH 206/261] Enable github actions (#457) --- .github/workflows/tests.yml | 38 +++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 2 files changed, 39 insertions(+) create mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..9cfed7f3 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,38 @@ +name: Tests + +on: + pull_request: + push: + branches: + - master + +jobs: + tests: + name: Tox ${{ matrix.session }} session on Python ${{ matrix.python-version }} + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + include: + - { python-version: "3.9", session: "py39" } + - { python-version: "3.8", session: "py38" } + - { python-version: "3.7", session: "py37" } + - { python-version: "3.6", session: "py36" } + + steps: + - name: Check out the repository + uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox-setuptools-version + run: | + pip install tox-setuptools-version + + - name: Run tox + run: | + pip install tox + tox -e ${{ matrix.session }} diff --git a/tox.ini b/tox.ini index 62a29e7a..82ce6410 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ exclude = .tox,.venv,build,*.egg [testenv] distribute = True sitepackages = False +setuptools_version = setuptools<58.0 deps = nose nose-cover3 From 1451f0f288f5765af8d8d542d9b274ae5a5a7e28 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 2 Apr 2022 21:13:57 +0200 Subject: [PATCH 207/261] Ignore `Enum` if it is unavailable (#454) --- .github/workflows/tests.yml | 2 ++ voluptuous/tests/tests.py | 66 ++++++++++++++++++++----------------- voluptuous/validators.py | 7 ++-- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9cfed7f3..fd90ddd1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,7 @@ jobs: - { python-version: "3.8", session: "py38" } - { python-version: "3.7", session: "py37" } - { python-version: "3.6", session: "py36" } + - { python-version: "2.7", session: "py27" } steps: - name: Check out the repository @@ -29,6 +30,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install tox-setuptools-version + if: ${{ matrix.session != 'py27' }} run: | pip install tox-setuptools-version diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 049221be..8b955002 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,6 +1,9 @@ import copy import collections -from enum import Enum +try: + from enum import Enum +except ImportError: + Enum = None import os import sys @@ -1613,38 +1616,39 @@ def test_any_with_discriminant(): else: assert False, "Did not raise correct Invalid" -def test_coerce_enum(): - """Test Coerce Enum""" - class Choice(Enum): - Easy = 1 - Medium = 2 - Hard = 3 +if Enum: + 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" + class StringChoice(str, Enum): + Easy = "easy" + Medium = "medium" + Hard = "hard" - schema = Schema(Coerce(Choice)) - string_schema = Schema(Coerce(StringChoice)) + schema = Schema(Coerce(Choice)) + string_schema = Schema(Coerce(StringChoice)) - # Valid value - assert schema(1) == Choice.Easy - assert string_schema("easy") == StringChoice.Easy + # Valid value + assert schema(1) == Choice.Easy + assert string_schema("easy") == StringChoice.Easy - # Invalid value - try: - schema(4) - except Invalid as e: - assert_equal(str(e), - "expected Choice or one of 1, 2, 3") - else: - assert False, "Did not raise Invalid for String" + # Invalid value + try: + schema(4) + except Invalid as e: + assert_equal(str(e), + "expected Choice or one of 1, 2, 3") + else: + assert False, "Did not raise Invalid for String" - try: - string_schema("hello") - except Invalid as e: - assert_equal(str(e), - "expected StringChoice or one of 'easy', 'medium', 'hard'") - else: - assert False, "Did not raise Invalid for String" + try: + string_schema("hello") + except Invalid as e: + assert_equal(str(e), + "expected StringChoice or one of 'easy', 'medium', 'hard'") + else: + assert False, "Did not raise Invalid for String" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 109342df..bf1ab062 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -4,7 +4,10 @@ import sys from functools import wraps from decimal import Decimal, InvalidOperation -from enum import Enum +try: + from enum import Enum +except ImportError: + Enum = None from voluptuous.schema_builder import Schema, raises, message from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, @@ -104,7 +107,7 @@ def __call__(self, v): return self.type(v) except (ValueError, TypeError, InvalidOperation): msg = self.msg or ('expected %s' % self.type_name) - if not self.msg and issubclass(self.type, Enum): + 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) From e6bd67b064592ba671780e7069a279f6df89e206 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 2 Apr 2022 21:42:01 +0200 Subject: [PATCH 208/261] Enable flake8 in github actions (#459) --- .github/workflows/tests.yml | 1 + tox.ini | 3 ++- voluptuous/tests/tests.py | 30 ++++++++++++++++-------------- voluptuous/util.py | 4 ++-- voluptuous/validators.py | 10 +++++----- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fd90ddd1..118b77a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,6 +14,7 @@ jobs: fail-fast: false matrix: include: + - { python-version: "3.9", session: "flake8" } - { python-version: "3.9", session: "py39" } - { python-version: "3.8", session: "py38" } - { python-version: "3.7", session: "py37" } diff --git a/tox.ini b/tox.ini index 82ce6410..ec138341 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,8 @@ envlist = flake8,py27,py36,py37,py38,py39 [flake8] ; E501: line too long (X > 79 characters) -ignore = E501 +; W504 line break after binary operator +ignore = E501,W504 exclude = .tox,.venv,build,*.egg [testenv] diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 8b955002..c9d2b93a 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -248,6 +248,7 @@ def test_email_validation_without_host(): else: assert False, "Did not raise Invalid for empty string URL" + def test_email_validation_with_bad_data(): """ Test with bad data in email address """ schema = Schema({"email": Email()}) @@ -256,7 +257,7 @@ def test_email_validation_with_bad_data(): schema({"email": 'john@voluptuous.com>'}) except MultipleInvalid as e: assert_equal(str(e), - "expected an email address for dictionary value @ data['email']") + "expected an email address for dictionary value @ data['email']") else: assert False, "Did not raise Invalid for bad email " + email @@ -796,7 +797,7 @@ def test_schema_empty_list(): except MultipleInvalid as e: assert_equal(str(e), "expected a list") else: - assert False, "Did not raise correct Invalid" + assert False, "Did not raise correct Invalid" def test_schema_empty_dict(): @@ -947,7 +948,7 @@ def test_unicode_as_key(): if sys.version_info >= (3,): text_type = str else: - text_type = unicode + text_type = unicode # noqa: F821 schema = Schema({text_type: int}) schema({u("foobar"): 1}) @@ -1533,9 +1534,9 @@ def test_any_required_with_subschema(): def test_inclusive(): schema = Schema({ - Inclusive('x', 'stuff'): int, - Inclusive('y', 'stuff'): int, - }) + Inclusive('x', 'stuff'): int, + Inclusive('y', 'stuff'): int, + }) r = schema({}) assert_equal(r, {}) @@ -1554,9 +1555,9 @@ def test_inclusive(): def test_inclusive_defaults(): schema = Schema({ - Inclusive('x', 'stuff', default=3): int, - Inclusive('y', 'stuff', default=4): int, - }) + Inclusive('x', 'stuff', default=3): int, + Inclusive('y', 'stuff', default=4): int, + }) r = schema({}) assert_equal(r, {'x': 3, 'y': 4}) @@ -1572,9 +1573,9 @@ def test_inclusive_defaults(): def test_exclusive(): schema = Schema({ - Exclusive('x', 'stuff'): int, - Exclusive('y', 'stuff'): int, - }) + Exclusive('x', 'stuff'): int, + Exclusive('y', 'stuff'): int, + }) r = schema({}) assert_equal(r, {}) @@ -1616,6 +1617,7 @@ def test_any_with_discriminant(): else: assert False, "Did not raise correct Invalid" + if Enum: def test_coerce_enum(): """Test Coerce Enum""" @@ -1641,7 +1643,7 @@ class StringChoice(str, Enum): schema(4) except Invalid as e: assert_equal(str(e), - "expected Choice or one of 1, 2, 3") + "expected Choice or one of 1, 2, 3") else: assert False, "Did not raise Invalid for String" @@ -1649,6 +1651,6 @@ class StringChoice(str, Enum): string_schema("hello") except Invalid as e: assert_equal(str(e), - "expected StringChoice or one of 'easy', 'medium', 'hard'") + "expected StringChoice or one of 'easy', 'medium', 'hard'") else: assert False, "Did not raise Invalid for String" diff --git a/voluptuous/util.py b/voluptuous/util.py index e0ff43f8..f57b8d78 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -8,7 +8,7 @@ def _fix_str(v): - if sys.version_info[0] == 2 and isinstance(v, unicode): + if sys.version_info[0] == 2 and isinstance(v, unicode): # noqa: F821 s = v else: s = str(v) @@ -157,6 +157,6 @@ def __repr__(self): def u(x): if sys.version_info < (3,): - return unicode(x) + return unicode(x) # noqa: F821 else: return x diff --git a/voluptuous/validators.py b/voluptuous/validators.py index bf1ab062..d0f1c434 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -452,7 +452,7 @@ def Email(v): if not (USER_REGEX.match(user_part) and DOMAIN_REGEX.match(domain_part)): raise EmailInvalid("Invalid email address") return v - except: + except: # noqa: E722 raise ValueError @@ -471,7 +471,7 @@ def FqdnUrl(v): if "." not in parsed_url.netloc: raise UrlInvalid("must have a domain name in URL") return v - except: + except: # noqa: E722 raise ValueError @@ -488,7 +488,7 @@ def Url(v): try: _url_validation(v) return v - except: + except: # noqa: E722 raise ValueError @@ -751,7 +751,7 @@ def __call__(self, v): except TypeError: check = True if check: - raise InInvalid(self.msg or + raise InInvalid(self.msg or 'value must be one of {}'.format(sorted(self.container))) return v @@ -772,7 +772,7 @@ def __call__(self, v): except TypeError: check = True if check: - raise NotInInvalid(self.msg or + raise NotInInvalid(self.msg or 'value must not be one of {}'.format(sorted(self.container))) return v From 402da7c8785c41db875c5eaaa13c89a98ae42d65 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sat, 2 Apr 2022 21:55:38 +0200 Subject: [PATCH 209/261] Convert codebase to adhere to flake8 W504 (PEP 8) (#462) --- tox.ini | 4 ++-- voluptuous/schema_builder.py | 11 ++++++----- voluptuous/tests/tests.py | 12 ++++++------ voluptuous/validators.py | 8 ++++---- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/tox.ini b/tox.ini index ec138341..bcd52cbe 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,8 @@ envlist = flake8,py27,py36,py37,py38,py39 [flake8] ; E501: line too long (X > 79 characters) -; W504 line break after binary operator -ignore = E501,W504 +; W503 line break before binary operator +ignore = E501,W503 exclude = .tox,.venv,build,*.egg [testenv] diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index df19c8da..d308bd3e 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -308,14 +308,15 @@ def _compile_mapping(self, schema, invalid_msg=None): # 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))) + 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)) + if isinstance(key, Required) + or isinstance(key, Optional)) _compiled_schema = {} for skey, svalue in iteritems(schema): diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index c9d2b93a..577732bb 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -497,8 +497,8 @@ def test_inequality(): def test_inequality_negative(): assert_false(Schema('foo') != Schema('foo')) - assert_false(Schema(['foo', 'bar', 'baz']) != - Schema(['foo', 'bar', 'baz'])) + assert_false(Schema(['foo', 'bar', 'baz']) + != Schema(['foo', 'bar', 'baz'])) # Ensure two Schemas w/ two equivalent dicts initialized in a different # order are considered equal. @@ -1305,8 +1305,8 @@ def test_any_error_has_path(): s({'q': 'str', 'q2': 'tata'}) except MultipleInvalid as exc: assert ( - (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) or - (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) + (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) + or (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) ) else: assert False, "Did not raise AnyInvalid" @@ -1322,8 +1322,8 @@ def test_all_error_has_path(): s({'q': 'str', 'q2': 12}) except MultipleInvalid as exc: assert ( - (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) or - (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) + (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) + or (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) ) else: assert False, "Did not raise AllInvalid" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index d0f1c434..9019f1f7 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -751,8 +751,8 @@ def __call__(self, v): except TypeError: check = True if check: - raise InInvalid(self.msg or - 'value must be one of {}'.format(sorted(self.container))) + raise InInvalid(self.msg + or 'value must be one of {}'.format(sorted(self.container))) return v def __repr__(self): @@ -772,8 +772,8 @@ def __call__(self, v): except TypeError: check = True if check: - raise NotInInvalid(self.msg or - 'value must not be one of {}'.format(sorted(self.container))) + raise NotInInvalid(self.msg + or 'value must not be one of {}'.format(sorted(self.container))) return v def __repr__(self): From 6003cb30c520e650976ea5b936d04494b50d7849 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 3 Apr 2022 11:51:13 +0200 Subject: [PATCH 210/261] `pytest` migration + enable Python 3.10 (#464) Co-authored-by: pgajdos Co-authored-by: epenet --- .github/workflows/tests.yml | 13 +- README.md | 4 +- pytest.ini | 4 + setup.cfg | 4 - tox.ini | 14 +- voluptuous/tests/tests.py | 391 +++++++++++++++++------------------- 6 files changed, 203 insertions(+), 227 deletions(-) create mode 100644 pytest.ini delete mode 100644 setup.cfg diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 118b77a2..9b9413f8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,12 +14,13 @@ jobs: fail-fast: false matrix: include: - - { python-version: "3.9", session: "flake8" } - - { python-version: "3.9", session: "py39" } - - { python-version: "3.8", session: "py38" } - - { python-version: "3.7", session: "py37" } - - { python-version: "3.6", session: "py36" } - - { python-version: "2.7", session: "py27" } + - { python-version: "3.10", session: "flake8" } + - { python-version: "3.10", session: "py310" } + - { python-version: "3.9", session: "py39" } + - { python-version: "3.8", session: "py38" } + - { python-version: "3.7", session: "py37" } + - { python-version: "3.6", session: "py36" } + - { python-version: "2.7", session: "py27" } steps: - name: Check out the repository diff --git a/README.md b/README.md index a6900586..21a4a61d 100644 --- a/README.md +++ b/README.md @@ -715,9 +715,9 @@ s({'password':'123', 'password_again': 1337}) ## Running tests -Voluptuous is using nosetests: +Voluptuous is using pytest: - $ nosetests + $ pytest ## Other libraries and inspirations diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..0048dd07 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +python_files = tests.py +testpaths = voluptuous/tests +addopts = --doctest-glob=*.md -v diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index adcebe26..00000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[nosetests] -doctest-extension = md -with-doctest = 1 -where = . diff --git a/tox.ini b/tox.ini index bcd52cbe..683c5b77 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py36,py37,py38,py39 +envlist = flake8,py27,py36,py37,py38,py39,py310 [flake8] ; E501: line too long (X > 79 characters) @@ -12,15 +12,13 @@ distribute = True sitepackages = False setuptools_version = setuptools<58.0 deps = - nose - nose-cover3 + pytest + pytest-cov coverage>=3.0 commands = - nosetests \ - --with-coverage3 \ - --cover3-package=voluptuous \ - --cover3-branch \ - --verbose {posargs} + pytest \ + --cov=voluptuous \ + voluptuous/tests/ [testenv:flake8] deps = flake8 diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 577732bb..c792f077 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -6,8 +6,7 @@ Enum = None import os import sys - -from nose.tools import assert_equal, assert_false, assert_raises, assert_true +import pytest from voluptuous import ( Schema, Required, Exclusive, Inclusive, Optional, Extra, Invalid, In, Remove, @@ -24,7 +23,7 @@ def test_new_required_test(): schema = Schema({ 'my_key': All(int, Range(1, 20)), }, required=True) - assert_true(schema.required) + assert schema.required def test_exact_sequence(): @@ -35,7 +34,7 @@ def test_exact_sequence(): assert True else: assert False, "Did not raise Invalid" - assert_equal(schema([1, 2]), [1, 2]) + assert schema([1, 2]) == [1, 2] def test_required(): @@ -45,7 +44,7 @@ def test_required(): try: schema({}) except Invalid as e: - assert_equal(str(e), "required key not provided @ data['q']") + assert str(e) == "required key not provided @ data['q']" else: assert False, "Did not raise Invalid" @@ -54,8 +53,7 @@ def test_extra_with_required(): """Verify that Required does not break Extra.""" schema = Schema({Required('toaster'): str, Extra: object}) r = schema({'toaster': 'blue', 'another_valid_key': 'another_valid_value'}) - assert_equal( - r, {'toaster': 'blue', 'another_valid_key': 'another_valid_value'}) + assert r == {'toaster': 'blue', 'another_valid_key': 'another_valid_value'} def test_iterate_candidates(): @@ -66,7 +64,7 @@ def test_iterate_candidates(): } # toaster should be first. from voluptuous.schema_builder import _iterate_mapping_candidates - assert_equal(_iterate_mapping_candidates(schema)[0][0], 'toaster') + assert _iterate_mapping_candidates(schema)[0][0] == 'toaster' def test_in(): @@ -76,7 +74,7 @@ def test_in(): try: schema({"color": "orange"}) except Invalid as e: - assert_equal(str(e), "value must be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']") + assert str(e) == "value must be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']" else: assert False, "Did not raise InInvalid" @@ -88,7 +86,7 @@ def test_not_in(): try: schema({"color": "blue"}) except Invalid as e: - assert_equal(str(e), "value must not be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']") + assert str(e) == "value must not be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']" else: assert False, "Did not raise NotInInvalid" @@ -100,8 +98,7 @@ def test_contains(): try: schema({'color': ['blue', 'yellow']}) except Invalid as e: - assert_equal(str(e), - "value is not allowed for dictionary value @ data['color']") + assert str(e) == "value is not allowed for dictionary value @ data['color']" def test_remove(): @@ -133,12 +130,12 @@ def test_remove(): # remove value from list schema = Schema([Remove(1), int]) out_ = schema([1, 2, 3, 4, 1, 5, 6, 1, 1, 1]) - assert_equal(out_, [2, 3, 4, 5, 6]) + 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_equal(out_, [1, 2, 1.0, 4]) + assert out_ == [1, 2, 1.0, 4] def test_extra_empty_errors(): @@ -157,7 +154,7 @@ def test_literal(): try: schema([{"c": 1}]) except Invalid as e: - assert_equal(str(e), "{'c': 1} not match for {'b': 1} @ data[0]") + assert str(e) == "{'c': 1} not match for {'b': 1} @ data[0]" else: assert False, "Did not raise Invalid" @@ -165,9 +162,9 @@ def test_literal(): try: schema({"b": 1}) except MultipleInvalid as e: - assert_equal(str(e), "{'b': 1} not match for {'a': 1}") - assert_equal(len(e.errors), 1) - assert_equal(type(e.errors[0]), LiteralInvalid) + assert str(e) == "{'b': 1} not match for {'a': 1}" + assert len(e.errors) == 1 + assert type(e.errors[0]) == LiteralInvalid else: assert False, "Did not raise Invalid" @@ -182,9 +179,9 @@ class C1(object): try: schema(None) except MultipleInvalid as e: - assert_equal(str(e), "expected C1") - assert_equal(len(e.errors), 1) - assert_equal(type(e.errors[0]), TypeInvalid) + assert str(e) == "expected C1" + assert len(e.errors) == 1 + assert type(e.errors[0]) == TypeInvalid else: assert False, "Did not raise Invalid" @@ -198,9 +195,9 @@ class C2: try: schema(None) except MultipleInvalid as e: - assert_equal(str(e), "expected C2") - assert_equal(len(e.errors), 1) - assert_equal(type(e.errors[0]), TypeInvalid) + assert str(e) == "expected C2" + assert len(e.errors) == 1 + assert type(e.errors[0]) == TypeInvalid else: assert False, "Did not raise Invalid" @@ -219,8 +216,7 @@ def test_email_validation_with_none(): try: schema({"email": None}) except MultipleInvalid as e: - assert_equal(str(e), - "expected an email address for dictionary value @ data['email']") + assert str(e) == "expected an email address for dictionary value @ data['email']" else: assert False, "Did not raise Invalid for None URL" @@ -231,8 +227,7 @@ def test_email_validation_with_empty_string(): try: schema({"email": ''}) except MultipleInvalid as e: - assert_equal(str(e), - "expected an email address for dictionary value @ data['email']") + assert str(e) == "expected an email address for dictionary value @ data['email']" else: assert False, "Did not raise Invalid for empty string URL" @@ -243,8 +238,7 @@ def test_email_validation_without_host(): try: schema({"email": 'a@.com'}) except MultipleInvalid as e: - assert_equal(str(e), - "expected an email address for dictionary value @ data['email']") + assert str(e) == "expected an email address for dictionary value @ data['email']" else: assert False, "Did not raise Invalid for empty string URL" @@ -256,8 +250,7 @@ def test_email_validation_with_bad_data(): try: schema({"email": 'john@voluptuous.com>'}) except MultipleInvalid as e: - assert_equal(str(e), - "expected an email address for dictionary value @ data['email']") + assert str(e) == "expected an email address for dictionary value @ data['email']" else: assert False, "Did not raise Invalid for bad email " + email @@ -276,8 +269,7 @@ def test_fqdn_url_without_domain_name(): try: schema({"url": "http://localhost/"}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a fully qualified domain name URL for dictionary value @ data['url']") + assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for None URL" @@ -288,8 +280,7 @@ def test_fqdnurl_validation_with_none(): try: schema({"url": None}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a fully qualified domain name URL for dictionary value @ data['url']") + assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for None URL" @@ -300,8 +291,7 @@ def test_fqdnurl_validation_with_empty_string(): try: schema({"url": ''}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a fully qualified domain name URL for dictionary value @ data['url']") + assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for empty string URL" @@ -312,8 +302,7 @@ def test_fqdnurl_validation_without_host(): try: schema({"url": 'http://'}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a fully qualified domain name URL for dictionary value @ data['url']") + assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for empty string URL" @@ -332,8 +321,7 @@ def test_url_validation_with_none(): try: schema({"url": None}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a URL for dictionary value @ data['url']") + assert str(e) == "expected a URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for None URL" @@ -344,8 +332,7 @@ def test_url_validation_with_empty_string(): try: schema({"url": ''}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a URL for dictionary value @ data['url']") + assert str(e) == "expected a URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for empty string URL" @@ -356,8 +343,7 @@ def test_url_validation_without_host(): try: schema({"url": 'http://'}) except MultipleInvalid as e: - assert_equal(str(e), - "expected a URL for dictionary value @ data['url']") + assert str(e) == "expected a URL for dictionary value @ data['url']" else: assert False, "Did not raise Invalid for empty string URL" @@ -421,10 +407,10 @@ def test_schema_extend_key_swap(): extension = {Required('a'): int} extended = base.extend(extension) - assert_equal(len(base.schema), 1) - assert_true(isinstance(list(base.schema)[0], Optional)) - assert_equal(len(extended.schema), 1) - assert_true((list(extended.schema)[0], Required)) + 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(): @@ -433,9 +419,9 @@ def test_subschema_extension(): extension = {'d': str, 'a': {'b': str, 'e': int}} extended = base.extend(extension) - assert_equal(base.schema, {'a': {'b': int, 'c': float}}) - assert_equal(extension, {'d': str, 'a': {'b': str, 'e': int}}) - assert_equal(extended.schema, {'a': {'b': str, 'c': float, 'e': int}, 'd': str}) + 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(): @@ -453,10 +439,9 @@ class S(Schema): def test_equality(): - assert_equal(Schema('foo'), Schema('foo')) + assert Schema('foo') == Schema('foo') - assert_equal(Schema(['foo', 'bar', 'baz']), - Schema(['foo', 'bar', 'baz'])) + assert Schema(['foo', 'bar', 'baz']) == Schema(['foo', 'bar', 'baz']) # Ensure two Schemas w/ two equivalent dicts initialized in a different # order are considered equal. @@ -470,35 +455,34 @@ def test_equality(): dict_b['bar'] = 2 dict_b['foo'] = 1 - assert_equal(Schema(dict_a), Schema(dict_b)) + assert Schema(dict_a) == Schema(dict_b) def test_equality_negative(): """Verify that Schema objects are not equal to string representations""" - assert_false(Schema('foo') == 'foo') + assert not Schema('foo') == 'foo' - assert_false(Schema(['foo', 'bar']) == "['foo', 'bar']") - assert_false(Schema(['foo', 'bar']) == Schema("['foo', 'bar']")) + assert not Schema(['foo', 'bar']) == "['foo', 'bar']" + assert not Schema(['foo', 'bar']) == Schema("['foo', 'bar']") - assert_false(Schema({'foo': 1, 'bar': 2}) == "{'foo': 1, 'bar': 2}") - assert_false(Schema({'foo': 1, 'bar': 2}) == Schema("{'foo': 1, 'bar': 2}")) + 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_true(Schema('foo') != 'foo') + assert Schema('foo') != 'foo' - assert_true(Schema(['foo', 'bar']) != "['foo', 'bar']") - assert_true(Schema(['foo', 'bar']) != Schema("['foo', 'bar']")) + assert Schema(['foo', 'bar']) != "['foo', 'bar']" + assert Schema(['foo', 'bar']) != Schema("['foo', 'bar']") - assert_true(Schema({'foo': 1, 'bar': 2}) != "{'foo': 1, 'bar': 2}") - assert_true(Schema({'foo': 1, 'bar': 2}) != Schema("{'foo': 1, 'bar': 2}")) + 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_false(Schema('foo') != Schema('foo')) + assert not Schema('foo') != Schema('foo') - assert_false(Schema(['foo', 'bar', 'baz']) - != Schema(['foo', 'bar', 'baz'])) + 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. @@ -512,7 +496,7 @@ def test_inequality_negative(): dict_b['bar'] = 2 dict_b['foo'] = 1 - assert_false(Schema(dict_a) != Schema(dict_b)) + assert not Schema(dict_a) != Schema(dict_b) def test_repr(): @@ -525,15 +509,12 @@ def test_repr(): all_ = All('10', Coerce(int), msg='all msg') maybe_int = Maybe(int) - assert_equal(repr(match), "Match('a pattern', msg='message')") - assert_equal(repr(replace), "Replace('you', 'I', msg='you and I')") - assert_equal( - repr(range_), - "Range(min=0, max=42, min_included=False, max_included=False, msg='number not in range')" - ) - assert_equal(repr(coerce_), "Coerce(int, msg='moo')") - assert_equal(repr(all_), "All('10', Coerce(int, msg=None), msg='all msg')") - assert_equal(repr(maybe_int), "Any(None, %s, msg=None)" % str(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(): @@ -549,9 +530,9 @@ def is_even(value): try: schema(dict(even_numbers=[3])) except Invalid as e: - assert_equal(len(e.errors), 1, e.errors) - assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") - assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + assert len(e.errors) == 1 + assert str(e.errors[0]) == "3 is not even @ data['even_numbers'][0]" + assert str(e) == "3 is not even @ data['even_numbers'][0]" else: assert False, "Did not raise Invalid" @@ -570,9 +551,9 @@ def is_even(value): try: schema(dict(even_numbers=[3])) except Invalid as e: - assert_equal(len(e.errors), 1, e.errors) - assert_equal(str(e.errors[0]), "3 is not even @ data['even_numbers'][0]") - assert_equal(str(e), "3 is not even @ data['even_numbers'][0]") + assert len(e.errors) == 1 + assert str(e.errors[0]) == "3 is not even @ data['even_numbers'][0]" + assert str(e) == "3 is not even @ data['even_numbers'][0]" else: assert False, "Did not raise Invalid" @@ -589,57 +570,58 @@ def test_humanize_error(): try: schema(data) except MultipleInvalid as e: - assert_equal( - humanize_error(data, e), - "expected int for dictionary value @ data['a']. Got 'not an int'\n" - "expected str @ data['b'][0]. Got 123" - ) + assert humanize_error(data, e) == "expected int for dictionary value @ data['a']. Got 'not an int'\nexpected str @ data['b'][0]. Got 123" else: assert False, 'Did not raise MultipleInvalid' def test_fix_157(): s = Schema(All([Any('one', 'two', 'three')]), Length(min=1)) - assert_equal(['one'], s(['one'])) - assert_raises(MultipleInvalid, s, ['four']) + assert ['one'] == s(['one']) + pytest.raises(MultipleInvalid, s, ['four']) def test_range_inside(): s = Schema(Range(min=0, max=10)) - assert_equal(5, s(5)) + assert 5 == s(5) def test_range_outside(): s = Schema(Range(min=0, max=10)) - assert_raises(MultipleInvalid, s, 12) - assert_raises(MultipleInvalid, s, -1) + with pytest.raises(MultipleInvalid): + s(12) + with pytest.raises(MultipleInvalid): + s(-1) def test_range_no_upper_limit(): s = Schema(Range(min=0)) - assert_equal(123, s(123)) - assert_raises(MultipleInvalid, s, -1) + assert 123 == s(123) + with pytest.raises(MultipleInvalid): + s(-1) def test_range_no_lower_limit(): s = Schema(Range(max=10)) - assert_equal(-1, s(-1)) - assert_raises(MultipleInvalid, s, 123) + assert -1 == s(-1) + with pytest.raises(MultipleInvalid): + s(123) def test_range_excludes_nan(): s = Schema(Range(min=0, max=10)) - assert_raises(MultipleInvalid, s, float('nan')) + pytest.raises(MultipleInvalid, s, float('nan')) def test_range_excludes_none(): s = Schema(Range(min=0, max=10)) - assert_raises(MultipleInvalid, s, None) + pytest.raises(MultipleInvalid, s, None) def test_range_excludes_string(): s = Schema(Range(min=0, max=10)) - assert_raises(MultipleInvalid, s, "abc") + with pytest.raises(MultipleInvalid): + s("abc") def test_range_excludes_unordered_object(): @@ -647,75 +629,81 @@ class MyObject(object): pass s = Schema(Range(min=0, max=10)) - assert_raises(MultipleInvalid, s, MyObject()) + pytest.raises(MultipleInvalid, s, MyObject()) def test_clamp_inside(): s = Schema(Clamp(min=1, max=10)) - assert_equal(5, s(5)) + assert 5 == s(5) def test_clamp_above(): s = Schema(Clamp(min=1, max=10)) - assert_equal(10, s(12)) + assert 10 == s(12) def test_clamp_below(): s = Schema(Clamp(min=1, max=10)) - assert_equal(1, s(-3)) + assert 1 == s(-3) def test_clamp_invalid(): s = Schema(Clamp(min=1, max=10)) if sys.version_info.major >= 3: - assert_raises(MultipleInvalid, s, None) - assert_raises(MultipleInvalid, s, "abc") + with pytest.raises(MultipleInvalid): + s(None) + with pytest.raises(MultipleInvalid): + s("abc") else: - assert_equal(1, s(None)) + assert 1 == s(None) def test_length_ok(): v1 = ['a', 'b', 'c'] s = Schema(Length(min=1, max=10)) - assert_equal(v1, s(v1)) + assert v1 == s(v1) v2 = "abcde" - assert_equal(v2, s(v2)) + assert v2 == s(v2) def test_length_too_short(): v1 = [] s = Schema(Length(min=1, max=10)) - assert_raises(MultipleInvalid, s, v1) - v2 = '' - assert_raises(MultipleInvalid, s, v2) + 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)) - assert_raises(MultipleInvalid, s, v) + with pytest.raises(MultipleInvalid): + s(v) def test_length_invalid(): v = None s = Schema(Length(min=0, max=2)) - assert_raises(MultipleInvalid, s, v) + with pytest.raises(MultipleInvalid): + s(v) def test_equal(): s = Schema(Equal(1)) s(1) - assert_raises(Invalid, s, 2) + pytest.raises(Invalid, s, 2) s = Schema(Equal('foo')) s('foo') - assert_raises(Invalid, s, 'bar') + pytest.raises(Invalid, s, 'bar') s = Schema(Equal([1, 2])) s([1, 2]) - assert_raises(Invalid, s, []) - assert_raises(Invalid, s, [1, 2, 3]) + pytest.raises(Invalid, s, []) + pytest.raises(Invalid, s, [1, 2, 3]) # Evaluates exactly, not through validators s = Schema(Equal(str)) - assert_raises(Invalid, s, 'foo') + pytest.raises(Invalid, s, 'foo') def test_unordered(): @@ -724,15 +712,15 @@ def test_unordered(): s([2, 1]) s([1, 2]) # Amount of errors is OK - assert_raises(Invalid, s, [2, 0]) - assert_raises(MultipleInvalid, s, [0, 0]) + pytest.raises(Invalid, s, [2, 0]) + pytest.raises(MultipleInvalid, s, [0, 0]) # Different length is NOK - assert_raises(Invalid, s, [1]) - assert_raises(Invalid, s, [1, 2, 0]) - assert_raises(MultipleInvalid, s, [1, 2, 0, 0]) + 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 - assert_raises(Invalid, s, 'foo') - assert_raises(Invalid, s, 10) + pytest.raises(Invalid, s, 'foo') + pytest.raises(Invalid, s, 10) # Validators are evaluated through as schemas s = Schema(Unordered([int, str])) s([1, '2']) @@ -741,7 +729,7 @@ def test_unordered(): s([{'foo': 3}, []]) # Most accurate validators must be positioned on left s = Schema(Unordered([int, 3])) - assert_raises(Invalid, s, [3, 2]) + pytest.raises(Invalid, s, [3, 2]) s = Schema(Unordered([3, int])) s([3, 2]) @@ -750,12 +738,12 @@ def test_maybe(): s = Schema(Maybe(int)) assert s(1) == 1 assert s(None) is None - assert_raises(Invalid, s, 'foo') + pytest.raises(Invalid, s, 'foo') s = Schema(Maybe({str: Coerce(int)})) assert s({'foo': '100'}) == {'foo': 100} assert s(None) is None - assert_raises(Invalid, s, {'foo': 'bar'}) + pytest.raises(Invalid, s, {'foo': 'bar'}) def test_maybe_accepts_msg(): @@ -776,7 +764,7 @@ def test_maybe_returns_default_error(): # Should trigger a MultipleInvalid exception schema(3) except MultipleInvalid as e: - assert_equal(str(e), "not a valid value") + assert str(e) == "not a valid value" else: assert False, "Did not raise correct Invalid" @@ -788,14 +776,14 @@ def test_schema_empty_list(): try: s([123]) except MultipleInvalid as e: - assert_equal(str(e), "not a valid value @ data[123]") + 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_equal(str(e), "expected a list") + assert str(e) == "expected a list" else: assert False, "Did not raise correct Invalid" @@ -807,14 +795,14 @@ def test_schema_empty_dict(): try: s({'var': 123}) except MultipleInvalid as e: - assert_equal(str(e), "extra keys not allowed @ data['var']") + 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_equal(str(e), "expected a dictionary") + assert str(e) == "expected a dictionary" else: assert False, "Did not raise correct Invalid" @@ -827,7 +815,7 @@ def test_schema_empty_dict_key(): try: s({'var': [123]}) except MultipleInvalid as e: - assert_equal(str(e), "not a valid value for dictionary value @ data['var']") + assert str(e) == "not a valid value for dictionary value @ data['var']" else: assert False, "Did not raise correct Invalid" @@ -845,7 +833,7 @@ def test_schema_decorator_unmatch_with_args(): def fn(arg): return arg - assert_raises(Invalid, fn, 1.0) + pytest.raises(Invalid, fn, 1.0) def test_schema_decorator_match_with_kwargs(): @@ -861,7 +849,7 @@ def test_schema_decorator_unmatch_with_kwargs(): def fn(arg): return arg - assert_raises(Invalid, fn, 1.0) + pytest.raises(Invalid, fn, 1.0) def test_schema_decorator_match_return_with_args(): @@ -877,7 +865,7 @@ def test_schema_decorator_unmatch_return_with_args(): def fn(arg): return "hello" - assert_raises(Invalid, fn, 1) + pytest.raises(Invalid, fn, 1) def test_schema_decorator_match_return_with_kwargs(): @@ -893,7 +881,7 @@ def test_schema_decorator_unmatch_return_with_kwargs(): def fn(arg): return "hello" - assert_raises(Invalid, fn, 1) + pytest.raises(Invalid, fn, 1) def test_schema_decorator_return_only_match(): @@ -909,7 +897,7 @@ def test_schema_decorator_return_only_unmatch(): def fn(arg): return "hello" - assert_raises(Invalid, fn, 1) + pytest.raises(Invalid, fn, 1) def test_schema_decorator_partial_match_called_with_args(): @@ -925,7 +913,7 @@ def test_schema_decorator_partial_unmatch_called_with_args(): def fn(arg1, arg2): return arg1 - assert_raises(Invalid, fn, "bar", "foo") + pytest.raises(Invalid, fn, "bar", "foo") def test_schema_decorator_partial_match_called_with_kwargs(): @@ -941,7 +929,7 @@ def test_schema_decorator_partial_unmatch_called_with_kwargs(): def fn(arg1, arg2): return arg1 - assert_raises(Invalid, fn, arg1=1, arg2="foo") + pytest.raises(Invalid, fn, arg1=1, arg2="foo") def test_unicode_as_key(): @@ -959,8 +947,7 @@ def test_number_validation_with_string(): try: schema({"number": 'teststr'}) except MultipleInvalid as e: - assert_equal(str(e), - "Value must be a number enclosed with string for dictionary value @ data['number']") + 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" @@ -971,8 +958,7 @@ def test_number_validation_with_invalid_precision_invalid_scale(): try: schema({"number": '123456.712'}) except MultipleInvalid as e: - assert_equal(str(e), - "Precision must be equal to 6, and Scale must be equal to 2 for dictionary value @ data['number']") + 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" @@ -981,28 +967,28 @@ def test_number_validation_with_valid_precision_scale_yield_decimal_true(): """ Test with Number with valid precision and scale""" schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=True)}) out_ = schema({"number": '1234.00'}) - assert_equal(float(out_.get("number")), 1234.00) + 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_equal(out_.get("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_equal(float(out_.get("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_equal(float(out_.get("number")), 123456789012.00) + assert float(out_.get("number")) == 123456789012.00 def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): @@ -1011,8 +997,7 @@ def test_number_when_precision_none_n_invalid_scale_yield_decimal_true(): try: schema({"number": '12345678901.234'}) except MultipleInvalid as e: - assert_equal(str(e), - "Scale must be equal to 2 for dictionary value @ data['number']") + assert str(e) == "Scale must be equal to 2 for dictionary value @ data['number']" else: assert False, "Did not raise Invalid for String" @@ -1021,7 +1006,7 @@ def test_number_when_valid_precision_n_scale_none_yield_decimal_true(): """ Test with Number with no precision and valid scale""" schema = Schema({"number": Number(precision=14, yield_decimal=True)}) out_ = schema({"number": '1234567.8901234'}) - assert_equal(float(out_.get("number")), 1234567.8901234) + assert float(out_.get("number")) == 1234567.8901234 def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): @@ -1030,8 +1015,7 @@ def test_number_when_invalid_precision_n_scale_none_yield_decimal_true(): try: schema({"number": '12345674.8901234'}) except MultipleInvalid as e: - assert_equal(str(e), - "Precision must be equal to 14 for dictionary value @ data['number']") + assert str(e) == "Precision must be equal to 14 for dictionary value @ data['number']" else: assert False, "Did not raise Invalid for String" @@ -1040,7 +1024,7 @@ def test_number_validation_with_valid_precision_scale_yield_decimal_false(): """ Test with Number with valid precision, scale and no yield_decimal""" schema = Schema({"number": Number(precision=6, scale=2, yield_decimal=False)}) out_ = schema({"number": '1234.00'}) - assert_equal(out_.get("number"), '1234.00') + assert out_.get("number") == '1234.00' def test_named_tuples_validate_as_tuples(): @@ -1057,19 +1041,19 @@ def test_named_tuples_validate_as_tuples(): def test_datetime(): schema = Schema({"datetime": Datetime()}) schema({"datetime": "2016-10-24T14:01:57.102152Z"}) - assert_raises(MultipleInvalid, schema, {"datetime": "2016-10-24T14:01:57"}) + pytest.raises(MultipleInvalid, schema, {"datetime": "2016-10-24T14:01:57"}) def test_date(): schema = Schema({"date": Date()}) schema({"date": "2016-10-24"}) - assert_raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) + pytest.raises(MultipleInvalid, schema, {"date": "2016-10-24Z"}) def test_date_custom_format(): schema = Schema({"date": Date("%Y%m%d")}) schema({"date": "20161024"}) - assert_raises(MultipleInvalid, schema, {"date": "2016-10-24"}) + pytest.raises(MultipleInvalid, schema, {"date": "2016-10-24"}) def test_ordered_dict(): @@ -1091,12 +1075,12 @@ def test_marker_hashable(): Required('x'): int, Optional('y'): float, Remove('j'): int, Remove(int): str, int: int } - assert_equal(definition.get('x'), int) - assert_equal(definition.get('y'), float) - assert_true(Required('x') == Required('x')) - assert_true(Required('x') != Required('y')) + 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_equal(definition.get('j'), None) + assert definition.get('j') is None def test_schema_infer(): @@ -1106,12 +1090,12 @@ def test_schema_infer(): 'int': 42, 'float': 3.14 }) - assert_equal(schema, Schema({ + assert schema == Schema({ Required('str'): str, Required('bool'): bool, Required('int'): int, Required('float'): float - })) + }) def test_schema_infer_dict(): @@ -1123,13 +1107,13 @@ def test_schema_infer_dict(): } }) - assert_equal(schema, Schema({ + assert schema == Schema({ Required('a'): { Required('b'): { Required('c'): str } } - })) + }) def test_schema_infer_list(): @@ -1137,18 +1121,18 @@ def test_schema_infer_list(): 'list': ['foo', True, 42, 3.14] }) - assert_equal(schema, Schema({ + assert schema == Schema({ Required('list'): [str, bool, int, float] - })) + }) def test_schema_infer_scalar(): - assert_equal(Schema.infer('foo'), Schema(str)) - assert_equal(Schema.infer(True), Schema(bool)) - assert_equal(Schema.infer(42), Schema(int)) - assert_equal(Schema.infer(3.14), Schema(float)) - assert_equal(Schema.infer({}), Schema(dict)) - assert_equal(Schema.infer([]), Schema(list)) + 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(): @@ -1211,19 +1195,19 @@ def __call__(self, *args, **kwargs): def test_IsDir(): schema = Schema(IsDir()) - assert_raises(MultipleInvalid, schema, 3) + pytest.raises(MultipleInvalid, schema, 3) schema(os.path.dirname(os.path.abspath(__file__))) def test_IsFile(): schema = Schema(IsFile()) - assert_raises(MultipleInvalid, schema, 3) + pytest.raises(MultipleInvalid, schema, 3) schema(os.path.abspath(__file__)) def test_PathExists(): schema = Schema(PathExists()) - assert_raises(MultipleInvalid, schema, 3) + pytest.raises(MultipleInvalid, schema, 3) schema(os.path.abspath(__file__)) @@ -1397,7 +1381,7 @@ def test_SomeOf_on_bounds_assertion(): def test_comparing_voluptuous_object_to_str(): - assert_true(Optional('Classification') < 'Name') + assert Optional('Classification') < 'Name' def test_set_of_integers(): @@ -1413,7 +1397,7 @@ def test_set_of_integers(): try: schema(set(['abc'])) except MultipleInvalid as e: - assert_equal(str(e), "invalid value in set") + assert str(e) == "invalid value in set" else: assert False, "Did not raise Invalid" @@ -1431,7 +1415,7 @@ def test_frozenset_of_integers(): try: schema(frozenset(['abc'])) except MultipleInvalid as e: - assert_equal(str(e), "invalid value in frozenset") + assert str(e) == "invalid value in frozenset" else: assert False, "Did not raise Invalid" @@ -1448,7 +1432,7 @@ def test_set_of_integers_and_strings(): try: schema(set([None])) except MultipleInvalid as e: - assert_equal(str(e), "invalid value in set") + assert str(e) == "invalid value in set" else: assert False, "Did not raise Invalid" @@ -1465,7 +1449,7 @@ def test_frozenset_of_integers_and_strings(): try: schema(frozenset([None])) except MultipleInvalid as e: - assert_equal(str(e), "invalid value in frozenset") + assert str(e) == "invalid value in frozenset" else: assert False, "Did not raise Invalid" @@ -1511,8 +1495,7 @@ def test_any_required(): try: schema({}) except MultipleInvalid as e: - assert_equal(str(e), - "required key not provided @ data['a']") + assert str(e) == "required key not provided @ data['a']" else: assert False, "Did not raise Invalid for MultipleInvalid" @@ -1526,8 +1509,7 @@ def test_any_required_with_subschema(): try: schema({}) except MultipleInvalid as e: - assert_equal(str(e), - "required key not provided @ data['a']") + assert str(e) == "required key not provided @ data['a']" else: assert False, "Did not raise Invalid for MultipleInvalid" @@ -1539,16 +1521,15 @@ def test_inclusive(): }) r = schema({}) - assert_equal(r, {}) + assert r == {} r = schema({'x': 1, 'y': 2}) - assert_equal(r, {'x': 1, 'y': 2}) + assert r == {'x': 1, 'y': 2} try: r = schema({'x': 1}) except MultipleInvalid as e: - assert_equal(str(e), - "some but not all values in the same group of inclusion 'stuff' @ data[]") + assert str(e) == "some but not all values in the same group of inclusion 'stuff' @ data[]" else: assert False, "Did not raise Invalid for incomplete Inclusive group" @@ -1560,13 +1541,12 @@ def test_inclusive_defaults(): }) r = schema({}) - assert_equal(r, {'x': 3, 'y': 4}) + assert r == {'x': 3, 'y': 4} try: r = schema({'x': 1}) except MultipleInvalid as e: - assert_equal(str(e), - "some but not all values in the same group of inclusion 'stuff' @ data[]") + assert str(e) == "some but not all values in the same group of inclusion 'stuff' @ data[]" else: assert False, "Did not raise Invalid for incomplete Inclusive group with defaults" @@ -1578,16 +1558,15 @@ def test_exclusive(): }) r = schema({}) - assert_equal(r, {}) + assert r == {} r = schema({'x': 1}) - assert_equal(r, {'x': 1}) + assert r == {'x': 1} try: r = schema({'x': 1, 'y': 2}) except MultipleInvalid as e: - assert_equal(str(e), - "two or more values in the same group of exclusion 'stuff' @ data[]") + assert str(e) == "two or more values in the same group of exclusion 'stuff' @ data[]" else: assert False, "Did not raise Invalid for multiple values in Exclusive group" @@ -1613,7 +1592,7 @@ def test_any_with_discriminant(): } }) except MultipleInvalid as e: - assert_equal(str(e), 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']') + assert str(e) == 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']' else: assert False, "Did not raise correct Invalid" @@ -1642,15 +1621,13 @@ class StringChoice(str, Enum): try: schema(4) except Invalid as e: - assert_equal(str(e), - "expected Choice or one of 1, 2, 3") + assert str(e) == "expected Choice or one of 1, 2, 3" else: assert False, "Did not raise Invalid for String" try: string_schema("hello") except Invalid as e: - assert_equal(str(e), - "expected StringChoice or one of 'easy', 'medium', 'hard'") + assert str(e) == "expected StringChoice or one of 'easy', 'medium', 'hard'" else: assert False, "Did not raise Invalid for String" From b4584ee4c20821e02b61e09b74d1044a2d3b0c6a Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 3 Apr 2022 12:07:36 +0200 Subject: [PATCH 211/261] Extend README to include coverage run commands + unpin `setupstools` (#465) --- README.md | 9 +++++++-- tox.ini | 1 - 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 21a4a61d..e7cec850 100644 --- a/README.md +++ b/README.md @@ -715,10 +715,15 @@ s({'password':'123', 'password_again': 1337}) ## Running tests -Voluptuous is using pytest: +Voluptuous is using `pytest`: - $ pytest + pip install pytest + pytest +To also include a coverage report: + + pip install pytest pytest-cov coverage>=3.0 + pytest --cov=voluptuous voluptuous/tests/ ## Other libraries and inspirations diff --git a/tox.ini b/tox.ini index 683c5b77..e09e2224 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,6 @@ exclude = .tox,.venv,build,*.egg [testenv] distribute = True sitepackages = False -setuptools_version = setuptools<58.0 deps = pytest pytest-cov From ea4d8bec81e8bfb8e66cb8127dc68bca9fda5da0 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 3 Apr 2022 12:26:27 +0200 Subject: [PATCH 212/261] Style and warning cleanups in README (#466) --- README.md | 68 +++++++++++++++---------------------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index e7cec850..08b66c7e 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ YAML, etc. It has three goals: -1. Simplicity. -2. Support for complex data structures. -3. Provide useful error messages. +1. Simplicity. +2. Support for complex data structures. +3. Provide useful error messages. ## Contact @@ -71,7 +71,7 @@ values. Callables are called to validate. Simple. 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' ``` @@ -84,7 +84,6 @@ To validate this we might use a schema like: ... 'per_page': int, ... 'page': int, ... }) - ``` This schema very succinctly and roughly describes the data required by @@ -101,7 +100,6 @@ schema will need to be more thoroughly defined: ... 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 @@ -118,7 +116,6 @@ documentation, and goes a little further for completeness. ... exc = e >>> str(exc) == "required key not provided @ data['q']" True - ``` ...must be a string: @@ -131,7 +128,6 @@ True ... exc = e >>> str(exc) == "expected str for dictionary value @ data['q']" True - ``` ...and must be at least one character in length: @@ -146,7 +142,6 @@ True True >>> schema({'q': '#topic'}) == {'q': '#topic', 'per_page': 5} True - ``` "per\_page" is a positive integer no greater than 20: @@ -166,7 +161,6 @@ True ... exc = e >>> str(exc) == "value must be at least 1 for dictionary value @ data['per_page']" True - ``` "page" is an integer \>= 0: @@ -181,7 +175,6 @@ True "expected int for dictionary value @ data['per_page']" >>> schema({'q': '#topic', 'page': 1}) == {'q': '#topic', 'page': 1, 'per_page': 5} True - ``` ## Defining schemas @@ -201,7 +194,6 @@ Literals in the schema are matched using normal equality checks: >>> schema = Schema('a string') >>> schema('a string') 'a string' - ``` ### Types @@ -220,7 +212,6 @@ is an instance of the type: ... exc = e >>> str(exc) == "expected int" True - ``` ### URLs @@ -239,7 +230,6 @@ URLs in the schema are matched by using `urlparse` library. ... exc = e >>> str(exc) == "expected a URL" True - ``` ### Lists @@ -255,7 +245,6 @@ in the schema list is compared to each value in the input data: [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 @@ -277,7 +266,6 @@ True [] >>> schema([1, 2]) [1, 2] - ``` ### Sets and frozensets @@ -310,7 +298,6 @@ True ... exc = e >>> str(exc) == 'expected a frozenset' True - ``` However, an empty set (`set()`) is treated as is. If you want to specify a set @@ -330,7 +317,6 @@ True >>> schema = Schema(set) >>> schema({1, 2}) == {1, 2} True - ``` ### Validation functions @@ -350,7 +336,6 @@ validator: >>> from datetime import datetime >>> def Date(fmt='%Y-%m-%d'): ... return lambda v: datetime.strptime(v, fmt) - ``` ```pycon @@ -364,7 +349,6 @@ datetime.datetime(2013, 3, 3, 0, 0) ... exc = e >>> str(exc) == "not a valid value" True - ``` In addition to simply determining if a value is valid, validators may @@ -385,7 +369,6 @@ def Coerce(type, msg=None): except ValueError: raise Invalid(msg or ('expected %s' % type.__name__)) return f - ``` This example also shows a common idiom where an optional human-readable @@ -401,7 +384,6 @@ key-value pair in the corresponding data dictionary: >>> schema = Schema({1: 'one', 2: 'two'}) >>> schema({1: 'one'}) {1: 'one'} - ``` #### Extra dictionary keys @@ -418,7 +400,6 @@ trigger exceptions: ... exc = e >>> str(exc) == "extra keys not allowed @ data[1]" True - ``` This behaviour can be altered on a per-schema basis. To allow @@ -430,7 +411,6 @@ additional keys use >>> schema = Schema({2: 3}, extra=ALLOW_EXTRA) >>> schema({1: 2, 2: 3}) {1: 2, 2: 3} - ``` To remove additional keys use @@ -441,7 +421,6 @@ To remove additional keys use >>> 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 @@ -452,7 +431,6 @@ token `extra` as a key: >>> schema = Schema({1: {Extra: object}}) >>> schema({1: {'foo': 'bar'}}) {1: {'foo': 'bar'}} - ``` #### Required dictionary keys @@ -463,7 +441,6 @@ By default, keys in the schema are not required to be in the data: >>> schema = Schema({1: 2, 3: 4}) >>> schema({3: 4}) {3: 4} - ``` Similarly to how extra\_ keys work, this behaviour can be overridden @@ -478,7 +455,6 @@ per-schema: ... exc = e >>> str(exc) == "required key not provided @ data[1]" True - ``` And per-key, with the marker token `Required(key)`: @@ -494,7 +470,6 @@ And per-key, with the marker token `Required(key)`: True >>> schema({1: 2}) {1: 2} - ``` #### Optional dictionary keys @@ -521,13 +496,11 @@ True ... exc = e >>> str(exc) == "extra keys not allowed @ data[4]" True - ``` ```pycon >>> schema({1: 2, 3: 4}) {1: 2, 3: 4} - ``` ### Recursive / nested schema @@ -539,7 +512,6 @@ You can use `voluptuous.Self` to define a nested schema: >>> recursive = Schema({"more": Self, "value": int}) >>> recursive({"more": {"value": 42}, "value": 41}) == {'more': {'value': 42}, 'value': 41} True - ``` ### Extending an existing Schema @@ -554,7 +526,6 @@ requirements. In that case you can use `Schema.extend` to create a new >>> person_with_age = person.extend({'age': int}) >>> sorted(list(person_with_age.schema.keys())) ['age', 'name'] - ``` The original `Schema` remains unchanged. @@ -575,7 +546,6 @@ attribute-value pair in the corresponding object: >>> schema = Schema(Object({'q': 'one'}, cls=Structure)) >>> schema(Structure(q='one')) - ``` ### Allow None values @@ -589,7 +559,6 @@ To allow value to be None as well, use Any: >>> schema(None) >>> schema(5) 5 - ``` ## Error reporting @@ -605,7 +574,6 @@ 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.""" @@ -626,7 +594,6 @@ an HTTP API. 'This email is invalid.' >>> exc.error_message 'This email is invalid.' - ``` The `path` attribute is used during error reporting, but also during matching @@ -641,7 +608,6 @@ 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 @@ -658,7 +624,6 @@ backtracking is attempted: ... 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 @@ -668,7 +633,6 @@ to the second element in the schema, and succeed: ```pycon >>> schema([6]) [6] - ``` ## Multi-field validation @@ -685,18 +649,18 @@ def passwords_must_match(passwords): raise Invalid('passwords must match') return passwords -s=Schema(All( +schema = Schema(All( # First "pass" for field types - {'password':str, 'password_again':str}, + {'password': str, 'password_again': str}, # Follow up the first "pass" with your multi-field rules passwords_must_match )) # valid -s({'password':'123', 'password_again':'123'}) +schema({'password': '123', 'password_again': '123'}) # raises MultipleInvalid: passwords must match -s({'password':'123', 'password_again':'and now for something completely different'}) +schema({'password': '123', 'password_again': 'and now for something completely different'}) ``` @@ -707,23 +671,27 @@ 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 -s({'password':'123', 'password_again': 1337}) +schema({'password': '123', 'password_again': 1337}) ``` ## Running tests Voluptuous is using `pytest`: - pip install pytest - pytest +```bash +$ pip install pytest +$ pytest +``` To also include a coverage report: - pip install pytest pytest-cov coverage>=3.0 - pytest --cov=voluptuous voluptuous/tests/ +```bash +$ pip install pytest pytest-cov coverage>=3.0 +$ pytest --cov=voluptuous voluptuous/tests/ +``` ## Other libraries and inspirations From 51751cab05cc0d08428f3add6502f58968701e8f Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Sun, 3 Apr 2022 22:49:45 +0200 Subject: [PATCH 213/261] Add tests for `Object` and `MultipleInvalid` (#467) --- voluptuous/schema_builder.py | 3 -- voluptuous/tests/tests.py | 55 +++++++++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index d308bd3e..29766dba 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -974,9 +974,6 @@ def __repr__(self): return self.__str__() -# Markers.py - - class Marker(object): """Mark nodes for special treatment.""" diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index c792f077..1ac99d3c 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,22 +1,24 @@ -import copy import collections +import copy + try: from enum import Enum except ImportError: Enum = None import os import sys -import pytest -from voluptuous import ( - Schema, Required, Exclusive, Inclusive, Optional, Extra, Invalid, In, Remove, - Literal, Url, MultipleInvalid, LiteralInvalid, TypeInvalid, NotIn, Match, Email, - Replace, Range, Coerce, All, Any, Length, FqdnUrl, ALLOW_EXTRA, PREVENT_EXTRA, - validate, ExactSequence, Equal, Unordered, Number, Maybe, Datetime, Date, - Contains, Marker, IsDir, IsFile, PathExists, SomeOf, TooManyValid, Self, - raises, Union, Clamp) +import pytest +from voluptuous import (ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Clamp, Coerce, + Contains, Date, Datetime, Email, Equal, ExactSequence, + Exclusive, Extra, FqdnUrl, In, Inclusive, Invalid, + IsDir, IsFile, Length, Literal, LiteralInvalid, Marker, + Match, Maybe, MultipleInvalid, NotIn, Number, Object, + Optional, PathExists, Range, Remove, Replace, Required, + Schema, Self, SomeOf, TooManyValid, TypeInvalid, Union, + Unordered, Url, raises, validate) from voluptuous.humanize import humanize_error -from voluptuous.util import u, Capitalize, Lower, Strip, Title, Upper +from voluptuous.util import Capitalize, Lower, Strip, Title, Upper, u def test_new_required_test(): @@ -1631,3 +1633,36 @@ class StringChoice(str, Enum): assert str(e) == "expected StringChoice or one of 'easy', 'medium', 'hard'" else: assert False, "Did not raise Invalid for String" + + +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) + + +# Python 3.7 removed the trainling comma in repr() of BaseException +# https://bugs.python.org/issue30399 +if sys.version_info >= (3, 7): + invalid_scalar_excp_repr = "ScalarInvalid('not a valid value')" +else: + invalid_scalar_excp_repr = "ScalarInvalid('not a valid value',)" + + +def test_exception(): + s = Schema(None) + try: + s(123) + except MultipleInvalid as e: + assert repr(e) == "MultipleInvalid([{}])".format(invalid_scalar_excp_repr) + assert str(e.msg) == "not a valid value" + assert str(e.error_message) == "not a valid value" + assert str(e.errors) == "[{}]".format(invalid_scalar_excp_repr) + e.add("Test Error") + assert str(e.errors) == "[{}, 'Test Error']".format(invalid_scalar_excp_repr) From 2466ee2ffe901cd3df3f8675c1f21b4786b5b58e Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Thu, 7 Apr 2022 13:58:15 +0200 Subject: [PATCH 214/261] Release 0.13.1 (#469) --- CHANGELOG.md | 19 +++++++++++++++++-- setup.py | 1 + voluptuous/__init__.py | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab925f46..0f550b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,30 @@ # Changelog +## [0.13.1] + +**Fixes**: + +- [#439](https://github.com/alecthomas/voluptuous/pull/454): Ignore `Enum` if it is unavailable +- [#456](https://github.com/alecthomas/voluptuous/pull/456): Fix email regex match for Python 2.7 + +**New**: + +- [#457](https://github.com/alecthomas/voluptuous/pull/457): Enable github actions +- [#462](https://github.com/alecthomas/voluptuous/pull/462): Convert codebase to adhere to `flake8` W504 (PEP 8) +- [#459](https://github.com/alecthomas/voluptuous/pull/459): Enable `flake8` in github actions +- [#464](https://github.com/alecthomas/voluptuous/pull/464): `pytest` migration + enable Python 3.10 + ## [0.13.0] **Changes**: -- [#450](https://github.com/alecthomas/voluptuous/pull/450): Display valid Enum values in Coerce +- [#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 + +- [#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] diff --git a/setup.py b/setup.py index 05791619..8bb523ff 100644 --- a/setup.py +++ b/setup.py @@ -36,5 +36,6 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ] ) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 6f9e20b1..de178d0f 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.13.0' +__version__ = '0.13.1' __author__ = 'alecthomas' From 41184fef85909c7500db44c5a254cd61425840c7 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 13 Jul 2022 22:30:11 +1000 Subject: [PATCH 215/261] Fix a few typos (#470) --- voluptuous/schema_builder.py | 2 +- voluptuous/tests/tests.py | 4 ++-- voluptuous/validators.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 29766dba..0cb207fb 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -866,7 +866,7 @@ def item_priority(item_): for i, check_ in priority: if check_(key_): return i - # values have hightest priorities + # values have highest priorities return 0 return item_priority diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 1ac99d3c..58512df4 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -115,7 +115,7 @@ def test_remove(): # remove keys by type schema = Schema({"weight": float, "amount": int, - # remvove str keys with int values + # remove str keys with int values Remove(str): int, # keep str keys with str values str: str}) @@ -1647,7 +1647,7 @@ def test_object(): pytest.raises(MultipleInvalid, s, 345) -# Python 3.7 removed the trainling comma in repr() of BaseException +# Python 3.7 removed the trailing comma in repr() of BaseException # https://bugs.python.org/issue30399 if sys.version_info >= (3, 7): invalid_scalar_excp_repr = "ScalarInvalid('not a valid value')" diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 9019f1f7..35a80e66 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -689,7 +689,7 @@ def __call__(self, v): self.msg or 'length of value must be at most %s' % self.max) return v - # Objects that havbe no length e.g. None or strings will raise TypeError + # Objects that have no length e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid( self.msg or 'invalid value or type') @@ -845,7 +845,7 @@ def __repr__(self): class Unique(object): """Ensure an iterable does not contain duplicate items. - Only iterables convertable to a set are supported (native types and + Only iterables convertible to a set are supported (native types and objects with correct __eq__). JSON does not support set, so they need to be presented as arrays. From 1105b31432ac5ebb3b0e3238be94b4231c427c67 Mon Sep 17 00:00:00 2001 From: elprimato Date: Sun, 23 Oct 2022 22:50:06 +0200 Subject: [PATCH 216/261] Change to SPDX conform license string (#472) Co-authored-by: Volker Schaus --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8bb523ff..be6b6b3a 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ description=description, long_description=long_description, long_description_content_type='text/markdown', - license='BSD', + license='BSD-3-Clause', platforms=['any'], packages=['voluptuous'], author='Alec Thomas', From bd2f9adc7b38ba28b93bf00fdbd931843a48300c Mon Sep 17 00:00:00 2001 From: DS/Charlie <82801887+ds-cbo@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:41:18 +0100 Subject: [PATCH 217/261] add typing information (#475) --- .github/workflows/tests.yml | 7 --- setup.py | 3 ++ tox.ini | 2 +- voluptuous/error.py | 26 +++++----- voluptuous/humanize.py | 11 +++-- voluptuous/py.typed | 0 voluptuous/schema_builder.py | 49 +++++++++++-------- voluptuous/tests/tests.py | 27 ++++++----- voluptuous/util.py | 33 +++++++------ voluptuous/validators.py | 94 ++++++++++++++++++++++-------------- 10 files changed, 147 insertions(+), 105 deletions(-) create mode 100644 voluptuous/py.typed diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b9413f8..7d28ec44 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,8 +19,6 @@ jobs: - { python-version: "3.9", session: "py39" } - { python-version: "3.8", session: "py38" } - { python-version: "3.7", session: "py37" } - - { python-version: "3.6", session: "py36" } - - { python-version: "2.7", session: "py27" } steps: - name: Check out the repository @@ -31,11 +29,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install tox-setuptools-version - if: ${{ matrix.session != 'py27' }} - run: | - pip install tox-setuptools-version - - name: Run tox run: | pip install tox diff --git a/setup.py b/setup.py index be6b6b3a..cd0254fd 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,9 @@ license='BSD-3-Clause', platforms=['any'], packages=['voluptuous'], + package_data={ + 'voluptuous': ['py.typed'], + }, author='Alec Thomas', author_email='alec@swapoff.org', classifiers=[ diff --git a/tox.ini b/tox.ini index e09e2224..25d2eb8a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py27,py36,py37,py38,py39,py310 +envlist = flake8,py37,py38,py39,py310 [flake8] ; E501: line too long (X > 79 characters) diff --git a/voluptuous/error.py b/voluptuous/error.py index 97f37d2c..35d392e5 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -1,3 +1,5 @@ +import typing + class Error(Exception): """Base validation exception.""" @@ -17,17 +19,17 @@ class Invalid(Error): """ - def __init__(self, message, path=None, error_message=None, error_type=None): + def __init__(self, message: str, path: typing.Optional[typing.List[str]] = 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): + def msg(self) -> str: return self.args[0] - def __str__(self): + def __str__(self) -> str: path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \ if self.path else '' output = Exception.__str__(self) @@ -35,36 +37,36 @@ def __str__(self): output += ' for ' + self.error_type return output + path - def prepend(self, path): + def prepend(self, path: typing.List[str]) -> None: self.path = path + self.path class MultipleInvalid(Invalid): - def __init__(self, errors=None): + def __init__(self, errors: typing.Optional[typing.List[Invalid]] = None) -> None: self.errors = errors[:] if errors else [] - def __repr__(self): + def __repr__(self) -> str: return 'MultipleInvalid(%r)' % self.errors @property - def msg(self): + def msg(self) -> str: return self.errors[0].msg @property - def path(self): + def path(self) -> typing.List[str]: return self.errors[0].path @property - def error_message(self): + def error_message(self) -> str: return self.errors[0].error_message - def add(self, error): + def add(self, error: Invalid) -> None: self.errors.append(error) - def __str__(self): + def __str__(self) -> str: return str(self.errors[0]) - def prepend(self, path): + def prepend(self, path: typing.List[str]) -> None: for error in self.errors: error.prepend(path) diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py index 91ab2015..734f3672 100644 --- a/voluptuous/humanize.py +++ b/voluptuous/humanize.py @@ -1,11 +1,16 @@ from voluptuous import Invalid, MultipleInvalid from voluptuous.error import Error +from voluptuous.schema_builder import Schema +import typing MAX_VALIDATION_ERROR_ITEM_LENGTH = 500 -def _nested_getitem(data, path): +IndexT = typing.TypeVar("IndexT") + + +def _nested_getitem(data: typing.Dict[IndexT, typing.Any], path: typing.List[IndexT]) -> typing.Optional[typing.Any]: for item_index in path: try: data = data[item_index] @@ -16,7 +21,7 @@ def _nested_getitem(data, path): return data -def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH): +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. @@ -33,7 +38,7 @@ def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_E return '%s. Got %s' % (validation_error, offending_item_summary) -def validate_with_humanized_errors(data, schema, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH): +def validate_with_humanized_errors(data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH) -> typing.Any: try: return schema(data) except (Invalid, MultipleInvalid) as e: diff --git a/voluptuous/py.typed b/voluptuous/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 0cb207fb..e7e8d353 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import inspect import re @@ -7,6 +9,9 @@ import itertools from voluptuous import error as er +from collections.abc import Generator +import typing +from voluptuous.error import Error if sys.version_info >= (3,): long = int @@ -127,18 +132,21 @@ def __repr__(self): UNDEFINED = Undefined() -def Self(): +def Self() -> None: raise er.SchemaError('"Self" should never be called') -def default_factory(value): +DefaultFactory = typing.Union[Undefined, typing.Callable[[], typing.Any]] + + +def default_factory(value) -> DefaultFactory: if value is UNDEFINED or callable(value): return value return lambda: value @contextmanager -def raises(exc, msg=None, regex=None): +def raises(exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None) -> Generator[None, None, None]: try: yield except exc as e: @@ -148,7 +156,7 @@ def raises(exc, msg=None, regex=None): assert re.search(regex, str(e)), '%r does not match %r' % (str(e), regex) -def Extra(_): +def Extra(_) -> None: """Allow keys in the data that are not present in the schema.""" raise er.SchemaError('"Extra" should never be called') @@ -157,6 +165,8 @@ def Extra(_): # deprecated object, so we just leave an alias here instead. extra = Extra +Schemable = typing.Union[dict, list, type, typing.Callable] + class Schema(object): """A validation schema. @@ -186,7 +196,7 @@ class Schema(object): PREVENT_EXTRA: 'PREVENT_EXTRA', } - def __init__(self, schema, required=False, extra=PREVENT_EXTRA): + def __init__(self, schema: Schemable, required: bool = False, extra: int = PREVENT_EXTRA) -> None: """Create a new Schema. :param schema: Validation schema. See :module:`voluptuous` for details. @@ -207,7 +217,7 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA): self._compiled = self._compile(schema) @classmethod - def infer(cls, data, **kwargs): + def infer(cls, data, **kwargs) -> Schema: """Create a Schema from concrete data (e.g. an API response). For example, this will take a dict like: @@ -723,7 +733,7 @@ def validate_set(path, data): return validate_set - def extend(self, schema, required=None, extra=None): + def extend(self, schema: dict, 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 @@ -738,6 +748,7 @@ def extend(self, schema, required=None, extra=None): """ assert type(self.schema) == dict and type(schema) == dict, 'Both schemas must be dictionary-based' + assert isinstance(self.schema, dict) result = self.schema.copy() @@ -936,7 +947,7 @@ class Msg(object): ... assert isinstance(e.errors[0], er.RangeInvalid) """ - def __init__(self, schema, msg, cls=None): + def __init__(self, schema: dict, 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") @@ -961,7 +972,7 @@ def __repr__(self): class Object(dict): """Indicate that we should work with attributes, not keys.""" - def __init__(self, schema, cls=UNDEFINED): + def __init__(self, schema, cls: object = UNDEFINED) -> None: self.cls = cls super(Object, self).__init__(schema) @@ -977,7 +988,7 @@ def __repr__(self): class Marker(object): """Mark nodes for special treatment.""" - def __init__(self, schema_, msg=None, description=None): + def __init__(self, schema_: dict, msg: typing.Optional[str] = None, description: typing.Optional[str] = None) -> None: self.schema = schema_ self._schema = Schema(schema_) self.msg = msg @@ -1009,7 +1020,7 @@ def __eq__(self, other): return self.schema == other def __ne__(self, other): - return not(self.schema == other) + return not (self.schema == other) class Optional(Marker): @@ -1035,7 +1046,7 @@ class Optional(Marker): {'key2': 'value'} """ - def __init__(self, schema, msg=None, default=UNDEFINED, description=None): + def __init__(self, schema: dict, 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) @@ -1077,7 +1088,7 @@ class Exclusive(Optional): ... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}}) """ - def __init__(self, schema, group_of_exclusion, msg=None, description=None): + def __init__(self, schema: dict, 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 @@ -1125,8 +1136,8 @@ class Inclusive(Optional): True """ - def __init__(self, schema, group_of_inclusion, - msg=None, description=None, default=UNDEFINED): + def __init__(self, schema: dict, 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) @@ -1148,7 +1159,7 @@ class Required(Marker): {'key': []} """ - def __init__(self, schema, msg=None, default=UNDEFINED, description=None): + def __init__(self, schema: dict, msg: typing.Optional[str] = None, default=UNDEFINED, description: typing.Optional[str] = None) -> None: super(Required, self).__init__(schema, msg=msg, description=description) self.default = default_factory(default) @@ -1169,7 +1180,7 @@ class Remove(Marker): [1, 2, 3, 5, '7'] """ - def __call__(self, v): + def __call__(self, v: object): super(Remove, self).__call__(v) return self.__class__ @@ -1180,7 +1191,7 @@ def __hash__(self): return object.__hash__(self) -def message(default=None, cls=None): +def message(default: typing.Optional[str] = None, cls: typing.Optional[typing.Type[Error]] = None) -> typing.Callable: """Convenience decorator to allow functions to provide a message. Set a default message: @@ -1251,7 +1262,7 @@ def _merge_args_with_kwargs(args_dict, kwargs_dict): return ret -def validate(*a, **kw): +def validate(*a, **kw) -> typing.Callable: """Decorator for validating arguments of a function against a given schema. Set restrictions for arguments: diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 58512df4..ef4580b7 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,14 +1,5 @@ -import collections -import copy - -try: - from enum import Enum -except ImportError: - Enum = None -import os -import sys - -import pytest +from voluptuous.util import Capitalize, Lower, Strip, Title, Upper, u +from voluptuous.humanize import humanize_error from voluptuous import (ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Clamp, Coerce, Contains, Date, Datetime, Email, Equal, ExactSequence, Exclusive, Extra, FqdnUrl, In, Inclusive, Invalid, @@ -17,8 +8,18 @@ Optional, PathExists, Range, Remove, Replace, Required, Schema, Self, SomeOf, TooManyValid, TypeInvalid, Union, Unordered, Url, raises, validate) -from voluptuous.humanize import humanize_error -from voluptuous.util import Capitalize, Lower, Strip, Title, Upper, u +import pytest +import sys +import os +import collections +import copy +import typing + +Enum: typing.Union[type, None] +try: + from enum import Enum +except ImportError: + Enum = None def test_new_required_test(): diff --git a/voluptuous/util.py b/voluptuous/util.py index f57b8d78..58b91959 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,13 +1,16 @@ import sys -from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid -from voluptuous.schema_builder import Schema, default_factory, raises -from voluptuous import validators +# F401: "imported but unused" +from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid # noqa: F401 +from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401 +from voluptuous import validators # noqa: F401 +from voluptuous.schema_builder import DefaultFactory # noqa: F401 +import typing __author__ = 'tusharmakkar08' -def _fix_str(v): +def _fix_str(v: str) -> str: if sys.version_info[0] == 2 and isinstance(v, unicode): # noqa: F821 s = v else: @@ -15,7 +18,7 @@ def _fix_str(v): return s -def Lower(v): +def Lower(v: str) -> str: """Transform a string to lower case. >>> s = Schema(Lower) @@ -25,7 +28,7 @@ def Lower(v): return _fix_str(v).lower() -def Upper(v): +def Upper(v: str) -> str: """Transform a string to upper case. >>> s = Schema(Upper) @@ -35,7 +38,7 @@ def Upper(v): return _fix_str(v).upper() -def Capitalize(v): +def Capitalize(v: str) -> str: """Capitalise a string. >>> s = Schema(Capitalize) @@ -45,7 +48,7 @@ def Capitalize(v): return _fix_str(v).capitalize() -def Title(v): +def Title(v: str) -> str: """Title case a string. >>> s = Schema(Title) @@ -55,7 +58,7 @@ def Title(v): return _fix_str(v).title() -def Strip(v): +def Strip(v: str) -> str: """Strip whitespace from a string. >>> s = Schema(Strip) @@ -76,7 +79,7 @@ class DefaultTo(object): [] """ - def __init__(self, default_value, msg=None): + def __init__(self, default_value, msg: typing.Optional[str] = None) -> None: self.default_value = default_factory(default_value) self.msg = msg @@ -99,7 +102,7 @@ class SetTo(object): 42 """ - def __init__(self, value): + def __init__(self, value) -> None: self.value = default_factory(value) def __call__(self, v): @@ -121,7 +124,7 @@ class Set(object): ... s([set([1, 2]), set([3, 4])]) """ - def __init__(self, msg=None): + def __init__(self, msg: typing.Optional[str] = None) -> None: self.msg = msg def __call__(self, v): @@ -137,10 +140,10 @@ def __repr__(self): class Literal(object): - def __init__(self, lit): + def __init__(self, lit) -> None: self.lit = lit - def __call__(self, value, msg=None): + def __call__(self, value, msg: typing.Optional[str] = None): if self.lit != value: raise LiteralInvalid( msg or '%s not match for %s' % (value, self.lit) @@ -155,7 +158,7 @@ def __repr__(self): return repr(self.lit) -def u(x): +def u(x: str) -> str: if sys.version_info < (3,): return unicode(x) # noqa: F821 else: diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 35a80e66..776adb8b 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -1,20 +1,25 @@ +from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, + AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, + RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, + DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, + TooManyValid) + +# F401: flake8 complains about 'raises' not being used, but it is used in doctests +from voluptuous.schema_builder import Schema, raises, message, Schemable # noqa: F401 import os import re import datetime import sys from functools import wraps from decimal import Decimal, InvalidOperation +import typing + +Enum: typing.Union[type, None] try: from enum import Enum except ImportError: Enum = None -from voluptuous.schema_builder import Schema, raises, message -from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, - AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, - RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, - DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, - TooManyValid) if sys.version_info >= (3,): import urllib.parse as urlparse @@ -53,7 +58,7 @@ __author__ = 'tusharmakkar08' -def truth(f): +def truth(f: typing.Callable) -> typing.Callable: """Convenience decorator to convert truth functions into validators. >>> @truth @@ -97,7 +102,7 @@ class Coerce(object): ... validate('foo') """ - def __init__(self, type, msg=None): + def __init__(self, type: type, msg: typing.Optional[str] = None) -> None: self.type = type self.msg = msg self.type_name = type.__name__ @@ -203,13 +208,13 @@ class _WithSubValidators(object): sub-validators are compiled by the parent `Schema`. """ - def __init__(self, *validators, **kwargs): + def __init__(self, *validators, msg=None, required=False, discriminant=None, **kwargs) -> None: self.validators = validators - self.msg = kwargs.pop('msg', None) - self.required = kwargs.pop('required', False) - self.discriminant = kwargs.pop('discriminant', None) + self.msg = msg + self.required = required + self.discriminant = discriminant - def __voluptuous_compile__(self, schema): + def __voluptuous_compile__(self, schema: Schema) -> typing.Callable: self._compiled = [] old_required = schema.required self.schema = schema @@ -219,7 +224,7 @@ def __voluptuous_compile__(self, schema): schema.required = old_required return self._run - def _run(self, path, value): + def _run(self, path: typing.List[str], value): if self.discriminant is not None: self._compiled = [ self.schema._compile(v) @@ -238,6 +243,9 @@ def __repr__(self): self.msg ) + def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[str]] = None): + raise NotImplementedError() + class Any(_WithSubValidators): """Use the first validated value. @@ -379,7 +387,7 @@ class Match(object): '0x123ef4' """ - def __init__(self, pattern, msg=None): + def __init__(self, pattern: typing.Union[re.Pattern, str], msg: typing.Optional[str] = None) -> None: if isinstance(pattern, basestring): pattern = re.compile(pattern) self.pattern = pattern @@ -407,7 +415,7 @@ class Replace(object): 'I say goodbye' """ - def __init__(self, pattern, substitution, msg=None): + def __init__(self, pattern: typing.Union[re.Pattern, str], substitution: str, msg: typing.Optional[str] = None) -> None: if isinstance(pattern, basestring): pattern = re.compile(pattern) self.pattern = pattern @@ -423,7 +431,7 @@ def __repr__(self): self.msg) -def _url_validation(v): +def _url_validation(v: str) -> urlparse.ParseResult: parsed = urlparse.urlparse(v) if not parsed.scheme or not parsed.netloc: raise UrlInvalid("must have a URL scheme and host") @@ -556,7 +564,7 @@ def PathExists(v): raise PathInvalid("Not a Path") -def Maybe(validator, msg=None): +def Maybe(validator: typing.Callable, msg: typing.Optional[str] = None): """Validate that the object matches given validator or is None. :raises Invalid: If the value does not match the given validator and is not @@ -572,6 +580,9 @@ def Maybe(validator, msg=None): return Any(None, validator, msg=msg) +NullableNumber = typing.Union[int, float, None] + + class Range(object): """Limit a value to a range. @@ -593,8 +604,9 @@ class Range(object): ... Schema(Range(max=10, max_included=False))(20) """ - def __init__(self, min=None, max=None, min_included=True, - max_included=True, msg=None): + def __init__(self, min: NullableNumber = None, max: NullableNumber = None, + min_included: bool = True, max_included: bool = True, + msg: typing.Optional[str] = None) -> None: self.min = min self.max = max self.min_included = min_included @@ -649,7 +661,8 @@ class Clamp(object): 0 """ - def __init__(self, min=None, max=None, msg=None): + def __init__(self, min: NullableNumber = None, max: NullableNumber = None, + msg: typing.Optional[str] = None) -> None: self.min = min self.max = max self.msg = msg @@ -674,7 +687,8 @@ def __repr__(self): class Length(object): """The length of a value must be in a certain range.""" - def __init__(self, min=None, max=None, msg=None): + def __init__(self, min: NullableNumber = None, max: NullableNumber = None, + msg: typing.Optional[str] = None) -> None: self.min = min self.max = max self.msg = msg @@ -703,7 +717,7 @@ class Datetime(object): DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - def __init__(self, format=None, msg=None): + def __init__(self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None) -> None: self.format = format or self.DEFAULT_FORMAT self.msg = msg @@ -741,7 +755,7 @@ def __repr__(self): class In(object): """Validate that a value is in a collection.""" - def __init__(self, container, msg=None): + def __init__(self, container: typing.Iterable, msg: typing.Optional[str] = None) -> None: self.container = container self.msg = msg @@ -762,7 +776,7 @@ def __repr__(self): class NotIn(object): """Validate that a value is not in a collection.""" - def __init__(self, container, msg=None): + def __init__(self, container: typing.Iterable, msg: typing.Optional[str] = None) -> None: self.container = container self.msg = msg @@ -790,7 +804,7 @@ class Contains(object): ... s([3, 2]) """ - def __init__(self, item, msg=None): + def __init__(self, item, msg: typing.Optional[str] = None) -> None: self.item = item self.msg = msg @@ -823,9 +837,9 @@ class ExactSequence(object): ('hourly_report', 10, [], []) """ - def __init__(self, validators, **kwargs): + def __init__(self, validators: typing.Iterable[Schemable], msg: typing.Optional[str] = None, **kwargs) -> None: self.validators = validators - self.msg = kwargs.pop('msg', None) + self.msg = msg self._schemas = [Schema(val, **kwargs) for val in validators] def __call__(self, v): @@ -868,7 +882,7 @@ class Unique(object): ... s('aabbc') """ - def __init__(self, msg=None): + def __init__(self, msg: typing.Optional[str] = None) -> None: self.msg = msg def __call__(self, v): @@ -904,7 +918,7 @@ class Equal(object): ... s('foo') """ - def __init__(self, target, msg=None): + def __init__(self, target, msg: typing.Optional[str] = None) -> None: self.target = target self.msg = msg @@ -932,7 +946,8 @@ class Unordered(object): [1, 'foo'] """ - def __init__(self, validators, msg=None, **kwargs): + def __init__(self, validators: typing.Iterable[Schemable], + msg: typing.Optional[str] = None, **kwargs) -> None: self.validators = validators self.msg = msg self._schemas = [Schema(val, **kwargs) for val in validators] @@ -989,7 +1004,8 @@ class Number(object): Decimal('1234.01') """ - def __init__(self, precision=None, scale=None, msg=None, yield_decimal=False): + def __init__(self, precision: typing.Optional[int] = None, scale: typing.Optional[int] = None, + msg: typing.Optional[str] = None, yield_decimal: bool = False) -> None: self.precision = precision self.scale = scale self.msg = msg @@ -1021,7 +1037,7 @@ def __call__(self, v): def __repr__(self): return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg)) - def _get_precision_scale(self, number): + def _get_precision_scale(self, number) -> typing.Tuple[int, int, Decimal]: """ :param number: :return: tuple(precision, scale, decimal_number) @@ -1031,7 +1047,13 @@ def _get_precision_scale(self, number): except InvalidOperation: raise Invalid(self.msg or 'Value must be a number enclosed with string') - return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) + exp = decimal_num.as_tuple().exponent + if isinstance(exp, int): + return (len(decimal_num.as_tuple().digits), -exp, decimal_num) + else: + # TODO: handle infinity and NaN + # raise Invalid(self.msg or 'Value has no precision') + raise TypeError("infinity and NaN have no precision") class SomeOf(_WithSubValidators): @@ -1058,7 +1080,9 @@ class SomeOf(_WithSubValidators): ... validate(6.2) """ - def __init__(self, validators, min_valid=None, max_valid=None, **kwargs): + 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 From ecb0cdc27ca4859152cdc2bb85e81cb194447add Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Mon, 13 Feb 2023 17:56:05 +0100 Subject: [PATCH 218/261] Set static PyPI project description (#476) --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index cd0254fd..e7e19c00 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ with io.open('README.md', encoding='utf-8') as f: long_description = f.read() - description = long_description.splitlines()[0].strip() setup( @@ -16,7 +15,7 @@ url='https://github.com/alecthomas/voluptuous', download_url='https://pypi.python.org/pypi/voluptuous', version=version, - description=description, + description='Python data validation library', long_description=long_description, long_description_content_type='text/markdown', license='BSD-3-Clause', From 72641b83317cf91a1fc264c2b08ee31519c22b34 Mon Sep 17 00:00:00 2001 From: DS/Charlie <82801887+ds-cbo@users.noreply.github.com> Date: Wed, 22 Feb 2023 20:29:06 +0100 Subject: [PATCH 219/261] Fix type hint of schemas, for example for Required('key') (#478) --- voluptuous/schema_builder.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index e7e8d353..946cefd6 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -118,9 +118,6 @@ def _isnamedtuple(obj): return isinstance(obj, tuple) and hasattr(obj, '_fields') -primitive_types = (str, unicode, bool, int, float) - - class Undefined(object): def __nonzero__(self): return False @@ -165,7 +162,15 @@ def Extra(_) -> None: # deprecated object, so we just leave an alias here instead. extra = Extra -Schemable = typing.Union[dict, list, type, typing.Callable] +primitive_types = (bool, bytes, int, long, str, unicode, float, complex) + +Schemable = typing.Union[ + Extra, 'Schema', 'Object', + _Mapping, + list, tuple, frozenset, set, + bool, bytes, int, long, str, unicode, float, complex, + type, object, dict, type(None), typing.Callable +] class Schema(object): @@ -306,8 +311,7 @@ def _compile(self, schema): type_ = type(schema) if inspect.isclass(schema): type_ = schema - if type_ in (bool, bytes, int, long, str, unicode, float, complex, object, - list, dict, type(None)) or callable(schema): + if type_ in (*primitive_types, object, list, dict, type(None)) or callable(schema): return _compile_scalar(schema) raise er.SchemaError('unsupported schema data type %r' % type(schema).__name__) @@ -733,7 +737,7 @@ def validate_set(path, data): return validate_set - def extend(self, schema: dict, required: typing.Optional[bool] = None, extra: typing.Optional[int] = None) -> Schema: + 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 @@ -947,7 +951,7 @@ class Msg(object): ... assert isinstance(e.errors[0], er.RangeInvalid) """ - def __init__(self, schema: dict, msg: str, cls: typing.Optional[typing.Type[Error]] = None) -> None: + def __init__(self, schema: Schemable, msg: str, cls: typing.Optional[typing.Type[Error]] = None) -> None: if cls and not issubclass(cls, er.Invalid): raise er.SchemaError("Msg can only use subclases of" " Invalid as custom class") @@ -972,7 +976,7 @@ def __repr__(self): class Object(dict): """Indicate that we should work with attributes, not keys.""" - def __init__(self, schema, cls: object = UNDEFINED) -> None: + def __init__(self, schema: typing.Any, cls: object = UNDEFINED) -> None: self.cls = cls super(Object, self).__init__(schema) @@ -988,7 +992,7 @@ def __repr__(self): class Marker(object): """Mark nodes for special treatment.""" - def __init__(self, schema_: dict, msg: typing.Optional[str] = None, description: typing.Optional[str] = None) -> None: + 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 @@ -1046,7 +1050,7 @@ class Optional(Marker): {'key2': 'value'} """ - def __init__(self, schema: dict, msg: typing.Optional[str] = None, default=UNDEFINED, description: typing.Optional[str] = None) -> None: + 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) @@ -1088,7 +1092,7 @@ class Exclusive(Optional): ... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}}) """ - def __init__(self, schema: dict, group_of_exclusion: str, msg: typing.Optional[str] = None, description: typing.Optional[str] = None) -> None: + 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 @@ -1136,7 +1140,7 @@ class Inclusive(Optional): True """ - def __init__(self, schema: dict, group_of_inclusion: str, + 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, @@ -1159,7 +1163,7 @@ class Required(Marker): {'key': []} """ - def __init__(self, schema: dict, msg: typing.Optional[str] = None, default=UNDEFINED, description: typing.Optional[str] = None) -> None: + 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) @@ -1180,8 +1184,8 @@ class Remove(Marker): [1, 2, 3, 5, '7'] """ - def __call__(self, v: object): - super(Remove, self).__call__(v) + def __call__(self, schema: Schemable): + super(Remove, self).__call__(schema) return self.__class__ def __repr__(self): From 41bc53df12c078a2fc9ea586a280605c53db5ea1 Mon Sep 17 00:00:00 2001 From: Ken Kundert Date: Tue, 13 Jun 2023 04:54:28 -0700 Subject: [PATCH 220/261] Allow error reporting on keys (#479) --- voluptuous/schema_builder.py | 6 +++-- voluptuous/tests/tests.py | 43 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 946cefd6..0dbec453 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -372,7 +372,6 @@ def validate_mapping(path, iterable, out): # key, insert it. key_value_map[key.schema] = key.default() - error = None errors = [] for key, value in key_value_map.items(): key_path = path + [key] @@ -383,6 +382,7 @@ def validate_mapping(path, iterable, out): # 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) @@ -430,7 +430,9 @@ def validate_mapping(path, iterable, out): break else: - if remove_key: + if error: + errors.append(error) + elif remove_key: # remove key continue elif self.extra == ALLOW_EXTRA: diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index ef4580b7..35052790 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1600,6 +1600,49 @@ def test_any_with_discriminant(): assert False, "Did not raise correct Invalid" +def test_key1(): + def as_int(a): + return int(a) + + schema = Schema({as_int: str}) + try: + schema({ + '1': 'one', + 'two': '2', + '3': 'three', + 'four': '4', + }) + except MultipleInvalid as e: + assert len(e.errors) == 2 + assert str(e.errors[0]) == "not a valid value @ data['two']" + assert str(e.errors[1]) == "not a valid value @ data['four']" + else: + assert False, "Did not raise correct Invalid" + + +def test_key2(): + def as_int(a): + try: + return int(a) + except ValueError: + raise Invalid('expecting a number') + + schema = Schema({as_int: str}) + try: + schema({ + '1': 'one', + 'two': '2', + '3': 'three', + 'four': '4', + }) + except MultipleInvalid as e: + assert len(e.errors) == 2 + assert str(e.errors[0]) == "expecting a number @ data['two']" + assert str(e.errors[1]) == "expecting a number @ data['four']" + else: + assert False, "Did not raise correct Invalid" + + if Enum: def test_coerce_enum(): """Test Coerce Enum""" From 9405620b999cd5d4de197ad38dad6a645353493d Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Fri, 23 Jun 2023 11:38:56 +0200 Subject: [PATCH 221/261] Remove Travis build status badge (#482) --- README.md | 16 +++++++++-- update_documentation.sh | 61 ----------------------------------------- 2 files changed, 14 insertions(+), 63 deletions(-) delete mode 100644 update_documentation.sh diff --git a/README.md b/README.md index 08b66c7e..3c571e84 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ [![image](https://img.shields.io/pypi/v/voluptuous.svg)](https://python.org/pypi/voluptuous) [![image](https://img.shields.io/pypi/l/voluptuous.svg)](https://python.org/pypi/voluptuous) [![image](https://img.shields.io/pypi/pyversions/voluptuous.svg)](https://python.org/pypi/voluptuous) -[![Build Status](https://travis-ci.org/alecthomas/voluptuous.svg)](https://travis-ci.org/alecthomas/voluptuous) -[![Coverage Status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) [![Gitter chat](https://badges.gitter.im/alecthomas.svg)](https://gitter.im/alecthomas/Lobby) +[![Test status](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml/badge.svg)](https://github.com/alecthomas/voluptuous/actions/workflows/tests.yml) +[![Coverage status](https://coveralls.io/repos/github/alecthomas/voluptuous/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/voluptuous?branch=master) +[![Gitter chat](https://badges.gitter.im/alecthomas.svg)](https://gitter.im/alecthomas/Lobby) Voluptuous, *despite* the name, is a Python data validation library. It is primarily intended for validating data coming into Python as JSON, @@ -40,6 +41,16 @@ To file a bug, create a [new issue](https://github.com/alecthomas/voluptuous/iss The documentation is provided [here](http://alecthomas.github.io/voluptuous/). +## Contribution to Documentation + +Documentation is built using `Sphinx`. You can install it by + + pip install -r requirements.txt + +For building `sphinx-apidoc` from scratch you need to set PYTHONPATH to `voluptuous/voluptuous` repository. + +The documentation is provided [here.](http://alecthomas.github.io/voluptuous/) + ## Changelog See [CHANGELOG.md](https://github.com/alecthomas/voluptuous/blob/master/CHANGELOG.md). @@ -706,3 +717,4 @@ using voluptuous validators in `assert`s. I greatly prefer the light-weight style promoted by these libraries to the complexity of libraries like FormEncode. + diff --git a/update_documentation.sh b/update_documentation.sh deleted file mode 100644 index 76518354..00000000 --- a/update_documentation.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env bash - -# Merge pushes to development branch to stable branch -if [ ! -n $2 ] ; then - echo "Usage: merge.sh " - exit 1; -fi - -GIT_USER="$1" -GIT_PASS="$2" - -# Specify the development branch and stable branch names -FROM_BRANCH="master" -TO_BRANCH="gh-pages" - -# Needed for setting identity -git config --global user.email "tusharmakkar08@gmail.com" -git config --global user.name "Tushar Makkar" -git config --global push.default "simple" - -# Get the current branch -export PAGER=cat -CURRENT_BRANCH=$(git log -n 1 --pretty=%d HEAD | cut -d"," -f3 | cut -d" " -f2 | cut -d")" -f1) -echo "current branch is '$CURRENT_BRANCH'" - -# Create the URL to push merge to -URL=$(git remote -v | head -n1 | cut -f2 | cut -d" " -f1) -echo "Repo url is $URL" -PUSH_URL="https://$GIT_USER:$GIT_PASS@${URL:8}" - -git remote set-url origin ${PUSH_URL} - -echo "Checking out $FROM_BRANCH..." && \ -git fetch origin ${FROM_BRANCH}:${FROM_BRANCH} && \ -git checkout ${FROM_BRANCH} - - -echo "Checking out $TO_BRANCH..." && \ -# Checkout the latest stable -git fetch origin ${TO_BRANCH}:${TO_BRANCH} && \ -git checkout ${TO_BRANCH} && \ - -# Merge the dev into latest stable -echo "Merging changes..." && \ -git merge ${FROM_BRANCH} && \ - -# Push changes back to remote vcs -echo "Pushing changes..." && \ -git push origin gh-pages &> /dev/null && \ -echo "Merge complete!" || \ -echo "Error Occurred. Merge failed" - -export PYTHONPATH=${PYTHONPATH}:$(pwd):$(pwd)/voluptuous - -pip install -r requirements.txt && sphinx-apidoc -o docs -f voluptuous && -cd docs && make html || -echo "Sphinx not able to generate HTML" - -git status && git add . && -git commit -m "Auto updating documentation from $CURRENT_BRANCH" && -git push origin gh-pages &> /dev/null && echo "Documentation pushed" From d499a4115d23b9ba7bb70ec81fa961a4ce7abc15 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Tue, 4 Jul 2023 21:52:18 +1000 Subject: [PATCH 222/261] Update contributions-only.md --- .github/ISSUE_TEMPLATE/contributions-only.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/contributions-only.md b/.github/ISSUE_TEMPLATE/contributions-only.md index cca06c35..b3e10453 100644 --- a/.github/ISSUE_TEMPLATE/contributions-only.md +++ b/.github/ISSUE_TEMPLATE/contributions-only.md @@ -6,7 +6,7 @@ labels: '' assignees: '' --- - + From 5599057300ddd1e19afe5919efae07b9c6cf57ac Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Sun, 12 Nov 2023 00:13:16 +0000 Subject: [PATCH 223/261] Fix type hints and enable mypy (#486) --- .github/workflows/tests.yml | 1 + tox.ini | 6 ++++++ voluptuous/error.py | 14 +++++++++++--- voluptuous/schema_builder.py | 9 ++++----- voluptuous/tests/tests.py | 6 +++--- 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7d28ec44..768188a4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,7 @@ jobs: matrix: include: - { python-version: "3.10", session: "flake8" } + - { python-version: "3.10", session: "mypy" } - { python-version: "3.10", session: "py310" } - { python-version: "3.9", session: "py39" } - { python-version: "3.8", session: "py38" } diff --git a/tox.ini b/tox.ini index 25d2eb8a..b3681a29 100644 --- a/tox.ini +++ b/tox.ini @@ -22,3 +22,9 @@ commands = [testenv:flake8] deps = flake8 commands = flake8 --doctests setup.py voluptuous + +[testenv:mypy] +deps = + mypy + pytest +commands = mypy voluptuous diff --git a/voluptuous/error.py b/voluptuous/error.py index 35d392e5..27898807 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -21,14 +21,22 @@ class Invalid(Error): def __init__(self, message: str, path: typing.Optional[typing.List[str]] = None, error_message: typing.Optional[str] = None, error_type: typing.Optional[str] = None) -> None: Error.__init__(self, message) - self.path = path or [] - self.error_message = error_message or message + self._path = path or [] + self._error_message = error_message or message self.error_type = error_type @property def msg(self) -> str: return self.args[0] + @property + def path(self) -> typing.List[str]: + 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 '' @@ -38,7 +46,7 @@ def __str__(self) -> str: return output + path def prepend(self, path: typing.List[str]) -> None: - self.path = path + self.path + self._path = path + self.path class MultipleInvalid(Invalid): diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 0dbec453..9da759ba 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -165,11 +165,11 @@ def Extra(_) -> None: primitive_types = (bool, bytes, int, long, str, unicode, float, complex) Schemable = typing.Union[ - Extra, 'Schema', 'Object', + 'Schema', 'Object', _Mapping, list, tuple, frozenset, set, bool, bytes, int, long, str, unicode, float, complex, - type, object, dict, type(None), typing.Callable + type, object, dict, None, typing.Callable ] @@ -753,8 +753,7 @@ def extend(self, schema: Schemable, required: typing.Optional[bool] = None, extr :param extra: if set, overrides `extra` of this `Schema` """ - assert type(self.schema) == dict and type(schema) == dict, 'Both schemas must be dictionary-based' - assert isinstance(self.schema, dict) + assert isinstance(self.schema, dict) and isinstance(schema, dict), 'Both schemas must be dictionary-based' result = self.schema.copy() @@ -779,7 +778,7 @@ def key_literal(key): # if both are dictionaries, we need to extend recursively # create the new extended sub schema, then remove the old key and add the new one - if type(result_value) == dict and type(value) == dict: + if isinstance(result_value, dict) and isinstance(value, dict): new_value = Schema(result_value).extend(value).schema del result[result_key] result[key] = new_value diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 35052790..abca5f89 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -167,7 +167,7 @@ def test_literal(): except MultipleInvalid as e: assert str(e) == "{'b': 1} not match for {'a': 1}" assert len(e.errors) == 1 - assert type(e.errors[0]) == LiteralInvalid + assert isinstance(e.errors[0], LiteralInvalid) else: assert False, "Did not raise Invalid" @@ -184,7 +184,7 @@ class C1(object): except MultipleInvalid as e: assert str(e) == "expected C1" assert len(e.errors) == 1 - assert type(e.errors[0]) == TypeInvalid + assert isinstance(e.errors[0], TypeInvalid) else: assert False, "Did not raise Invalid" @@ -200,7 +200,7 @@ class C2: except MultipleInvalid as e: assert str(e) == "expected C2" assert len(e.errors) == 1 - assert type(e.errors[0]) == TypeInvalid + assert isinstance(e.errors[0], TypeInvalid) else: assert False, "Did not raise Invalid" From c891a7bb7a629b809189e3cdd0798ecd26968d8b Mon Sep 17 00:00:00 2001 From: spacegaier Date: Sun, 12 Nov 2023 18:00:13 +0100 Subject: [PATCH 224/261] Release 0.14.0 --- CHANGELOG.md | 17 +++++++++++++++++ voluptuous/__init__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f550b7b..67e248f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [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**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index de178d0f..76170c42 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.13.1' +__version__ = '0.14.0' __author__ = 'alecthomas' From 82e87df58c6e301b70838c01267234413cc0f7d5 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Tue, 14 Nov 2023 22:11:14 +0100 Subject: [PATCH 225/261] Add pytest.ini and tox.ini to sdist (#487) --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index dd15c3a1..4c2fefca 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include *.md include COPYING include voluptuous/tests/*.py include voluptuous/tests/*.md +include pytest.ini +include tox.ini From d8a72df640c2f4499f6013b93de0719a78f2455d Mon Sep 17 00:00:00 2001 From: Chris Withers Date: Fri, 17 Nov 2023 05:14:38 -0500 Subject: [PATCH 226/261] Add python_requires so package installers know this requires 3.7 or newer (#494) Also remove some stale classifiers --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index e7e19c00..04cc463e 100644 --- a/setup.py +++ b/setup.py @@ -26,15 +26,13 @@ }, author='Alec Thomas', author_email='alec@swapoff.org', + python_requires=">=3.7", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', From 5160e055b62c3e132543fcf501b1bced2d9e9f61 Mon Sep 17 00:00:00 2001 From: spacegaier Date: Fri, 17 Nov 2023 12:05:54 +0100 Subject: [PATCH 227/261] Release 0.14.1 --- .travis.yml | 20 -------------------- CHANGELOG.md | 6 ++++++ voluptuous/__init__.py | 2 +- 3 files changed, 7 insertions(+), 21 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e6d6c6e3..00000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python -sudo: true -dist: xenial -python: -- '2.7' -- '3.6' -- '3.7' -- '3.8' -- '3.9' -install: -- pip install coveralls -- pip install coverage -script: nosetests --with-coverage --cover-package=voluptuous -after_success: -- coveralls -#- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then bash ./update_documentation.sh $USERNAME $PASSWORD; fi # Fix this later -env: - global: - - secure: UKVFCaFRRECYeNaLJr4POqt6zENBjyUe79U/5b9pEGBFWzXWoJ+EElOFOJdkquL6u3AwL6Bw93GqRIYHKcRW70doCYiEI7p2CuXey2mjoC7bLKdk4Fcrj0MTbiS6WJxEDfcsP/Tj3tv4kPqA4nYYm9DQoNfUX3skns442h0zals= - - secure: EK2dbVB4T7qNFWCSu3tL+l2YnpcrCvPk9E3W05rGZnkT38Do21kVDncf8XRh/5Nn4J6zGmdoHw6NFqeQtF6/+3GNIqEW4PzA5x5pUx1rI6drB0hTaEURG3VYUmLOoQ/thziaEmnez8Qt1hUtn/0Jhl6eUYOtmSTSkDeLz7zehm0= diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e248f9..58d6370c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [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**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 76170c42..e8222640 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -5,5 +5,5 @@ from voluptuous.util import * from voluptuous.error import * -__version__ = '0.14.0' +__version__ = '0.14.1' __author__ = 'alecthomas' From 891b2eaef594cfcc1b8821d22c7085dc8666d742 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 1 Dec 2023 09:30:56 +0100 Subject: [PATCH 228/261] Drop duplicated type checks in Schema._compile (#495) --- voluptuous/schema_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 9da759ba..5d803c8a 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -311,7 +311,7 @@ def _compile(self, schema): type_ = type(schema) if inspect.isclass(schema): type_ = schema - if type_ in (*primitive_types, object, list, dict, type(None)) or callable(schema): + if type_ in (*primitive_types, object, type(None)) or callable(schema): return _compile_scalar(schema) raise er.SchemaError('unsupported schema data type %r' % type(schema).__name__) From b90b3f0a5ea2a565f0e219f6b7c0de428c04e522 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 2 Dec 2023 12:44:23 +1100 Subject: [PATCH 229/261] Configure renovate --- renovate.json5 | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 renovate.json5 diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 00000000..897864b8 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,11 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: [ + "config:recommended", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":semanticCommitScope(deps)", + "group:allNonMajor", + "schedule:earlyMondays", // Run once a week. + ], +} From 79d9dd7a8c0d34d15138725622179053d427d969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damien=20Coupp=C3=A9?= Date: Tue, 12 Dec 2023 22:50:29 +0100 Subject: [PATCH 230/261] Fix: Fix type hint for Coerce type param (#488) * Fix: Fix type hint for Coerce type param Coerce can accept a callable as type. * Update validators.py Fix: type. hint * Update voluptuous/validators.py Co-authored-by: Antoni Szych --------- Co-authored-by: Alec Thomas Co-authored-by: Antoni Szych --- voluptuous/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 776adb8b..c513d19e 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -102,7 +102,7 @@ class Coerce(object): ... validate('foo') """ - def __init__(self, type: type, msg: typing.Optional[str] = None) -> None: + def __init__(self, type: typing.Union[type, typing.Callable], msg: typing.Optional[str] = None) -> None: self.type = type self.msg = msg self.type_name = type.__name__ From ca5223ffd790c8a37b28bc40767a2cbdea3a7a95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damien=20Coupp=C3=A9?= Date: Wed, 13 Dec 2023 11:13:34 +0100 Subject: [PATCH 231/261] ci: Drop support of eol versions of python (#499) --- .github/workflows/tests.yml | 1 - setup.py | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 768188a4..60d75d25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,6 @@ jobs: - { python-version: "3.10", session: "py310" } - { python-version: "3.9", session: "py39" } - { python-version: "3.8", session: "py38" } - - { python-version: "3.7", session: "py37" } steps: - name: Check out the repository diff --git a/setup.py b/setup.py index 04cc463e..80b21634 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,6 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', From 264b7e4f7f3452c2305e5e3663c2daa70e358917 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Wed, 13 Dec 2023 18:39:55 +0100 Subject: [PATCH 232/261] fix: allow path to be a list of strings, integers or any other hashables (#497) --- voluptuous/error.py | 16 ++++++--- voluptuous/humanize.py | 5 +-- voluptuous/tests/tests.py | 70 +++++++++++++++++++++++++++++++++++++++ voluptuous/validators.py | 4 +-- 4 files changed, 84 insertions(+), 11 deletions(-) diff --git a/voluptuous/error.py b/voluptuous/error.py index 27898807..992ba0da 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -19,7 +19,13 @@ class Invalid(Error): """ - def __init__(self, message: str, path: typing.Optional[typing.List[str]] = None, error_message: typing.Optional[str] = None, error_type: typing.Optional[str] = None) -> None: + def __init__( + self, + message: str, + path: typing.Optional[typing.List[typing.Hashable]] = None, + error_message: typing.Optional[str] = None, + error_type: typing.Optional[str] = None + ) -> None: Error.__init__(self, message) self._path = path or [] self._error_message = error_message or message @@ -30,7 +36,7 @@ def msg(self) -> str: return self.args[0] @property - def path(self) -> typing.List[str]: + def path(self) -> typing.List[typing.Hashable]: return self._path @property @@ -45,7 +51,7 @@ def __str__(self) -> str: output += ' for ' + self.error_type return output + path - def prepend(self, path: typing.List[str]) -> None: + def prepend(self, path: typing.List[typing.Hashable]) -> None: self._path = path + self.path @@ -61,7 +67,7 @@ def msg(self) -> str: return self.errors[0].msg @property - def path(self) -> typing.List[str]: + def path(self) -> typing.List[typing.Hashable]: return self.errors[0].path @property @@ -74,7 +80,7 @@ def add(self, error: Invalid) -> None: def __str__(self) -> str: return str(self.errors[0]) - def prepend(self, path: typing.List[str]) -> None: + def prepend(self, path: typing.List[typing.Hashable]) -> None: for error in self.errors: error.prepend(path) diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py index 734f3672..923f3d1a 100644 --- a/voluptuous/humanize.py +++ b/voluptuous/humanize.py @@ -7,10 +7,7 @@ MAX_VALIDATION_ERROR_ITEM_LENGTH = 500 -IndexT = typing.TypeVar("IndexT") - - -def _nested_getitem(data: typing.Dict[IndexT, typing.Any], path: typing.List[IndexT]) -> typing.Optional[typing.Any]: +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] diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index abca5f89..e2c5a662 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1329,6 +1329,76 @@ def test_match_error_has_path(): assert False, "Did not raise MatchInvalid" +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")}) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index c513d19e..229bab7d 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -224,7 +224,7 @@ def __voluptuous_compile__(self, schema: Schema) -> typing.Callable: schema.required = old_required return self._run - def _run(self, path: typing.List[str], value): + def _run(self, path: typing.List[typing.Hashable], value): if self.discriminant is not None: self._compiled = [ self.schema._compile(v) @@ -243,7 +243,7 @@ def __repr__(self): self.msg ) - def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[str]] = None): + def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[typing.Hashable]] = None): raise NotImplementedError() From dec0fcb6413d2237b58e22b642fe2438feea3b15 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Wed, 13 Dec 2023 20:59:06 +0100 Subject: [PATCH 233/261] refactor(tests): fix few tests, use pytest.raises, extend raises helper (#500) --- voluptuous/schema_builder.py | 59 ++-- voluptuous/tests/tests.py | 574 +++++++++++++---------------------- voluptuous/util.py | 27 +- 3 files changed, 235 insertions(+), 425 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 5d803c8a..0ab7831f 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -13,25 +13,6 @@ import typing from voluptuous.error import Error -if sys.version_info >= (3,): - long = int - unicode = str - basestring = str - ifilter = filter - - def iteritems(d): - return d.items() -else: - from itertools import ifilter - - def iteritems(d): - return d.iteritems() - -if sys.version_info >= (3, 3): - _Mapping = collections.abc.Mapping -else: - _Mapping = collections.Mapping - """Schema validation for Python data structures. Given eg. a nested data structure like this: @@ -151,6 +132,8 @@ def raises(exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Patt 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: @@ -162,13 +145,13 @@ def Extra(_) -> None: # deprecated object, so we just leave an alias here instead. extra = Extra -primitive_types = (bool, bytes, int, long, str, unicode, float, complex) +primitive_types = (bool, bytes, int, str, float, complex) Schemable = typing.Union[ 'Schema', 'Object', - _Mapping, + collections.abc.Mapping, list, tuple, frozenset, set, - bool, bytes, int, long, str, unicode, float, complex, + bool, bytes, int, str, float, complex, type, object, dict, None, typing.Callable ] @@ -187,9 +170,9 @@ class Schema(object): For Example: - >>> v = Schema({Required('a'): unicode}) - >>> v1 = Schema({Required('a'): unicode}) - >>> v2 = Schema({Required('b'): unicode}) + >>> v = Schema({Required('a'): str}) + >>> v1 = Schema({Required('a'): str}) + >>> v2 = Schema({Required('b'): str}) >>> assert v == v1 >>> assert v != v2 @@ -254,7 +237,7 @@ def value_to_schema_type(value): if len(value) == 0: return dict return {k: value_to_schema_type(v) - for k, v in iteritems(value)} + for k, v in value.items()} if isinstance(value, list): if len(value) == 0: return list @@ -300,7 +283,7 @@ def _compile(self, schema): return schema.__voluptuous_compile__(self) if isinstance(schema, Object): return self._compile_object(schema) - if isinstance(schema, _Mapping): + if isinstance(schema, collections.abc.Mapping): return self._compile_dict(schema) elif isinstance(schema, list): return self._compile_list(schema) @@ -333,7 +316,7 @@ def _compile_mapping(self, schema, invalid_msg=None): or isinstance(key, Optional)) _compiled_schema = {} - for skey, svalue in iteritems(schema): + for skey, svalue in schema.items(): new_key = self._compile(skey) new_value = self._compile(svalue) _compiled_schema[skey] = (new_key, new_value) @@ -477,7 +460,7 @@ def validate_object(path, data): if schema.cls is not UNDEFINED and not isinstance(data, schema.cls): raise er.ObjectInvalid('expected a {0!r}'.format(schema.cls), path) iterable = _iterate_object(data) - iterable = ifilter(lambda item: item[1] is not None, iterable) + iterable = filter(lambda item: item[1] is not None, iterable) out = base_validate(path, iterable, {}) return type(data)(**out) @@ -608,7 +591,7 @@ def validate_dict(path, data): raise er.MultipleInvalid(errors) out = data.__class__() - return base_validate(path, iteritems(data), out) + return base_validate(path, data.items(), out) return validate_dict @@ -767,7 +750,7 @@ def key_literal(key): # for each item in the extension schema, replace duplicates # or add new keys - for key, value in iteritems(schema): + for key, value in schema.items(): # if the key is already in the dictionary, we need to replace it # transform key to literal before checking presence @@ -896,7 +879,7 @@ def _iterate_mapping_candidates(schema): # Without this, Extra might appear first in the iterator, and fail to # validate a key even though it's a Required that has its own validation, # generating a false positive. - return sorted(iteritems(schema), key=_sort_item) + return sorted(schema.items(), key=_sort_item) def _iterate_object(obj): @@ -911,7 +894,7 @@ def _iterate_object(obj): # maybe we have named tuple here? if hasattr(obj, '_asdict'): d = obj._asdict() - for item in iteritems(d): + for item in d.items(): yield item try: slots = obj.__slots__ @@ -1076,15 +1059,15 @@ class Exclusive(Optional): >>> msg = 'Please, use only one type of authentication at the same time.' >>> schema = Schema({ ... Exclusive('classic', 'auth', msg=msg):{ - ... Required('email'): basestring, - ... Required('password'): basestring + ... Required('email'): str, + ... Required('password'): str ... }, ... Exclusive('internal', 'auth', msg=msg):{ - ... Required('secret_key'): basestring + ... Required('secret_key'): str ... }, ... Exclusive('social', 'auth', msg=msg):{ - ... Required('social_network'): basestring, - ... Required('token'): basestring + ... Required('social_network'): str, + ... Required('token'): str ... } ... }) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index e2c5a662..6505de2a 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,25 +1,20 @@ -from voluptuous.util import Capitalize, Lower, Strip, Title, Upper, u +from voluptuous.util import Capitalize, Lower, Strip, Title, Upper from voluptuous.humanize import humanize_error -from voluptuous import (ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Clamp, Coerce, - Contains, Date, Datetime, Email, Equal, ExactSequence, - Exclusive, Extra, FqdnUrl, In, Inclusive, Invalid, - IsDir, IsFile, Length, Literal, LiteralInvalid, Marker, - Match, Maybe, MultipleInvalid, NotIn, Number, Object, +from voluptuous import (ALLOW_EXTRA, PREVENT_EXTRA, All, AllInvalid, Any, Clamp, + Coerce, Contains, ContainsInvalid, Date, Datetime, Email, + EmailInvalid, Equal, ExactSequence, Exclusive, Extra, + FqdnUrl, In, InInvalid, Inclusive, 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, raises, validate) + Unordered, Url, UrlInvalid, raises, validate) import pytest +from enum import Enum import sys import os import collections import copy -import typing - -Enum: typing.Union[type, None] -try: - from enum import Enum -except ImportError: - Enum = None def test_new_required_test(): @@ -31,12 +26,8 @@ def test_new_required_test(): def test_exact_sequence(): schema = Schema(ExactSequence([int, int])) - try: + with raises(Invalid): schema([1, 2, 3]) - except Invalid: - assert True - else: - assert False, "Did not raise Invalid" assert schema([1, 2]) == [1, 2] @@ -44,12 +35,8 @@ def test_required(): """Verify that Required works.""" schema = Schema({Required('q'): int}) schema({"q": 123}) - try: + with raises(Invalid, "required key not provided @ data['q']"): schema({}) - except Invalid as e: - assert str(e) == "required key not provided @ data['q']" - else: - assert False, "Did not raise Invalid" def test_extra_with_required(): @@ -74,34 +61,36 @@ def test_in(): """Verify that In works.""" schema = Schema({"color": In(frozenset(["red", "blue", "yellow"]))}) schema({"color": "blue"}) - try: + with pytest.raises( + MultipleInvalid, + match=r"value must be one of \['blue', 'red', 'yellow'\] for dictionary value @ data\['color'\]" + ) as ctx: schema({"color": "orange"}) - except Invalid as e: - assert str(e) == "value must be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']" - else: - assert False, "Did not raise InInvalid" + 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"}) - try: + with pytest.raises( + MultipleInvalid, + match=r"value must not be one of \['blue', 'red', 'yellow'\] for dictionary value @ data\['color'\]" + ) as ctx: schema({"color": "blue"}) - except Invalid as e: - assert str(e) == "value must not be one of ['blue', 'red', 'yellow'] for dictionary value @ data['color']" - else: - assert False, "Did not raise NotInInvalid" + 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']}) - try: + with pytest.raises(MultipleInvalid, match=r"value is not allowed for dictionary value @ data\['color'\]") as ctx: schema({'color': ['blue', 'yellow']}) - except Invalid as e: - assert str(e) == "value is not allowed for dictionary value @ data['color']" + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], ContainsInvalid) def test_remove(): @@ -154,55 +143,29 @@ def test_literal(): schema([{"b": 1}]) schema([{"a": 1}, {"b": 1}]) - try: + with pytest.raises(MultipleInvalid, match=r"\{'c': 1\} not match for \{'b': 1\} @ data\[0\]") as ctx: schema([{"c": 1}]) - except Invalid as e: - assert str(e) == "{'c': 1} not match for {'b': 1} @ data[0]" - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], LiteralInvalid) schema = Schema(Literal({"a": 1})) - try: + with pytest.raises(MultipleInvalid, match=r"\{'b': 1\} not match for \{'a': 1\}") as ctx: schema({"b": 1}) - except MultipleInvalid as e: - assert str(e) == "{'b': 1} not match for {'a': 1}" - assert len(e.errors) == 1 - assert isinstance(e.errors[0], LiteralInvalid) - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], LiteralInvalid) def test_class(): - class C1(object): + class C1: pass schema = Schema(C1) schema(C1()) - try: + with pytest.raises(MultipleInvalid, match=r"expected C1") as ctx: schema(None) - except MultipleInvalid as e: - assert str(e) == "expected C1" - assert len(e.errors) == 1 - assert isinstance(e.errors[0], TypeInvalid) - else: - assert False, "Did not raise Invalid" - - # In Python 2, this will be an old-style class (classobj instance) - class C2: - pass - - schema = Schema(C2) - schema(C2()) - - try: - schema(None) - except MultipleInvalid as e: - assert str(e) == "expected C2" - assert len(e.errors) == 1 - assert isinstance(e.errors[0], TypeInvalid) - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], TypeInvalid) def test_email_validation(): @@ -216,46 +179,46 @@ def test_email_validation(): def test_email_validation_with_none(): """ Test with invalid None email address """ schema = Schema({"email": Email()}) - try: + with pytest.raises( + MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]" + ) as ctx: schema({"email": None}) - except MultipleInvalid as e: - assert str(e) == "expected an email address for dictionary value @ data['email']" - else: - assert False, "Did not raise Invalid for None URL" + 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()}) - try: + with pytest.raises( + MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]" + ) as ctx: schema({"email": ''}) - except MultipleInvalid as e: - assert str(e) == "expected an email address for dictionary value @ data['email']" - else: - assert False, "Did not raise Invalid for empty string URL" + 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()}) - try: + with pytest.raises( + MultipleInvalid, match=r"expected an email address for dictionary value @ data\['email'\]" + ) as ctx: schema({"email": 'a@.com'}) - except MultipleInvalid as e: - assert str(e) == "expected an email address for dictionary value @ data['email']" - else: - assert False, "Did not raise Invalid for empty string URL" + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], EmailInvalid) -def test_email_validation_with_bad_data(): +@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()}) - for email in ('john@voluptuous.com>', 'john!@voluptuous.org!@($*!'): - try: - schema({"email": 'john@voluptuous.com>'}) - except MultipleInvalid as e: - assert str(e) == "expected an email address for dictionary value @ data['email']" - else: - assert False, "Did not raise Invalid for bad 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(): @@ -266,48 +229,23 @@ def test_fqdn_url_validation(): assert 'http://example.com/', out_.get("url") -def test_fqdn_url_without_domain_name(): - """ Test with invalid fully qualified domain name URL """ - schema = Schema({"url": FqdnUrl()}) - try: - schema({"url": "http://localhost/"}) - except MultipleInvalid as e: - assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for None URL" - - -def test_fqdnurl_validation_with_none(): - """ Test with invalid None FQDN URL """ - schema = Schema({"url": FqdnUrl()}) - try: - schema({"url": None}) - except MultipleInvalid as e: - assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for None URL" - - -def test_fqdnurl_validation_with_empty_string(): - """ Test with empty string FQDN URL """ - schema = Schema({"url": FqdnUrl()}) - try: - schema({"url": ''}) - except MultipleInvalid as e: - assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for empty string URL" - - -def test_fqdnurl_validation_without_host(): - """ Test with empty host FQDN URL """ +@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()}) - try: - schema({"url": 'http://'}) - except MultipleInvalid as e: - assert str(e) == "expected a fully qualified domain name URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for empty string URL" + 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(): @@ -318,37 +256,22 @@ def test_url_validation(): assert 'http://example.com/', out_.get("url") -def test_url_validation_with_none(): - """ Test with invalid None URL""" - schema = Schema({"url": Url()}) - try: - schema({"url": None}) - except MultipleInvalid as e: - assert str(e) == "expected a URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for None URL" - - -def test_url_validation_with_empty_string(): - """ Test with empty string URL """ +@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()}) - try: - schema({"url": ''}) - except MultipleInvalid as e: - assert str(e) == "expected a URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for empty string URL" - - -def test_url_validation_without_host(): - """ Test with empty host URL """ - schema = Schema({"url": Url()}) - try: - schema({"url": 'http://'}) - except MultipleInvalid as e: - assert str(e) == "expected a URL for dictionary value @ data['url']" - else: - assert False, "Did not raise Invalid for empty string 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(): @@ -530,14 +453,12 @@ def is_even(value): schema = Schema(dict(even_numbers=[All(int, is_even)])) - try: + with pytest.raises(MultipleInvalid, match=r"3 is not even @ data\['even_numbers'\]\[0\]") as ctx: schema(dict(even_numbers=[3])) - except Invalid as e: - assert len(e.errors) == 1 - assert str(e.errors[0]) == "3 is not even @ data['even_numbers'][0]" - assert str(e) == "3 is not even @ data['even_numbers'][0]" - else: - assert False, "Did not raise Invalid" + + 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(): @@ -548,17 +469,14 @@ def is_even(value): raise Invalid('%i is not even' % value) return value - schema = Schema(dict(even_numbers=All([All(int, is_even)], - Length(min=1)))) + schema = Schema(dict(even_numbers=All([All(int, is_even)], Length(min=1)))) - try: + with pytest.raises(MultipleInvalid, match=r"3 is not even @ data\['even_numbers'\]\[0\]") as ctx: schema(dict(even_numbers=[3])) - except Invalid as e: - assert len(e.errors) == 1 - assert str(e.errors[0]) == "3 is not even @ data['even_numbers'][0]" - assert str(e) == "3 is not even @ data['even_numbers'][0]" - else: - assert False, "Did not raise Invalid" + + 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(): @@ -570,18 +488,19 @@ def test_humanize_error(): 'a': int, 'b': [str] }) - try: + with pytest.raises(MultipleInvalid) as ctx: schema(data) - except MultipleInvalid as e: - assert humanize_error(data, e) == "expected int for dictionary value @ data['a']. Got 'not an int'\nexpected str @ data['b'][0]. Got 123" - else: - assert False, 'Did not raise MultipleInvalid' + 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']) - pytest.raises(MultipleInvalid, s, ['four']) + with pytest.raises(MultipleInvalid): + s(['four']) def test_range_inside(): @@ -935,15 +854,6 @@ def fn(arg1, arg2): pytest.raises(Invalid, fn, arg1=1, arg2="foo") -def test_unicode_as_key(): - if sys.version_info >= (3,): - text_type = str - else: - text_type = unicode # noqa: F821 - schema = Schema({text_type: int}) - schema({u("foobar"): 1}) - - def test_number_validation_with_string(): """ Test with Number with string""" schema = Schema({"number": Number(precision=6, scale=2)}) @@ -1244,7 +1154,7 @@ def test_SomeOf_min_validation(): validator('a') with raises(MultipleInvalid, 'no uppercase letters, no lowercase letters'): - validator('wqs2!#s111') + validator('1232!#4111') with raises(MultipleInvalid, 'no lowercase letters, no symbols'): validator('3A34SDEF5') @@ -1266,18 +1176,12 @@ def test_SomeOf_max_validation(): def test_self_validation(): schema = Schema({"number": int, "follow": Self}) - try: + with raises(MultipleInvalid): schema({"number": "abc"}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" - try: + + with raises(MultipleInvalid): schema({"follow": {"number": '123456.712'}}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" + schema({"follow": {"number": 123456}}) schema({"follow": {"follow": {"number": 123456}}}) @@ -1288,15 +1192,14 @@ def test_any_error_has_path(): Optional('q'): int, Required('q2'): Any(int, msg='toto') }) - try: + + with pytest.raises(MultipleInvalid) as ctx: s({'q': 'str', 'q2': 'tata'}) - except MultipleInvalid as exc: - assert ( - (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) - or (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) - ) - else: - assert False, "Did not raise AnyInvalid" + + 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(): @@ -1305,15 +1208,18 @@ def test_all_error_has_path(): Optional('q'): int, Required('q2'): All([str, Length(min=10)], msg='toto'), }) - try: + with pytest.raises(MultipleInvalid) as ctx: s({'q': 'str', 'q2': 12}) - except MultipleInvalid as exc: - assert ( - (exc.errors[0].path == ['q'] and exc.errors[1].path == ['q2']) - or (exc.errors[1].path == ['q'] and exc.errors[0].path == ['q2']) - ) - else: - assert False, "Did not raise AllInvalid" + + 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(): @@ -1321,12 +1227,11 @@ def test_match_error_has_path(): s = Schema({ Required('q2'): Match("a"), }) - try: + with pytest.raises(MultipleInvalid) as ctx: s({'q2': 12}) - except MultipleInvalid as exc: - assert exc.errors[0].path == ['q2'] - else: - assert False, "Did not raise MatchInvalid" + 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(): @@ -1402,18 +1307,14 @@ def __hash__(self): def test_self_any(): schema = Schema({"number": int, "follow": Any(Self, "stop")}) - try: + with pytest.raises(MultipleInvalid) as ctx: schema({"number": "abc"}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" - try: + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], TypeInvalid) + + with raises(MultipleInvalid): schema({"follow": {"number": '123456.712'}}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" + schema({"follow": {"number": 123456}}) schema({"follow": {"follow": {"number": 123456}}}) schema({"follow": {"follow": {"number": 123456, "follow": "stop"}}}) @@ -1425,27 +1326,24 @@ def test_self_all(): Schema({"extra_number": int}, extra=ALLOW_EXTRA))}, extra=ALLOW_EXTRA) - try: + with pytest.raises(MultipleInvalid) as ctx: schema({"number": "abc"}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" - try: + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], TypeInvalid) + + with pytest.raises(MultipleInvalid) as ctx: schema({"follow": {"number": '123456.712'}}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" + 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}}) - try: + + with pytest.raises(MultipleInvalid) as ctx: schema({"follow": {"number": 123456, "extra_number": "123"}}) - except MultipleInvalid: - pass - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 + assert isinstance(ctx.value.errors[0], TypeInvalid) def test_SomeOf_on_bounds_assertion(): @@ -1467,12 +1365,9 @@ def test_set_of_integers(): schema(set()) schema(set([42])) schema(set([42, 43, 44])) - try: + with pytest.raises(MultipleInvalid, match="invalid value in set") as ctx: schema(set(['abc'])) - except MultipleInvalid as e: - assert str(e) == "invalid value in set" - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 def test_frozenset_of_integers(): @@ -1485,12 +1380,10 @@ def test_frozenset_of_integers(): schema(frozenset()) schema(frozenset([42])) schema(frozenset([42, 43, 44])) - try: + + with pytest.raises(MultipleInvalid, match="invalid value in frozenset") as ctx: schema(frozenset(['abc'])) - except MultipleInvalid as e: - assert str(e) == "invalid value in frozenset" - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 def test_set_of_integers_and_strings(): @@ -1502,12 +1395,10 @@ def test_set_of_integers_and_strings(): schema(set([42])) schema(set(['abc'])) schema(set([42, 'abc'])) - try: + + with pytest.raises(MultipleInvalid, match="invalid value in set") as ctx: schema(set([None])) - except MultipleInvalid as e: - assert str(e) == "invalid value in set" - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 def test_frozenset_of_integers_and_strings(): @@ -1519,12 +1410,10 @@ def test_frozenset_of_integers_and_strings(): schema(frozenset([42])) schema(frozenset(['abc'])) schema(frozenset([42, 'abc'])) - try: + + with pytest.raises(MultipleInvalid, match="invalid value in frozenset") as ctx: schema(frozenset([None])) - except MultipleInvalid as e: - assert str(e) == "invalid value in frozenset" - else: - assert False, "Did not raise Invalid" + assert len(ctx.value.errors) == 1 def test_lower_util_handles_various_inputs(): @@ -1565,12 +1454,8 @@ def test_strip_util_handles_various_inputs(): def test_any_required(): schema = Schema(Any({'a': int}, {'b': str}, required=True)) - try: + with raises(MultipleInvalid, "required key not provided @ data['a']"): schema({}) - except MultipleInvalid as e: - assert str(e) == "required key not provided @ data['a']" - else: - assert False, "Did not raise Invalid for MultipleInvalid" def test_any_required_with_subschema(): @@ -1579,12 +1464,8 @@ def test_any_required_with_subschema(): {'c': {'aa': int}}, required=True)) - try: + with raises(MultipleInvalid, "required key not provided @ data['a']"): schema({}) - except MultipleInvalid as e: - assert str(e) == "required key not provided @ data['a']" - else: - assert False, "Did not raise Invalid for MultipleInvalid" def test_inclusive(): @@ -1599,12 +1480,8 @@ def test_inclusive(): r = schema({'x': 1, 'y': 2}) assert r == {'x': 1, 'y': 2} - try: - r = schema({'x': 1}) - except MultipleInvalid as e: - assert str(e) == "some but not all values in the same group of inclusion 'stuff' @ data[]" - else: - assert False, "Did not raise Invalid for incomplete Inclusive group" + with raises(MultipleInvalid, "some but not all values in the same group of inclusion 'stuff' @ data[]"): + schema({'x': 1}) def test_inclusive_defaults(): @@ -1616,12 +1493,8 @@ def test_inclusive_defaults(): r = schema({}) assert r == {'x': 3, 'y': 4} - try: + with raises(MultipleInvalid, "some but not all values in the same group of inclusion 'stuff' @ data[]"): r = schema({'x': 1}) - except MultipleInvalid as e: - assert str(e) == "some but not all values in the same group of inclusion 'stuff' @ data[]" - else: - assert False, "Did not raise Invalid for incomplete Inclusive group with defaults" def test_exclusive(): @@ -1636,12 +1509,8 @@ def test_exclusive(): r = schema({'x': 1}) assert r == {'x': 1} - try: + with raises(MultipleInvalid, "two or more values in the same group of exclusion 'stuff' @ data[]"): r = schema({'x': 1, 'y': 2}) - except MultipleInvalid as e: - assert str(e) == "two or more values in the same group of exclusion 'stuff' @ data[]" - else: - assert False, "Did not raise Invalid for multiple values in Exclusive group" def test_any_with_discriminant(): @@ -1657,17 +1526,13 @@ def test_any_with_discriminant(): 'c-value': bool, }, discriminant=lambda value, alternatives: filter(lambda v: v['type'] == value['type'], alternatives)) }) - try: + with raises(MultipleInvalid, "expected bool for dictionary value @ data['implementation']['c-value']"): schema({ 'implementation': { 'type': 'C', 'c-value': None } }) - except MultipleInvalid as e: - assert str(e) == 'expected bool for dictionary value @ data[\'implementation\'][\'c-value\']' - else: - assert False, "Did not raise correct Invalid" def test_key1(): @@ -1675,19 +1540,17 @@ def as_int(a): return int(a) schema = Schema({as_int: str}) - try: + with pytest.raises(MultipleInvalid) as ctx: schema({ '1': 'one', 'two': '2', '3': 'three', 'four': '4', }) - except MultipleInvalid as e: - assert len(e.errors) == 2 - assert str(e.errors[0]) == "not a valid value @ data['two']" - assert str(e.errors[1]) == "not a valid value @ data['four']" - else: - assert False, "Did not raise correct Invalid" + + 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(): @@ -1698,55 +1561,43 @@ def as_int(a): raise Invalid('expecting a number') schema = Schema({as_int: str}) - try: + with pytest.raises(MultipleInvalid) as ctx: schema({ '1': 'one', 'two': '2', '3': 'three', 'four': '4', }) - except MultipleInvalid as e: - assert len(e.errors) == 2 - assert str(e.errors[0]) == "expecting a number @ data['two']" - assert str(e.errors[1]) == "expecting a number @ data['four']" - else: - assert False, "Did not raise correct Invalid" + 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']" -if Enum: - def test_coerce_enum(): - """Test Coerce Enum""" - class Choice(Enum): - Easy = 1 - Medium = 2 - Hard = 3 +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" + class StringChoice(str, Enum): + Easy = "easy" + Medium = "medium" + Hard = "hard" - schema = Schema(Coerce(Choice)) - string_schema = Schema(Coerce(StringChoice)) + schema = Schema(Coerce(Choice)) + string_schema = Schema(Coerce(StringChoice)) - # Valid value - assert schema(1) == Choice.Easy - assert string_schema("easy") == StringChoice.Easy + # Valid value + assert schema(1) == Choice.Easy + assert string_schema("easy") == StringChoice.Easy - # Invalid value - try: - schema(4) - except Invalid as e: - assert str(e) == "expected Choice or one of 1, 2, 3" - else: - assert False, "Did not raise Invalid for String" + # Invalid value + with raises(Invalid, "expected Choice or one of 1, 2, 3"): + schema(4) - try: - string_schema("hello") - except Invalid as e: - assert str(e) == "expected StringChoice or one of 'easy', 'medium', 'hard'" - else: - assert False, "Did not raise Invalid for String" + with raises(Invalid, "expected StringChoice or one of 'easy', 'medium', 'hard'"): + string_schema("hello") class MyValueClass(object): @@ -1761,22 +1612,15 @@ def test_object(): pytest.raises(MultipleInvalid, s, 345) -# Python 3.7 removed the trailing comma in repr() of BaseException -# https://bugs.python.org/issue30399 -if sys.version_info >= (3, 7): - invalid_scalar_excp_repr = "ScalarInvalid('not a valid value')" -else: - invalid_scalar_excp_repr = "ScalarInvalid('not a valid value',)" - - def test_exception(): s = Schema(None) - try: + with pytest.raises(MultipleInvalid) as ctx: s(123) - except MultipleInvalid as e: - assert repr(e) == "MultipleInvalid([{}])".format(invalid_scalar_excp_repr) - assert str(e.msg) == "not a valid value" - assert str(e.error_message) == "not a valid value" - assert str(e.errors) == "[{}]".format(invalid_scalar_excp_repr) - e.add("Test Error") - assert str(e.errors) == "[{}, 'Test Error']".format(invalid_scalar_excp_repr) + + 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']" diff --git a/voluptuous/util.py b/voluptuous/util.py index 58b91959..67021ef4 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,5 +1,3 @@ -import sys - # F401: "imported but unused" from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid # noqa: F401 from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401 @@ -10,14 +8,6 @@ __author__ = 'tusharmakkar08' -def _fix_str(v: str) -> str: - if sys.version_info[0] == 2 and isinstance(v, unicode): # noqa: F821 - s = v - else: - s = str(v) - return s - - def Lower(v: str) -> str: """Transform a string to lower case. @@ -25,7 +15,7 @@ def Lower(v: str) -> str: >>> s('HI') 'hi' """ - return _fix_str(v).lower() + return str(v).lower() def Upper(v: str) -> str: @@ -35,7 +25,7 @@ def Upper(v: str) -> str: >>> s('hi') 'HI' """ - return _fix_str(v).upper() + return str(v).upper() def Capitalize(v: str) -> str: @@ -45,7 +35,7 @@ def Capitalize(v: str) -> str: >>> s('hello world') 'Hello world' """ - return _fix_str(v).capitalize() + return str(v).capitalize() def Title(v: str) -> str: @@ -55,7 +45,7 @@ def Title(v: str) -> str: >>> s('hello world') 'Hello World' """ - return _fix_str(v).title() + return str(v).title() def Strip(v: str) -> str: @@ -65,7 +55,7 @@ def Strip(v: str) -> str: >>> s(' hello world ') 'hello world' """ - return _fix_str(v).strip() + return str(v).strip() class DefaultTo(object): @@ -156,10 +146,3 @@ def __str__(self): def __repr__(self): return repr(self.lit) - - -def u(x: str) -> str: - if sys.version_info < (3,): - return unicode(x) # noqa: F821 - else: - return x From abc6ed2cedab82348df1b3c5e46f797f964b9be8 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Thu, 14 Dec 2023 11:27:27 +0100 Subject: [PATCH 234/261] feat: add python 3.11 to github workflow tox matrix, remove 3.7 from tox (#501) * feat: add python 3.11 to github workflow tox matrix, remove 3.7 from tox * run flake8 and mypy on py311 * add py311 to package metadata --- .github/workflows/tests.yml | 5 +++-- setup.py | 1 + tox.ini | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60d75d25..cb4fdff4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,8 +14,9 @@ jobs: fail-fast: false matrix: include: - - { python-version: "3.10", session: "flake8" } - - { python-version: "3.10", session: "mypy" } + - { python-version: "3.11", session: "flake8" } + - { python-version: "3.11", session: "mypy" } + - { python-version: "3.11", session: "py311" } - { python-version: "3.10", session: "py310" } - { python-version: "3.9", session: "py39" } - { python-version: "3.8", session: "py38" } diff --git a/setup.py b/setup.py index 80b21634..c77ad292 100644 --- a/setup.py +++ b/setup.py @@ -36,5 +36,6 @@ 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ] ) diff --git a/tox.ini b/tox.ini index b3681a29..bd8ab7f3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py37,py38,py39,py310 +envlist = flake8,py38,py39,py310,py311 [flake8] ; E501: line too long (X > 79 characters) From 095322559b0a525c90c99d34bc233a93a3b54332 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Thu, 14 Dec 2023 12:13:44 +0100 Subject: [PATCH 235/261] feat: add python 3.12 to github workflow tox matrix and package metadata (#502) * feat: add python 3.12 to github workflow tox matrix and package metadata * bump minimal python requirement in setup.py --- .github/workflows/tests.yml | 1 + setup.py | 3 ++- tox.ini | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb4fdff4..515ee98e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,7 @@ jobs: include: - { python-version: "3.11", session: "flake8" } - { python-version: "3.11", session: "mypy" } + - { python-version: "3.12", session: "py312" } - { python-version: "3.11", session: "py311" } - { python-version: "3.10", session: "py310" } - { python-version: "3.9", session: "py39" } diff --git a/setup.py b/setup.py index c77ad292..cbd36eab 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ }, author='Alec Thomas', author_email='alec@swapoff.org', - python_requires=">=3.7", + python_requires=">=3.8", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', @@ -37,5 +37,6 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] ) diff --git a/tox.ini b/tox.ini index bd8ab7f3..cf8ecf66 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,py38,py39,py310,py311 +envlist = flake8,py38,py39,py310,py311,py312 [flake8] ; E501: line too long (X > 79 characters) From c5189d2b6f9dfb3016f094561afcfb5584d143ad Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Wed, 31 Jan 2024 13:19:58 +0100 Subject: [PATCH 236/261] Add linters configuration, reformat whole code (#503) --- .github/workflows/tests.yml | 2 + MANIFEST.in | 2 +- pyproject.toml | 18 ++ pytest.ini | 4 - setup.py | 7 +- tox.ini | 16 +- voluptuous/__init__.py | 9 +- voluptuous/error.py | 10 +- voluptuous/humanize.py | 35 ++- voluptuous/schema_builder.py | 237 ++++++++++------ voluptuous/tests/tests.py | 515 ++++++++++++++++++++--------------- voluptuous/util.py | 17 +- voluptuous/validators.py | 316 ++++++++++++++------- 13 files changed, 755 insertions(+), 433 deletions(-) create mode 100644 pyproject.toml delete mode 100644 pytest.ini diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 515ee98e..c6111320 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,8 +14,10 @@ jobs: fail-fast: false matrix: include: + - { python-version: "3.11", session: "black" } - { python-version: "3.11", session: "flake8" } - { python-version: "3.11", session: "mypy" } + - { python-version: "3.11", session: "isort" } - { python-version: "3.12", session: "py312" } - { python-version: "3.11", session: "py311" } - { python-version: "3.10", session: "py310" } diff --git a/MANIFEST.in b/MANIFEST.in index 4c2fefca..cfd67171 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,5 @@ include *.md include COPYING include voluptuous/tests/*.py include voluptuous/tests/*.md -include pytest.ini +include pyproject.toml include tox.ini diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..af6fa6bb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.black] +target-version = ["py38", "py39", "py310", "py311", "py312"] +skip-string-normalization = true + +[tool.isort] +skip_gitignore = true +profile = "black" +multi_line_output = 5 + +[tool.mypy] +python_version = "3.8" + +warn_unused_ignores = true + +[tool.pytest.ini_options] +python_files = "tests.py" +testpaths = "voluptuous/tests" +addopts = "--doctest-glob=*.md -v" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 0048dd07..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -python_files = tests.py -testpaths = voluptuous/tests -addopts = --doctest-glob=*.md -v diff --git a/setup.py b/setup.py index cbd36eab..efaa50c2 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ +import io +import sys + from setuptools import setup -import sys -import io sys.path.insert(0, '.') version = __import__('voluptuous').__version__ @@ -38,5 +39,5 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', - ] + ], ) diff --git a/tox.ini b/tox.ini index cf8ecf66..1296aabc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] -envlist = flake8,py38,py39,py310,py311,py312 +envlist = flake8,black,py38,py39,py310,py311,py312 [flake8] ; E501: line too long (X > 79 characters) -; W503 line break before binary operator -ignore = E501,W503 +; E203, E704: black-related ignores (see https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#flake8) +extend-ignore = E203, E501, E704 exclude = .tox,.venv,build,*.egg [testenv] @@ -28,3 +28,13 @@ deps = mypy pytest commands = mypy voluptuous + +[testenv:black] +deps = + black +commands = black --check . + +[testenv:isort] +deps = + isort +commands = isort --check . diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index e8222640..ad6669bb 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -1,9 +1,12 @@ # flake8: noqa - +# fmt: off from voluptuous.schema_builder import * -from voluptuous.validators import * from voluptuous.util import * -from voluptuous.error import * +from voluptuous.validators import * + +from voluptuous.error import * # isort: skip + +# fmt: on __version__ = '0.14.1' __author__ = 'alecthomas' diff --git a/voluptuous/error.py b/voluptuous/error.py index 992ba0da..9dab9435 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -1,5 +1,8 @@ +# fmt: off import typing +# fmt: on + class Error(Exception): """Base validation exception.""" @@ -24,7 +27,7 @@ def __init__( message: str, path: typing.Optional[typing.List[typing.Hashable]] = None, error_message: typing.Optional[str] = None, - error_type: typing.Optional[str] = None + error_type: typing.Optional[str] = None, ) -> None: Error.__init__(self, message) self._path = path or [] @@ -44,8 +47,7 @@ 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 '' + 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 @@ -207,9 +209,11 @@ class ExactSequenceInvalid(Invalid): class NotEnoughValid(Invalid): """The value did not pass enough validations.""" + pass class TooManyValid(Invalid): """The value passed more than expected validations.""" + pass diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py index 923f3d1a..eabfd027 100644 --- a/voluptuous/humanize.py +++ b/voluptuous/humanize.py @@ -1,13 +1,18 @@ +# fmt: off +import typing + from voluptuous import Invalid, MultipleInvalid from voluptuous.error import Error from voluptuous.schema_builder import Schema -import typing +# fmt: on MAX_VALIDATION_ERROR_ITEM_LENGTH = 500 -def _nested_getitem(data: typing.Any, path: typing.List[typing.Hashable]) -> typing.Optional[typing.Any]: +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] @@ -18,24 +23,34 @@ def _nested_getitem(data: typing.Any, path: typing.List[typing.Hashable]) -> typ 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 +def humanize_error( + data, + validation_error: Invalid, + max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH, +) -> str: + """Provide a more helpful + complete validation error message than that provided automatically Invalid and MultipleInvalid do not include the offending value in error messages, and MultipleInvalid.__str__ only provides the first error. """ if isinstance(validation_error, MultipleInvalid): - return '\n'.join(sorted( - humanize_error(data, sub_error, max_sub_error_length) - for sub_error in validation_error.errors - )) + return '\n'.join( + sorted( + humanize_error(data, sub_error, max_sub_error_length) + for sub_error in validation_error.errors + ) + ) else: offending_item_summary = repr(_nested_getitem(data, validation_error.path)) if len(offending_item_summary) > max_sub_error_length: - offending_item_summary = offending_item_summary[:max_sub_error_length - 3] + '...' + offending_item_summary = ( + offending_item_summary[: max_sub_error_length - 3] + '...' + ) return '%s. Got %s' % (validation_error, offending_item_summary) -def validate_with_humanized_errors(data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH) -> typing.Any: +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: diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 0ab7831f..6ad8758c 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1,18 +1,21 @@ +# fmt: off from __future__ import annotations import collections import inspect +import itertools import re -from functools import wraps import sys +import typing +from collections.abc import Generator from contextlib import contextmanager +from functools import wraps -import itertools from voluptuous import error as er -from collections.abc import Generator -import typing from voluptuous.error import Error +# fmt: on + """Schema validation for Python data structures. Given eg. a nested data structure like this: @@ -124,7 +127,9 @@ def default_factory(value) -> DefaultFactory: @contextmanager -def raises(exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None) -> Generator[None, None, None]: +def raises( + exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None +) -> Generator[None, None, None]: try: yield except exc as e: @@ -147,6 +152,7 @@ def Extra(_) -> None: primitive_types = (bool, bytes, int, str, float, complex) +# fmt: off Schemable = typing.Union[ 'Schema', 'Object', collections.abc.Mapping, @@ -154,6 +160,7 @@ def Extra(_) -> None: bool, bytes, int, str, float, complex, type, object, dict, None, typing.Callable ] +# fmt: on class Schema(object): @@ -184,7 +191,9 @@ class Schema(object): PREVENT_EXTRA: 'PREVENT_EXTRA', } - def __init__(self, schema: Schemable, required: bool = False, extra: int = PREVENT_EXTRA) -> None: + 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. @@ -232,18 +241,17 @@ def infer(cls, data, **kwargs) -> Schema: 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()} + return {k: value_to_schema_type(v) for k, v in value.items()} if isinstance(value, list): if len(value) == 0: return list else: - return [value_to_schema_type(v) - for v in value] + return [value_to_schema_type(v) for v in value] return type(value) return cls(value_to_schema_type(data), **kwargs) @@ -261,8 +269,11 @@ def __str__(self): def __repr__(self): return "" % ( - self.schema, self._extra_to_name.get(self.extra, '??'), - self.required, id(self)) + self.schema, + self._extra_to_name.get(self.extra, '??'), + self.required, + id(self), + ) def __call__(self, data): """Validate data against this schema.""" @@ -296,24 +307,29 @@ def _compile(self, 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__) + raise er.SchemaError('unsupported schema data type %r' % type(schema).__name__) def _compile_mapping(self, schema, invalid_msg=None): """Create validator for given mapping.""" invalid_msg = invalid_msg or 'mapping value' # Keys that may be required - all_required_keys = set(key for key in schema - if key is not Extra - and ((self.required - and not isinstance(key, (Optional, Remove))) - or isinstance(key, Required))) + all_required_keys = set( + key + for key in schema + if key is not Extra + and ( + (self.required and not isinstance(key, (Optional, Remove))) + or isinstance(key, Required) + ) + ) # Keys that may have defaults - all_default_keys = set(key for key in schema - if isinstance(key, Required) - or isinstance(key, Optional)) + all_default_keys = set( + key + for key in schema + if isinstance(key, Required) or isinstance(key, Optional) + ) _compiled_schema = {} for skey, svalue in schema.items(): @@ -332,7 +348,9 @@ def _compile_mapping(self, schema, invalid_msg=None): if type(skey) in primitive_types: candidates_by_key.setdefault(skey, []).append((skey, (ckey, cvalue))) elif isinstance(skey, Marker) and type(skey.schema) in primitive_types: - candidates_by_key.setdefault(skey.schema, []).append((skey, (ckey, cvalue))) + candidates_by_key.setdefault(skey.schema, []).append( + (skey, (ckey, cvalue)) + ) else: # These are wildcards such as 'int', 'str', 'Remove' and others which should be applied to all keys additional_candidates.append((skey, (ckey, cvalue))) @@ -349,8 +367,10 @@ def validate_mapping(path, iterable, out): # Insert default values for non-existing keys. for key in all_default_keys: - if not isinstance(key.default, Undefined) and \ - key.schema not in key_value_map: + if ( + not isinstance(key.default, Undefined) + and key.schema not in key_value_map + ): # A default value has been specified for this missing # key, insert it. key_value_map[key.schema] = key.default() @@ -361,7 +381,9 @@ def validate_mapping(path, iterable, out): remove_key = False # Optimization. Validate against the matching key first, then fallback to the rest - relevant_candidates = itertools.chain(candidates_by_key.get(key, []), additional_candidates) + relevant_candidates = itertools.chain( + candidates_by_key.get(key, []), additional_candidates + ) # compare each given key/value against all compiled key/values # schema key, (compiled key, compiled value) @@ -426,7 +448,11 @@ def validate_mapping(path, iterable, out): # for any required keys left that weren't found and don't have defaults: for key in required_keys: - msg = key.msg if hasattr(key, 'msg') and key.msg else 'required key not provided' + msg = ( + key.msg + if hasattr(key, 'msg') and key.msg + else 'required key not provided' + ) errors.append(er.RequiredFieldInvalid(msg, path + [key])) if errors: raise er.MultipleInvalid(errors) @@ -453,8 +479,7 @@ def _compile_object(self, schema): ... validate(Structure(one='three')) """ - base_validate = self._compile_mapping( - schema, invalid_msg='object value') + base_validate = self._compile_mapping(schema, invalid_msg='object value') def validate_object(path, data): if schema.cls is not UNDEFINED and not isinstance(data, schema.cls): @@ -542,8 +567,7 @@ def _compile_dict(self, schema): "expected str for dictionary value @ data['adict']['strfield']"] """ - base_validate = self._compile_mapping( - schema, invalid_msg='dictionary value') + base_validate = self._compile_mapping(schema, invalid_msg='dictionary value') groups_of_exclusion = {} groups_of_inclusion = {} @@ -565,8 +589,12 @@ def validate_dict(path, data): for exclusive in group: if exclusive.schema in data: if exists: - msg = exclusive.msg if hasattr(exclusive, 'msg') and exclusive.msg else \ - "two or more values in the same group of exclusion '%s'" % label + msg = ( + exclusive.msg + if hasattr(exclusive, 'msg') and exclusive.msg + else "two or more values in the same group of exclusion '%s'" + % label + ) next_path = path + [VirtualPathComponent(label)] errors.append(er.ExclusiveInvalid(msg, next_path)) break @@ -578,7 +606,10 @@ def validate_dict(path, data): for label, group in groups_of_inclusion.items(): included = [node.schema in data for node in group] if any(included) and not all(included): - msg = "some but not all values in the same group of inclusion '%s'" % label + msg = ( + "some but not all values in the same group of inclusion '%s'" + % label + ) for g in group: if hasattr(g, 'msg') and g.msg: msg = g.msg @@ -618,9 +649,9 @@ def validate_sequence(path, data): # Empty seq schema, reject any data. if not schema: if data: - raise er.MultipleInvalid([ - er.ValueInvalid('not a valid value', path if path else data) - ]) + raise er.MultipleInvalid( + [er.ValueInvalid('not a valid value', path if path else data)] + ) return data out = [] @@ -722,7 +753,12 @@ def validate_set(path, data): return validate_set - def extend(self, schema: Schemable, required: typing.Optional[bool] = None, extra: typing.Optional[int] = None) -> Schema: + 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 @@ -736,13 +772,15 @@ def extend(self, schema: Schemable, required: typing.Optional[bool] = None, extr :param extra: if set, overrides `extra` of this `Schema` """ - assert isinstance(self.schema, dict) and isinstance(schema, dict), 'Both schemas must be dictionary-based' + assert isinstance(self.schema, dict) and isinstance( + schema, dict + ), 'Both schemas must be dictionary-based' result = self.schema.copy() # returns the key that may have been passed as an argument to Marker constructor def key_literal(key): - return (key.schema if isinstance(key, Marker) else key) + return key.schema if isinstance(key, Marker) else key # build a map that takes the key literals to the needed objects # literal -> Required|Optional|literal @@ -751,11 +789,9 @@ def key_literal(key): # 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] @@ -777,8 +813,8 @@ def key_literal(key): # recompile and send old object result_cls = type(self) - result_required = (required if required is not None else self.required) - result_extra = (extra if extra is not None else self.extra) + result_required = required if required is not None else self.required + result_extra = extra if extra is not None else self.extra return result_cls(result, required=result_required, extra=result_extra) @@ -802,6 +838,7 @@ def _compile_scalar(schema): ... _compile_scalar(lambda v: float(v))([], 'a') """ if inspect.isclass(schema): + def validate_instance(path, data): if isinstance(data, schema): return data @@ -812,6 +849,7 @@ def validate_instance(path, data): return validate_instance if callable(schema): + def validate_callable(path, data): try: return schema(data) @@ -854,11 +892,13 @@ def is_callable(key_): # Remove markers should match first (since invalid values will not # raise an Error, instead the validator will check if other schemas match # the same value). - priority = [(1, is_remove), # Remove highest priority after values - (2, is_marker), # then other Markers - (4, is_type), # types/classes lowest before Extra - (3, is_callable), # callables after markers - (5, is_extra)] # Extra lowest priority + priority = [ + (1, is_remove), # Remove highest priority after values + (2, is_marker), # then other Markers + (4, is_type), # types/classes lowest before Extra + (3, is_callable), # callables after markers + (5, is_extra), # Extra lowest priority + ] def item_priority(item_): key_ = item_[0] @@ -935,10 +975,16 @@ class Msg(object): ... assert isinstance(e.errors[0], er.RangeInvalid) """ - def __init__(self, schema: Schemable, msg: str, cls: typing.Optional[typing.Type[Error]] = None) -> None: + def __init__( + self, + schema: Schemable, + msg: str, + cls: typing.Optional[typing.Type[Error]] = None, + ) -> None: if cls and not issubclass(cls, er.Invalid): - raise er.SchemaError("Msg can only use subclases of" - " Invalid as custom class") + raise er.SchemaError( + "Msg can only use subclases of Invalid as custom class" + ) self._schema = schema self.schema = Schema(schema) self.msg = msg @@ -976,7 +1022,12 @@ def __repr__(self): class Marker(object): """Mark nodes for special treatment.""" - def __init__(self, schema_: Schemable, msg: typing.Optional[str] = None, description: typing.Optional[str] = None) -> None: + 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 @@ -1034,9 +1085,14 @@ class Optional(Marker): {'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) + 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) @@ -1076,14 +1132,19 @@ class Exclusive(Optional): ... '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) + def __init__( + self, + schema: Schemable, + group_of_exclusion: str, + msg: typing.Optional[str] = None, + description: typing.Optional[str] = None, + ) -> None: + super(Exclusive, self).__init__(schema, msg=msg, description=description) self.group_of_exclusion = group_of_exclusion class Inclusive(Optional): - """ Mark a node in the schema as inclusive. + """Mark a node in the schema as inclusive. Inclusive keys inherited from Optional: @@ -1124,11 +1185,17 @@ class Inclusive(Optional): 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) + 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 @@ -1147,9 +1214,14 @@ class Required(Marker): {'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) + 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) @@ -1179,7 +1251,10 @@ def __hash__(self): return object.__hash__(self) -def message(default: typing.Optional[str] = None, cls: typing.Optional[typing.Type[Error]] = None) -> typing.Callable: +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: @@ -1208,7 +1283,9 @@ def message(default: typing.Optional[str] = None, cls: typing.Optional[typing.Ty ... assert isinstance(e.errors[0], IntegerInvalid) """ if cls and not issubclass(cls, er.Invalid): - raise er.SchemaError("message can only use subclases of Invalid as custom class") + raise er.SchemaError( + "message can only use subclases of Invalid as custom class" + ) def decorator(f): @wraps(f) @@ -1218,7 +1295,9 @@ def wrapper(*args, **kwargs): try: return f(*args, **kwargs) except ValueError: - raise (clsoverride or cls or er.ValueInvalid)(msg or default or 'invalid value') + raise (clsoverride or cls or er.ValueInvalid)( + msg or default or 'invalid value' + ) return wrapper @@ -1237,9 +1316,11 @@ def _args_to_dict(func, args): arg_names = func.func_code.co_varnames[:arg_count] arg_value_list = list(args) - arguments = dict((arg_name, arg_value_list[i]) - for i, arg_name in enumerate(arg_names) - if i < len(arg_value_list)) + arguments = dict( + (arg_name, arg_value_list[i]) + for i, arg_name in enumerate(arg_names) + if i < len(arg_value_list) + ) return arguments @@ -1269,7 +1350,6 @@ def validate(*a, **kw) -> typing.Callable: RETURNS_KEY = '__return__' def validate_schema_decorator(func): - returns_defined = False returns = None @@ -1281,8 +1361,11 @@ def validate_schema_decorator(func): returns = schema_arguments[RETURNS_KEY] del schema_arguments[RETURNS_KEY] - input_schema = (Schema(schema_arguments, extra=ALLOW_EXTRA) - if len(schema_arguments) != 0 else lambda x: x) + input_schema = ( + Schema(schema_arguments, extra=ALLOW_EXTRA) + if len(schema_arguments) != 0 + else lambda x: x + ) output_schema = Schema(returns) if returns_defined else lambda x: x @wraps(func) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 6505de2a..05b7e8e7 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1,26 +1,34 @@ -from voluptuous.util import Capitalize, Lower, Strip, Title, Upper -from voluptuous.humanize import humanize_error -from voluptuous import (ALLOW_EXTRA, PREVENT_EXTRA, All, AllInvalid, Any, Clamp, - Coerce, Contains, ContainsInvalid, Date, Datetime, Email, - EmailInvalid, Equal, ExactSequence, Exclusive, Extra, - FqdnUrl, In, InInvalid, Inclusive, 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) -import pytest -from enum import Enum -import sys -import os +# 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) + schema = Schema( + { + 'my_key': All(int, Range(1, 20)), + }, + required=True, + ) assert schema.required @@ -54,6 +62,7 @@ def test_iterate_candidates(): } # toaster should be first. from voluptuous.schema_builder import _iterate_mapping_candidates + assert _iterate_mapping_candidates(schema)[0][0] == 'toaster' @@ -63,7 +72,7 @@ def test_in(): schema({"color": "blue"}) with pytest.raises( MultipleInvalid, - match=r"value must be one of \['blue', 'red', 'yellow'\] for dictionary value @ data\['color'\]" + 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 @@ -76,7 +85,7 @@ def test_not_in(): schema({"color": "orange"}) with pytest.raises( MultipleInvalid, - match=r"value must not be one of \['blue', 'red', 'yellow'\] for dictionary value @ data\['color'\]" + match=r"value must not be one of \['blue', 'red', 'yellow'\] for dictionary value @ data\['color'\]", ) as ctx: schema({"color": "blue"}) assert len(ctx.value.errors) == 1 @@ -87,7 +96,10 @@ 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: + 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) @@ -96,23 +108,22 @@ def test_contains(): def test_remove(): """Verify that Remove works.""" # remove dict keys - schema = Schema({"weight": int, - Remove("color"): str, - Remove("amount"): int}) + 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}) + 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_ @@ -136,20 +147,24 @@ def test_extra_empty_errors(): def test_literal(): - """ Test with 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: + 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: + 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) @@ -169,7 +184,7 @@ class C1: def test_email_validation(): - """ Test with valid email address """ + """Test with valid email address""" schema = Schema({"email": Email()}) out_ = schema({"email": "example@example.com"}) @@ -177,10 +192,11 @@ def test_email_validation(): def test_email_validation_with_none(): - """ Test with invalid None email address """ + """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'\]" + MultipleInvalid, + match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": None}) assert len(ctx.value.errors) == 1 @@ -188,10 +204,11 @@ def test_email_validation_with_none(): def test_email_validation_with_empty_string(): - """ Test with empty string email address""" + """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'\]" + MultipleInvalid, + match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": ''}) assert len(ctx.value.errors) == 1 @@ -199,22 +216,26 @@ def test_email_validation_with_empty_string(): def test_email_validation_without_host(): - """ Test with empty host name in email address """ + """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'\]" + 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!@($*!']) +@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 """ + """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'\]" + MultipleInvalid, + match=r"expected an email address for dictionary value @ data\['email'\]", ) as ctx: schema({"email": input_value}) assert len(ctx.value.errors) == 1 @@ -222,7 +243,7 @@ def test_email_validation_with_bad_data(input_value: str): def test_fqdn_url_validation(): - """ Test with valid fully qualified domain name URL """ + """Test with valid fully qualified domain name URL""" schema = Schema({"url": FqdnUrl()}) out_ = schema({"url": "http://example.com/"}) @@ -236,12 +257,13 @@ def test_fqdn_url_validation(): 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'\]" + 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 @@ -249,7 +271,7 @@ def test_fqdn_url_validation_with_bad_data(input_value): def test_url_validation(): - """ Test with valid URL """ + """Test with valid URL""" schema = Schema({"url": Url()}) out_ = schema({"url": "http://example.com/"}) @@ -262,7 +284,7 @@ def test_url_validation(): 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()}) @@ -275,10 +297,8 @@ def test_url_validation_with_bad_data(input_value): def test_copy_dict_undefined(): - """ Test with a copied dictionary """ - fields = { - Required("foo"): int - } + """Test with a copied dictionary""" + fields = {Required("foo"): int} copied_fields = copy.deepcopy(fields) schema = Schema(copied_fields) @@ -292,7 +312,7 @@ def test_copy_dict_undefined(): def test_sorting(): - """ Expect alphabetic sorting """ + """Expect alphabetic sorting""" foo = Required('foo') bar = Required('bar') items = [foo, bar] @@ -352,6 +372,7 @@ def test_subschema_extension(): def test_schema_extend_handles_schema_subclass(): """Verify that Schema.extend handles a subclass of Schema""" + class S(Schema): pass @@ -429,22 +450,26 @@ 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') + 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(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 """ + """Make sure useful error messages are available""" def is_even(value): if value % 2: @@ -453,7 +478,9 @@ def is_even(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: + 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 @@ -462,7 +489,7 @@ def is_even(value): def test_nested_multiple_validation_errors(): - """ Make sure useful error messages are available """ + """Make sure useful error messages are available""" def is_even(value): if value % 2: @@ -471,7 +498,9 @@ def is_even(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: + 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 @@ -480,14 +509,8 @@ def is_even(value): def test_humanize_error(): - data = { - 'a': 'not an int', - 'b': [123] - } - schema = Schema({ - 'a': int, - 'b': [str] - }) + 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 @@ -730,7 +753,7 @@ def test_schema_empty_dict(): def test_schema_empty_dict_key(): - """ https://github.com/alecthomas/voluptuous/pull/434 """ + """https://github.com/alecthomas/voluptuous/pull/434""" s = Schema({'var': []}) s({'var': []}) @@ -855,86 +878,97 @@ def fn(arg1, arg2): def test_number_validation_with_string(): - """ Test with Number 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']" + 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""" + """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']" + 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""" + """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""" + """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""" + """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""" + """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""" + """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']" + 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""" + """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""" + """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']" + 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""" + """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' @@ -974,9 +1008,17 @@ def test_ordered_dict(): # 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)]) + 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' @@ -985,8 +1027,11 @@ def test_ordered_dict(): 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 + Required('x'): int, + Optional('y'): float, + Remove('j'): int, + Remove(int): str, + int: int, } assert definition.get('x') == int assert definition.get('y') == float @@ -997,46 +1042,27 @@ def test_marker_hashable(): 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 - }) + 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' - } - } - }) + schema = Schema.infer({'a': {'b': {'c': 'foo'}}}) - assert schema == Schema({ - Required('a'): { - Required('b'): { - Required('c'): str - } - } - }) + assert schema == Schema({Required('a'): {Required('b'): {Required('c'): str}}}) def test_schema_infer_list(): - schema = Schema.infer({ - 'list': ['foo', True, 42, 3.14] - }) + schema = Schema.infer({'list': ['foo', True, 42, 3.14]}) - assert schema == Schema({ - Required('list'): [str, bool, int, float] - }) + assert schema == Schema({Required('list'): [str, bool, int, float]}) def test_schema_infer_scalar(): @@ -1049,10 +1075,7 @@ def test_schema_infer_scalar(): def test_schema_infer_accepts_kwargs(): - schema = Schema.infer({ - 'str': 'foo', - 'bool': True - }, required=False, extra=True) + schema = Schema.infer({'str': 'foo', 'bool': True}, required=False, extra=True) # Subset of schema should be acceptable thanks to required=False. schema({'bool': False}) @@ -1092,18 +1115,26 @@ def __call__(self, *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 + 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) + 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) + assert counter[0] <= num_of_keys, "Validation complexity is not linear! %s > %s" % ( + counter[0], + num_of_keys, + ) def test_IsDir(): @@ -1142,12 +1173,18 @@ def test_description(): 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 = 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'): @@ -1163,10 +1200,13 @@ def test_SomeOf_min_validation(): 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') + 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'): @@ -1174,8 +1214,7 @@ def test_SomeOf_max_validation(): def test_self_validation(): - schema = Schema({"number": int, - "follow": Self}) + schema = Schema({"number": int, "follow": Self}) with raises(MultipleInvalid): schema({"number": "abc"}) @@ -1188,45 +1227,47 @@ def test_self_validation(): def test_any_error_has_path(): """https://github.com/alecthomas/voluptuous/issues/347""" - s = Schema({ - Optional('q'): int, - Required('q2'): Any(int, msg='toto') - }) + 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']) - ) + 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'), - }) + 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)) + 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']) - ) + 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"), - }) + s = Schema( + { + Required('q2'): Match("a"), + } + ) with pytest.raises(MultipleInvalid) as ctx: s({'q2': 12}) assert len(ctx.value.errors) == 1 @@ -1305,8 +1346,7 @@ def __hash__(self): def test_self_any(): - schema = Schema({"number": int, - "follow": Any(Self, "stop")}) + schema = Schema({"number": int, "follow": Any(Self, "stop")}) with pytest.raises(MultipleInvalid) as ctx: schema({"number": "abc"}) assert len(ctx.value.errors) == 1 @@ -1321,11 +1361,13 @@ def test_self_any(): def test_self_all(): - schema = Schema({"number": int, - "follow": All(Self, - Schema({"extra_number": int}, - extra=ALLOW_EXTRA))}, - extra=ALLOW_EXTRA) + 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 @@ -1347,7 +1389,10 @@ def test_self_all(): def test_SomeOf_on_bounds_assertion(): - with raises(AssertionError, 'when using "SomeOf" you should specify at least one of min_valid and max_valid'): + with raises( + AssertionError, + 'when using "SomeOf" you should specify at least one of min_valid and max_valid', + ): SomeOf(validators=[]) @@ -1433,7 +1478,9 @@ def test_upper_util_handles_various_inputs(): 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(b'\xe2\x98\x83'.decode("UTF-8")) == b'\xe2\x98\x83'.decode( + "UTF-8" + ) assert Capitalize(u"aaa aaa") == u"Aaa aaa" @@ -1459,20 +1506,21 @@ def test_any_required(): def test_any_required_with_subschema(): - schema = Schema(Any({'a': Any(float, int)}, - {'b': int}, - {'c': {'aa': int}}, - required=True)) + 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, - }) + schema = Schema( + { + Inclusive('x', 'stuff'): int, + Inclusive('y', 'stuff'): int, + } + ) r = schema({}) assert r == {} @@ -1480,28 +1528,38 @@ def test_inclusive(): 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[]"): + 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, - }) + 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[]"): + 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, - }) + schema = Schema( + { + Exclusive('x', 'stuff'): int, + Exclusive('y', 'stuff'): int, + } + ) r = schema({}) assert r == {} @@ -1509,30 +1567,40 @@ def test_exclusive(): r = schema({'x': 1}) assert r == {'x': 1} - with raises(MultipleInvalid, "two or more values in the same group of exclusion 'stuff' @ data[]"): + 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 - } - }) + 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(): @@ -1541,12 +1609,14 @@ def as_int(a): schema = Schema({as_int: str}) with pytest.raises(MultipleInvalid) as ctx: - schema({ - '1': 'one', - 'two': '2', - '3': 'three', - 'four': '4', - }) + 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']" @@ -1562,12 +1632,14 @@ def as_int(a): schema = Schema({as_int: str}) with pytest.raises(MultipleInvalid) as ctx: - schema({ - '1': 'one', - 'two': '2', - '3': 'three', - 'four': '4', - }) + 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']" @@ -1575,6 +1647,7 @@ def as_int(a): def test_coerce_enum(): """Test Coerce Enum""" + class Choice(Enum): Easy = 1 Medium = 2 diff --git a/voluptuous/util.py b/voluptuous/util.py index 67021ef4..0bf93022 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,9 +1,13 @@ # F401: "imported but unused" -from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid # noqa: F401 -from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401 +# 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 -import typing +from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401 + +# fmt: on __author__ = 'tusharmakkar08' @@ -121,8 +125,7 @@ def __call__(self, v): try: set_v = set(v) except Exception as e: - raise TypeInvalid( - self.msg or 'cannot be presented as set: {0}'.format(e)) + raise TypeInvalid(self.msg or 'cannot be presented as set: {0}'.format(e)) return set_v def __repr__(self): @@ -135,9 +138,7 @@ def __init__(self, lit) -> None: def __call__(self, value, msg: typing.Optional[str] = None): if self.lit != value: - raise LiteralInvalid( - msg or '%s not match for %s' % (value, self.lit) - ) + raise LiteralInvalid(msg or '%s not match for %s' % (value, self.lit)) else: return self.lit diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 229bab7d..a372afe5 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -1,18 +1,25 @@ -from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, - AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, - RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, - DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, - TooManyValid) - -# F401: flake8 complains about 'raises' not being used, but it is used in doctests -from voluptuous.schema_builder import Schema, raises, message, Schemable # noqa: F401 +# fmt: off +import datetime import os import re -import datetime import sys -from functools import wraps -from decimal import Decimal, InvalidOperation 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: @@ -29,6 +36,7 @@ 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 "(?:" @@ -40,20 +48,23 @@ r"""\\[\001-\011\013\014\016-\177])*"$)""" # end anchor, because fullmatch is not available in python 2.7 r")\Z", - re.IGNORECASE + re.IGNORECASE, ) DOMAIN_REGEX = re.compile( # start anchor, because fullmatch is not available in python 2.7 "(?:" # domain r'(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' + # tld r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?$)' # literal form, ipv4 address (SMTP 4.1.3) r'|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)' r'(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]$' # end anchor, because fullmatch is not available in python 2.7 r")\Z", - re.IGNORECASE) + re.IGNORECASE, +) +# fmt: on __author__ = 'tusharmakkar08' @@ -61,14 +72,14 @@ def truth(f: typing.Callable) -> typing.Callable: """Convenience decorator to convert truth functions into validators. - >>> @truth - ... def isdir(v): - ... return os.path.isdir(v) - >>> validate = Schema(isdir) - >>> validate('/') - '/' - >>> with raises(MultipleInvalid, 'not a valid value'): - ... validate('/notavaliddir') + >>> @truth + ... def isdir(v): + ... return os.path.isdir(v) + >>> validate = Schema(isdir) + >>> validate('/') + '/' + >>> with raises(MultipleInvalid, 'not a valid value'): + ... validate('/notavaliddir') """ @wraps(f) @@ -102,7 +113,11 @@ class Coerce(object): ... validate('foo') """ - def __init__(self, type: typing.Union[type, typing.Callable], msg: typing.Optional[str] = None) -> None: + def __init__( + self, + type: typing.Union[type, typing.Callable], + msg: typing.Optional[str] = None, + ) -> None: self.type = type self.msg = msg self.type_name = type.__name__ @@ -208,7 +223,9 @@ class _WithSubValidators(object): sub-validators are compiled by the parent `Schema`. """ - def __init__(self, *validators, msg=None, required=False, discriminant=None, **kwargs) -> None: + def __init__( + self, *validators, msg=None, required=False, discriminant=None, **kwargs + ) -> None: self.validators = validators self.msg = msg self.required = required @@ -240,10 +257,15 @@ def __repr__(self): return '%s(%s, msg=%r)' % ( self.__class__.__name__, ", ".join(repr(v) for v in self.validators), - self.msg + self.msg, ) - def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[typing.Hashable]] = None): + def _exec( + self, + funcs: typing.Iterable, + v, + path: typing.Optional[typing.List[typing.Hashable]] = None, + ): raise NotImplementedError() @@ -285,10 +307,8 @@ def _exec(self, funcs, v, path=None): error = e else: if error: - raise error if self.msg is None else AnyInvalid( - self.msg, path=path) - raise AnyInvalid(self.msg or 'no valid value found', - path=path) + raise error if self.msg is None else AnyInvalid(self.msg, path=path) + raise AnyInvalid(self.msg or 'no valid value found', path=path) # Convenience alias @@ -329,10 +349,8 @@ def _exec(self, funcs, v, path=None): error = e else: if error: - raise error if self.msg is None else AnyInvalid( - self.msg, path=path) - raise AnyInvalid(self.msg or 'no valid value found', - path=path) + raise error if self.msg is None else AnyInvalid(self.msg, path=path) + raise AnyInvalid(self.msg or 'no valid value found', path=path) # Convenience alias @@ -387,7 +405,9 @@ class Match(object): '0x123ef4' """ - def __init__(self, pattern: typing.Union[re.Pattern, str], msg: typing.Optional[str] = None) -> None: + def __init__( + self, pattern: typing.Union[re.Pattern, str], msg: typing.Optional[str] = None + ) -> None: if isinstance(pattern, basestring): pattern = re.compile(pattern) self.pattern = pattern @@ -399,7 +419,10 @@ def __call__(self, 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)) + raise MatchInvalid( + self.msg + or 'does not match regular expression {}'.format(self.pattern.pattern) + ) return v def __repr__(self): @@ -415,7 +438,12 @@ class Replace(object): 'I say goodbye' """ - def __init__(self, pattern: typing.Union[re.Pattern, str], substitution: str, msg: typing.Optional[str] = None) -> None: + def __init__( + self, + pattern: typing.Union[re.Pattern, str], + substitution: str, + msg: typing.Optional[str] = None, + ) -> None: if isinstance(pattern, basestring): pattern = re.compile(pattern) self.pattern = pattern @@ -426,9 +454,11 @@ 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) + return 'Replace(%r, %r, msg=%r)' % ( + self.pattern.pattern, + self.substitution, + self.msg, + ) def _url_validation(v: str) -> urlparse.ParseResult: @@ -604,9 +634,14 @@ class Range(object): ... 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: + 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 @@ -618,33 +653,40 @@ def __call__(self, v): if self.min_included: if self.min is not None and not v >= self.min: raise RangeInvalid( - self.msg or 'value must be at least %s' % self.min) + self.msg or 'value must be at least %s' % self.min + ) else: if self.min is not None and not v > self.min: raise RangeInvalid( - self.msg or 'value must be higher than %s' % self.min) + self.msg or 'value must be higher than %s' % self.min + ) if self.max_included: if self.max is not None and not v <= self.max: raise RangeInvalid( - self.msg or 'value must be at most %s' % self.max) + self.msg or 'value must be at most %s' % self.max + ) else: if self.max is not None and not v < self.max: raise RangeInvalid( - self.msg or 'value must be lower than %s' % self.max) + self.msg or 'value must be lower than %s' % self.max + ) return v # Objects that lack a partial ordering, e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid( - self.msg or 'invalid value or type (must have a partial ordering)') + self.msg or 'invalid value or type (must have a partial ordering)' + ) def __repr__(self): - return ('Range(min=%r, max=%r, min_included=%r,' - ' max_included=%r, msg=%r)' % (self.min, self.max, - self.min_included, - self.max_included, - self.msg)) + return 'Range(min=%r, max=%r, min_included=%r, max_included=%r, msg=%r)' % ( + self.min, + self.max, + self.min_included, + self.max_included, + self.msg, + ) class Clamp(object): @@ -661,8 +703,12 @@ class Clamp(object): 0 """ - def __init__(self, min: NullableNumber = None, max: NullableNumber = None, - msg: typing.Optional[str] = None) -> None: + def __init__( + self, + min: NullableNumber = None, + max: NullableNumber = None, + msg: typing.Optional[str] = None, + ) -> None: self.min = min self.max = max self.msg = msg @@ -678,7 +724,8 @@ def __call__(self, v): # Objects that lack a partial ordering, e.g. None or strings will raise TypeError except TypeError: raise RangeInvalid( - self.msg or 'invalid value or type (must have a partial ordering)') + self.msg or 'invalid value or type (must have a partial ordering)' + ) def __repr__(self): return 'Clamp(min=%s, max=%s)' % (self.min, self.max) @@ -687,8 +734,12 @@ def __repr__(self): 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: + def __init__( + self, + min: NullableNumber = None, + max: NullableNumber = None, + msg: typing.Optional[str] = None, + ) -> None: self.min = min self.max = max self.msg = msg @@ -697,16 +748,17 @@ 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) + self.msg or 'length of value must be at least %s' % self.min + ) if self.max is not None and len(v) > self.max: raise LengthInvalid( - self.msg or 'length of value must be at most %s' % self.max) + self.msg or 'length of value must be at most %s' % self.max + ) return v # Objects that have no length e.g. None or strings will raise TypeError except TypeError: - raise RangeInvalid( - self.msg or 'invalid value or type') + raise RangeInvalid(self.msg or 'invalid value or type') def __repr__(self): return 'Length(min=%s, max=%s)' % (self.min, self.max) @@ -717,7 +769,9 @@ class Datetime(object): DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - def __init__(self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None) -> None: + def __init__( + self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None + ) -> None: self.format = format or self.DEFAULT_FORMAT self.msg = msg @@ -726,8 +780,8 @@ def __call__(self, v): datetime.datetime.strptime(v, self.format) except (TypeError, ValueError): raise DatetimeInvalid( - self.msg or 'value does not match' - ' expected format %s' % self.format) + self.msg or 'value does not match expected format %s' % self.format + ) return v def __repr__(self): @@ -744,8 +798,8 @@ def __call__(self, v): datetime.datetime.strptime(v, self.format) except (TypeError, ValueError): raise DateInvalid( - self.msg or 'value does not match' - ' expected format %s' % self.format) + self.msg or 'value does not match expected format %s' % self.format + ) return v def __repr__(self): @@ -755,7 +809,9 @@ def __repr__(self): class In(object): """Validate that a value is in a collection.""" - def __init__(self, container: typing.Iterable, msg: typing.Optional[str] = None) -> None: + def __init__( + self, container: typing.Iterable, msg: typing.Optional[str] = None + ) -> None: self.container = container self.msg = msg @@ -765,8 +821,9 @@ def __call__(self, v): except TypeError: check = True if check: - raise InInvalid(self.msg - or 'value must be one of {}'.format(sorted(self.container))) + raise InInvalid( + self.msg or 'value must be one of {}'.format(sorted(self.container)) + ) return v def __repr__(self): @@ -776,7 +833,9 @@ def __repr__(self): class NotIn(object): """Validate that a value is not in a collection.""" - def __init__(self, container: typing.Iterable, msg: typing.Optional[str] = None) -> None: + def __init__( + self, container: typing.Iterable, msg: typing.Optional[str] = None + ) -> None: self.container = container self.msg = msg @@ -786,8 +845,9 @@ def __call__(self, v): except TypeError: check = True if check: - raise NotInInvalid(self.msg - or 'value must not be one of {}'.format(sorted(self.container))) + raise NotInInvalid( + self.msg or 'value must not be one of {}'.format(sorted(self.container)) + ) return v def __repr__(self): @@ -837,7 +897,12 @@ class ExactSequence(object): ('hourly_report', 10, [], []) """ - def __init__(self, validators: typing.Iterable[Schemable], msg: typing.Optional[str] = None, **kwargs) -> None: + 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] @@ -852,8 +917,7 @@ def __call__(self, v): return v def __repr__(self): - return 'ExactSequence([%s])' % (", ".join(repr(v) - for v in self.validators)) + return 'ExactSequence([%s])' % ", ".join(repr(v) for v in self.validators) class Unique(object): @@ -889,13 +953,11 @@ def __call__(self, v): try: set_v = set(v) except TypeError as e: - raise TypeInvalid( - self.msg or 'contains unhashable elements: {0}'.format(e)) + raise TypeInvalid(self.msg or 'contains unhashable elements: {0}'.format(e)) if len(set_v) != len(v): seen = set() dupes = list(set(x for x in v if x in seen or seen.add(x))) - raise Invalid( - self.msg or 'contains duplicate items: {0}'.format(dupes)) + raise Invalid(self.msg or 'contains duplicate items: {0}'.format(dupes)) return v def __repr__(self): @@ -924,7 +986,10 @@ def __init__(self, target, msg: typing.Optional[str] = None) -> None: def __call__(self, v): if v != self.target: - raise Invalid(self.msg or 'Values are not equal: value:{} != target:{}'.format(v, self.target)) + raise Invalid( + self.msg + or 'Values are not equal: value:{} != target:{}'.format(v, self.target) + ) return v def __repr__(self): @@ -946,8 +1011,12 @@ class Unordered(object): [1, 'foo'] """ - def __init__(self, validators: typing.Iterable[Schemable], - msg: typing.Optional[str] = None, **kwargs) -> None: + 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] @@ -957,7 +1026,12 @@ def __call__(self, v): raise Invalid(self.msg or 'Value {} is not sequence!'.format(v)) if len(v) != len(self._schemas): - raise Invalid(self.msg or 'List lengths differ, value:{} != target:{}'.format(len(v), len(self._schemas))) + raise Invalid( + self.msg + or 'List lengths differ, value:{} != target:{}'.format( + len(v), len(self._schemas) + ) + ) consumed = set() missing = [] @@ -979,10 +1053,24 @@ def __call__(self, v): if len(missing) == 1: el = missing[0] - raise Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format(el[0], el[1])) + raise Invalid( + self.msg + or 'Element #{} ({}) is not valid against any validator'.format( + el[0], el[1] + ) + ) elif missing: - raise MultipleInvalid([Invalid(self.msg or 'Element #{} ({}) is not valid against any validator'.format( - el[0], el[1])) for el in missing]) + raise MultipleInvalid( + [ + Invalid( + self.msg + or 'Element #{} ({}) is not valid against any validator'.format( + el[0], el[1] + ) + ) + for el in missing + ] + ) return v def __repr__(self): @@ -1004,8 +1092,13 @@ class Number(object): 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: + 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 @@ -1018,13 +1111,22 @@ def __call__(self, v): """ precision, scale, decimal_num = self._get_precision_scale(v) - if self.precision is not None and self.scale is not None and precision != self.precision\ - and scale != self.scale: - raise Invalid(self.msg or "Precision must be equal to %s, and Scale must be equal to %s" % (self.precision, - self.scale)) + if ( + self.precision is not None + and self.scale is not None + and precision != self.precision + and scale != self.scale + ): + raise Invalid( + self.msg + or "Precision must be equal to %s, and Scale must be equal to %s" + % (self.precision, self.scale) + ) else: if self.precision is not None and precision != self.precision: - raise Invalid(self.msg or "Precision must be equal to %s" % self.precision) + raise Invalid( + self.msg or "Precision must be equal to %s" % self.precision + ) if self.scale is not None and scale != self.scale: raise Invalid(self.msg or "Scale must be equal to %s" % self.scale) @@ -1035,7 +1137,11 @@ def __call__(self, v): return v def __repr__(self): - return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg)) + return 'Number(precision=%s, scale=%s, msg=%s)' % ( + self.precision, + self.scale, + self.msg, + ) def _get_precision_scale(self, number) -> typing.Tuple[int, int, Decimal]: """ @@ -1080,11 +1186,17 @@ class SomeOf(_WithSubValidators): ... 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__,) + 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) @@ -1115,4 +1227,8 @@ def _exec(self, funcs, v, path=None): def __repr__(self): return 'SomeOf(min_valid=%s, validators=[%s], max_valid=%s, msg=%r)' % ( - self.min_valid, ", ".join(repr(v) for v in self.validators), self.max_valid, self.msg) + self.min_valid, + ", ".join(repr(v) for v in self.validators), + self.max_valid, + self.msg, + ) From 09d0f066a5f7b80973996c90c54a7bb00f62c905 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Wed, 31 Jan 2024 21:40:08 +0100 Subject: [PATCH 237/261] Ignore styling-only commit in git blame (#505) --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..08653ed8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Add linters configuration, reformat whole code (#503) +c5189d2b6f9dfb3016f094561afcfb5584d143ad From 503dd346a1be9ac016c43b902a6601ddeaa9bda8 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Thu, 1 Feb 2024 21:17:46 +0100 Subject: [PATCH 238/261] fix: allow unsortable containers in In and NotIn validators (fixes #451) (#506) credits to: @spacegaier and @beastd --- voluptuous/tests/tests.py | 37 ++++++++++++++++++++++++++++++++++++- voluptuous/validators.py | 24 ++++++++++++++++++------ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 05b7e8e7..7286b313 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -79,19 +79,54 @@ def test_in(): 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 value @ data\['color'\]", + 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')}) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index a372afe5..99517381 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -821,9 +821,15 @@ def __call__(self, v): except TypeError: check = True if check: - raise InInvalid( - self.msg or 'value must be one of {}'.format(sorted(self.container)) - ) + try: + raise InInvalid( + self.msg or f'value must be one of {sorted(self.container)}' + ) + except TypeError: + raise InInvalid( + self.msg + or f'value must be one of {sorted(self.container, key=str)}' + ) return v def __repr__(self): @@ -845,9 +851,15 @@ def __call__(self, v): except TypeError: check = True if check: - raise NotInInvalid( - self.msg or 'value must not be one of {}'.format(sorted(self.container)) - ) + try: + raise NotInInvalid( + self.msg or f'value must not be one of {sorted(self.container)}' + ) + except TypeError: + raise NotInInvalid( + self.msg + or f'value must not be one of {sorted(self.container, key=str)}' + ) return v def __repr__(self): From 28a799a6f7744367599078ef19b571d6758cf271 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Thu, 1 Feb 2024 21:18:30 +0100 Subject: [PATCH 239/261] docs: document description field of Marker (#507) --- voluptuous/schema_builder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 6ad8758c..f7427f82 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1020,7 +1020,11 @@ def __repr__(self): class Marker(object): - """Mark nodes for special treatment.""" + """Mark nodes for special treatment. + + `description` is an optional field, unused by Voluptuous itself, but can be used + introspected by any external tool, for example to generate schema documentation. + """ def __init__( self, From 1fcf849f16279d43010c89c5d8cf69d5de96a800 Mon Sep 17 00:00:00 2001 From: Antoni Szych Date: Fri, 2 Feb 2024 21:44:49 +0100 Subject: [PATCH 240/261] Release 0.14.2 (#508) --- CHANGELOG.md | 18 ++++++++++++++++++ voluptuous/__init__.py | 4 ++-- voluptuous/schema_builder.py | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d6370c..f2a10bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # 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**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index ad6669bb..3138aafb 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -4,9 +4,9 @@ from voluptuous.util import * from voluptuous.validators import * -from voluptuous.error import * # isort: skip +from voluptuous.error import * # isort: skip # fmt: on -__version__ = '0.14.1' +__version__ = '0.14.2' __author__ = 'alecthomas' diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index f7427f82..a0449a2d 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1022,7 +1022,7 @@ def __repr__(self): class Marker(object): """Mark nodes for special treatment. - `description` is an optional field, unused by Voluptuous itself, but can be used + `description` is an optional field, unused by Voluptuous itself, but can be introspected by any external tool, for example to generate schema documentation. """ From ed7ca997558989b4e0f83984807346ad61a1421d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 22:18:53 -0500 Subject: [PATCH 241/261] Remove python 3.8 support (#514) python 3.8 goes EOL in 2024-10 --- .github/workflows/tests.yml | 1 - pyproject.toml | 4 ++-- setup.py | 3 +-- tox.ini | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c6111320..81b55468 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,6 @@ jobs: - { python-version: "3.11", session: "py311" } - { python-version: "3.10", session: "py310" } - { python-version: "3.9", session: "py39" } - - { python-version: "3.8", session: "py38" } steps: - name: Check out the repository diff --git a/pyproject.toml b/pyproject.toml index af6fa6bb..2fd32941 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py38", "py39", "py310", "py311", "py312"] +target-version = ["py39", "py310", "py311", "py312"] skip-string-normalization = true [tool.isort] @@ -8,7 +8,7 @@ profile = "black" multi_line_output = 5 [tool.mypy] -python_version = "3.8" +python_version = "3.9" warn_unused_ignores = true diff --git a/setup.py b/setup.py index efaa50c2..a889e41a 100644 --- a/setup.py +++ b/setup.py @@ -27,14 +27,13 @@ }, author='Alec Thomas', author_email='alec@swapoff.org', - python_requires=">=3.8", + python_requires=">=3.9", 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', diff --git a/tox.ini b/tox.ini index 1296aabc..acb505dc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = flake8,black,py38,py39,py310,py311,py312 +envlist = flake8,black,py39,py310,py311,py312 [flake8] ; E501: line too long (X > 79 characters) From 9451b613c0b08f79cdb8ec6ff7e214fd3254c6ea Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 16 Jun 2024 18:39:11 +0200 Subject: [PATCH 242/261] Add `Any` type to defaults (#512) --- voluptuous/schema_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index a0449a2d..dba71ba3 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1093,7 +1093,7 @@ def __init__( self, schema: Schemable, msg: typing.Optional[str] = None, - default=UNDEFINED, + default: typing.Any = UNDEFINED, description: typing.Optional[str] = None, ) -> None: super(Optional, self).__init__(schema, msg=msg, description=description) @@ -1195,7 +1195,7 @@ def __init__( group_of_inclusion: str, msg: typing.Optional[str] = None, description: typing.Optional[str] = None, - default=UNDEFINED, + default: typing.Any = UNDEFINED, ) -> None: super(Inclusive, self).__init__( schema, msg=msg, default=default, description=description @@ -1222,7 +1222,7 @@ def __init__( self, schema: Schemable, msg: typing.Optional[str] = None, - default=UNDEFINED, + default: typing.Any = UNDEFINED, description: typing.Optional[str] = None, ) -> None: super(Required, self).__init__(schema, msg=msg, description=description) From b1b1090bd9f62fc5695b54512d48cb5827e6f18b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 16 Jun 2024 11:40:20 -0500 Subject: [PATCH 243/261] Only calculate hash once for Marker objects (#513) --- voluptuous/schema_builder.py | 20 +++++++++++++------- voluptuous/tests/tests.py | 1 + 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index dba71ba3..ca134cae 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -9,7 +9,7 @@ import typing from collections.abc import Generator from contextlib import contextmanager -from functools import wraps +from functools import cache, wraps from voluptuous import error as er from voluptuous.error import Error @@ -1026,6 +1026,8 @@ class Marker(object): introspected by any external tool, for example to generate schema documentation. """ + __slots__ = ('schema', '_schema', 'msg', 'description', '__hash__') + def __init__( self, schema_: Schemable, @@ -1036,6 +1038,7 @@ def __init__( self._schema = Schema(schema_) self.msg = msg self.description = description + self.__hash__ = cache(lambda: hash(schema_)) # type: ignore[method-assign] def __call__(self, v): try: @@ -1056,9 +1059,6 @@ def __lt__(self, other): return self.schema < other.schema return self.schema < other - def __hash__(self): - return hash(self.schema) - def __eq__(self, other): return self.schema == other @@ -1244,6 +1244,15 @@ class Remove(Marker): [1, 2, 3, 5, '7'] """ + def __init__( + self, + schema_: Schemable, + msg: typing.Optional[str] = None, + description: typing.Optional[str] = None, + ) -> None: + super().__init__(schema_, msg, description) + self.__hash__ = cache(lambda: object.__hash__(self)) # type: ignore[method-assign] + def __call__(self, schema: Schemable): super(Remove, self).__call__(schema) return self.__class__ @@ -1251,9 +1260,6 @@ def __call__(self, schema: Schemable): def __repr__(self): return "Remove(%r)" % (self.schema,) - def __hash__(self): - return object.__hash__(self) - def message( default: typing.Optional[str] = None, diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 7286b313..f6d6784b 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1072,6 +1072,7 @@ def test_marker_hashable(): assert definition.get('y') == float assert Required('x') == Required('x') assert Required('x') != Required('y') + assert hash(Required('x').schema) == hash(Required('x')) # Remove markers are not hashable assert definition.get('j') is None From 2232c0e556bc68343388fa8499bdbf13196a7514 Mon Sep 17 00:00:00 2001 From: spacegaier Date: Sun, 23 Jun 2024 23:03:41 +0200 Subject: [PATCH 244/261] Release 0.15.0 --- CHANGELOG.md | 11 +++++++++++ voluptuous/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a10bc9..0e181896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.15.0] + +**Fixes**: + +* [#512](https://github.com/alecthomas/voluptuous/pull/512): Add Any type to defaults +* [#513](https://github.com/alecthomas/voluptuous/pull/513): Only calculate hash once for Marker objects + +**Changes**: + +* [#514](https://github.com/alecthomas/voluptuous/pull/514): Remove python 3.8 support + ## [0.14.2] **New**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 3138aafb..f8095215 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -8,5 +8,5 @@ # fmt: on -__version__ = '0.14.2' +__version__ = '0.15.0' __author__ = 'alecthomas' From ca4006a6838be83f682c594bc15f7ea3d79cc80f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Jun 2024 16:55:31 -0500 Subject: [PATCH 245/261] Fix vol.Remove not removing keys that do not validate (#515) * Fix vol.Remove not removing keys fixes a regression from #479 blocks https://github.com/alecthomas/voluptuous/pull/479 * add test --- voluptuous/schema_builder.py | 6 +++--- voluptuous/tests/tests.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index ca134cae..2bf69544 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -435,11 +435,11 @@ def validate_mapping(path, iterable, out): break else: - if error: - errors.append(error) - elif remove_key: + if remove_key: # remove key continue + elif error: + errors.append(error) elif self.extra == ALLOW_EXTRA: out[key] = value elif self.extra != REMOVE_EXTRA: diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index f6d6784b..8fa1883c 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -176,6 +176,29 @@ def test_remove(): assert out_ == [1, 2, 1.0, 4] +def test_remove_with_error(): + def starts_with_dot(key: str) -> str: + """Check if key starts with dot.""" + if not key.startswith("."): + raise Invalid("Key does not start with .") + return key + + def does_not_start_with_dot(key: str) -> str: + """Check if key does not start with dot.""" + if key.startswith("."): + raise Invalid("Key starts with .") + return key + + schema = Schema( + { + Remove(All(str, starts_with_dot)): object, + does_not_start_with_dot: Any(None), + } + ) + out_ = schema({".remove": None, "ok": None}) + assert ".remove" not in out_ and "ok" in out_ + + def test_extra_empty_errors(): schema = Schema({'a': {Extra: object}}, required=True) schema({'a': {}}) From 65f736cf6fde9bae22aa643495cbda384cb3df59 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:55:29 +0200 Subject: [PATCH 246/261] Improve validator typing to allow non-number formats for min and max (#516) --- voluptuous/validators.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 99517381..89eed0f9 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -1,4 +1,6 @@ # fmt: off +from __future__ import annotations + import datetime import os import re @@ -18,6 +20,9 @@ # 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 +if typing.TYPE_CHECKING: + from _typeshed import SupportsAllComparisons + # fmt: on @@ -610,9 +615,6 @@ def Maybe(validator: typing.Callable, msg: typing.Optional[str] = None): return Any(None, validator, msg=msg) -NullableNumber = typing.Union[int, float, None] - - class Range(object): """Limit a value to a range. @@ -636,8 +638,8 @@ class Range(object): def __init__( self, - min: NullableNumber = None, - max: NullableNumber = None, + min: SupportsAllComparisons | None = None, + max: SupportsAllComparisons | None = None, min_included: bool = True, max_included: bool = True, msg: typing.Optional[str] = None, @@ -705,8 +707,8 @@ class Clamp(object): def __init__( self, - min: NullableNumber = None, - max: NullableNumber = None, + min: SupportsAllComparisons | None = None, + max: SupportsAllComparisons | None = None, msg: typing.Optional[str] = None, ) -> None: self.min = min @@ -736,8 +738,8 @@ class Length(object): def __init__( self, - min: NullableNumber = None, - max: NullableNumber = None, + min: SupportsAllComparisons | None = None, + max: SupportsAllComparisons | None = None, msg: typing.Optional[str] = None, ) -> None: self.min = min From 4a610d38c8bc895683fbb3b81afb7ac836840904 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 25 Jun 2024 23:54:46 +0200 Subject: [PATCH 247/261] Use typing.Container for In validator (#518) --- voluptuous/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 89eed0f9..cd002784 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -812,7 +812,7 @@ class In(object): """Validate that a value is in a collection.""" def __init__( - self, container: typing.Iterable, msg: typing.Optional[str] = None + self, container: typing.Container, msg: typing.Optional[str] = None ) -> None: self.container = container self.msg = msg From 66df7bef3790af39b330832f0dc3d0be742c8171 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 26 Jun 2024 01:15:04 +0200 Subject: [PATCH 248/261] Remove Maybe validator typing (#517) * Remove Maybe validator typing * Use Schemable as validator type --- voluptuous/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index cd002784..d385260e 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -599,7 +599,7 @@ def PathExists(v): raise PathInvalid("Not a Path") -def Maybe(validator: typing.Callable, msg: typing.Optional[str] = None): +def Maybe(validator: Schemable, 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 From 4cde166a39c9438a666a128eebe0996ce94c3974 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:05:05 +0200 Subject: [PATCH 249/261] Don't enforce type for unused description attribute (#519) --- voluptuous/schema_builder.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 2bf69544..56935c4a 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1032,7 +1032,7 @@ def __init__( self, schema_: Schemable, msg: typing.Optional[str] = None, - description: typing.Optional[str] = None, + description: typing.Any | None = None, ) -> None: self.schema = schema_ self._schema = Schema(schema_) @@ -1094,7 +1094,7 @@ def __init__( schema: Schemable, msg: typing.Optional[str] = None, default: typing.Any = UNDEFINED, - description: typing.Optional[str] = None, + description: typing.Any | None = None, ) -> None: super(Optional, self).__init__(schema, msg=msg, description=description) self.default = default_factory(default) @@ -1141,7 +1141,7 @@ def __init__( schema: Schemable, group_of_exclusion: str, msg: typing.Optional[str] = None, - description: typing.Optional[str] = None, + description: typing.Any | None = None, ) -> None: super(Exclusive, self).__init__(schema, msg=msg, description=description) self.group_of_exclusion = group_of_exclusion @@ -1194,7 +1194,7 @@ def __init__( schema: Schemable, group_of_inclusion: str, msg: typing.Optional[str] = None, - description: typing.Optional[str] = None, + description: typing.Any | None = None, default: typing.Any = UNDEFINED, ) -> None: super(Inclusive, self).__init__( @@ -1223,7 +1223,7 @@ def __init__( schema: Schemable, msg: typing.Optional[str] = None, default: typing.Any = UNDEFINED, - description: typing.Optional[str] = None, + description: typing.Any | None = None, ) -> None: super(Required, self).__init__(schema, msg=msg, description=description) self.default = default_factory(default) @@ -1248,7 +1248,7 @@ def __init__( self, schema_: Schemable, msg: typing.Optional[str] = None, - description: typing.Optional[str] = None, + description: typing.Any | None = None, ) -> None: super().__init__(schema_, msg, description) self.__hash__ = cache(lambda: object.__hash__(self)) # type: ignore[method-assign] From 16163aaeab0f99317438582aa10c26783605df5a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 27 Jun 2024 00:05:36 +0200 Subject: [PATCH 250/261] Type schema attribute as Any (#521) --- voluptuous/schema_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 56935c4a..237d5a9b 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -208,7 +208,7 @@ def __init__( - Any value other than the above defaults to :const:`~voluptuous.PREVENT_EXTRA` """ - self.schema = schema + self.schema: typing.Any = schema self.required = required self.extra = int(extra) # ensure the value is an integer self._compiled = self._compile(schema) @@ -1034,7 +1034,7 @@ def __init__( msg: typing.Optional[str] = None, description: typing.Any | None = None, ) -> None: - self.schema = schema_ + self.schema: typing.Any = schema_ self._schema = Schema(schema_) self.msg = msg self.description = description From 05c5e79e4eec87c9930673a8f3dd943c628c4dfb Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 26 Jun 2024 08:01:03 +1000 Subject: [PATCH 251/261] chore: move doc string to top of module --- voluptuous/__init__.py | 75 +++++++++++++++++++++++++++++++++++ voluptuous/schema_builder.py | 77 +----------------------------------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index f8095215..99fb3c9f 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -1,3 +1,78 @@ +"""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 +""" # flake8: noqa # fmt: off from voluptuous.schema_builder import * diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 237d5a9b..4b95cc7d 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1,3 +1,4 @@ + # fmt: off from __future__ import annotations @@ -16,82 +17,6 @@ # 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 From 85f14d89a451773be65f5c0803863d9071ce9793 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 27 Jun 2024 08:07:42 +1000 Subject: [PATCH 252/261] chore: add Hermit Python --- bin/.python3-3.12.3.pkg | 1 + bin/README.hermit.md | 7 +++++++ bin/activate-hermit | 21 ++++++++++++++++++++ bin/hermit | 43 +++++++++++++++++++++++++++++++++++++++++ bin/hermit.hcl | 0 bin/pip | 1 + bin/pip3 | 1 + bin/pip3.12 | 1 + bin/pydoc3 | 1 + bin/pydoc3.12 | 1 + bin/python | 1 + bin/python3 | 1 + bin/python3-config | 1 + bin/python3.12 | 1 + bin/python3.12-config | 1 + 15 files changed, 82 insertions(+) create mode 120000 bin/.python3-3.12.3.pkg create mode 100644 bin/README.hermit.md create mode 100755 bin/activate-hermit create mode 100755 bin/hermit create mode 100644 bin/hermit.hcl create mode 120000 bin/pip create mode 120000 bin/pip3 create mode 120000 bin/pip3.12 create mode 120000 bin/pydoc3 create mode 120000 bin/pydoc3.12 create mode 120000 bin/python create mode 120000 bin/python3 create mode 120000 bin/python3-config create mode 120000 bin/python3.12 create mode 120000 bin/python3.12-config diff --git a/bin/.python3-3.12.3.pkg b/bin/.python3-3.12.3.pkg new file mode 120000 index 00000000..383f4511 --- /dev/null +++ b/bin/.python3-3.12.3.pkg @@ -0,0 +1 @@ +hermit \ No newline at end of file diff --git a/bin/README.hermit.md b/bin/README.hermit.md new file mode 100644 index 00000000..e889550b --- /dev/null +++ b/bin/README.hermit.md @@ -0,0 +1,7 @@ +# Hermit environment + +This is a [Hermit](https://github.com/cashapp/hermit) bin directory. + +The symlinks in this directory are managed by Hermit and will automatically +download and install Hermit itself as well as packages. These packages are +local to this environment. diff --git a/bin/activate-hermit b/bin/activate-hermit new file mode 100755 index 00000000..fe28214d --- /dev/null +++ b/bin/activate-hermit @@ -0,0 +1,21 @@ +#!/bin/bash +# This file must be used with "source bin/activate-hermit" from bash or zsh. +# You cannot run it directly +# +# THIS FILE IS GENERATED; DO NOT MODIFY + +if [ "${BASH_SOURCE-}" = "$0" ]; then + echo "You must source this script: \$ source $0" >&2 + exit 33 +fi + +BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" +if "${BIN_DIR}/hermit" noop > /dev/null; then + eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" + + if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then + hash -r 2>/dev/null + fi + + echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" +fi diff --git a/bin/hermit b/bin/hermit new file mode 100755 index 00000000..7fef7692 --- /dev/null +++ b/bin/hermit @@ -0,0 +1,43 @@ +#!/bin/bash +# +# THIS FILE IS GENERATED; DO NOT MODIFY + +set -eo pipefail + +export HERMIT_USER_HOME=~ + +if [ -z "${HERMIT_STATE_DIR}" ]; then + case "$(uname -s)" in + Darwin) + export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit" + ;; + Linux) + export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit" + ;; + esac +fi + +export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" +HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" +export HERMIT_CHANNEL +export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} + +if [ ! -x "${HERMIT_EXE}" ]; then + echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 + INSTALL_SCRIPT="$(mktemp)" + # This value must match that of the install script + INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38" + if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then + curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" + else + # Install script is versioned by its sha256sum value + curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}" + # Verify install script's sha256sum + openssl dgst -sha256 "${INSTALL_SCRIPT}" | \ + awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \ + '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}' + fi + /bin/bash "${INSTALL_SCRIPT}" 1>&2 +fi + +exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" diff --git a/bin/hermit.hcl b/bin/hermit.hcl new file mode 100644 index 00000000..e69de29b diff --git a/bin/pip b/bin/pip new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/pip @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/pip3 b/bin/pip3 new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/pip3 @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/pip3.12 b/bin/pip3.12 new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/pip3.12 @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/pydoc3 b/bin/pydoc3 new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/pydoc3 @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/pydoc3.12 b/bin/pydoc3.12 new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/pydoc3.12 @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/python b/bin/python new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/python @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/python3 b/bin/python3 new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/python3 @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/python3-config b/bin/python3-config new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/python3-config @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/python3.12 b/bin/python3.12 new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/python3.12 @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file diff --git a/bin/python3.12-config b/bin/python3.12-config new file mode 120000 index 00000000..0289f6d3 --- /dev/null +++ b/bin/python3.12-config @@ -0,0 +1 @@ +.python3-3.12.3.pkg \ No newline at end of file From 23e178397d660f15fcf5544091f531d55dd24c37 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Thu, 27 Jun 2024 08:12:16 +1000 Subject: [PATCH 253/261] chore: bump version in __init__.py --- voluptuous/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 99fb3c9f..d00baa13 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -83,5 +83,5 @@ # fmt: on -__version__ = '0.15.0' +__version__ = '0.15.1' __author__ = 'alecthomas' From b24b80dec77a7ad035b851b8842a6629ad76a248 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 2 Jul 2024 20:54:57 +0200 Subject: [PATCH 254/261] Fix regression with ALLOW_EXTRA and Any validator (#522) --- voluptuous/__init__.py | 1 + voluptuous/schema_builder.py | 5 ++--- voluptuous/tests/tests.py | 17 +++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index d00baa13..3725e67e 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -73,6 +73,7 @@ ... 'Users': {'snmp_community': 'monkey'}}}} True """ + # flake8: noqa # fmt: off from voluptuous.schema_builder import * diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 4b95cc7d..cdeb5144 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1,4 +1,3 @@ - # fmt: off from __future__ import annotations @@ -363,10 +362,10 @@ def validate_mapping(path, iterable, out): if remove_key: # remove key continue - elif error: - errors.append(error) elif self.extra == ALLOW_EXTRA: out[key] = value + elif error: + errors.append(error) 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 diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 8fa1883c..77110d8f 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -1704,6 +1704,23 @@ def as_int(a): assert str(ctx.value.errors[1]) == "expecting a number @ data['four']" +def test_key3(): + schema = Schema( + { + Any("name", "area"): str, + "domain": str, + }, + extra=ALLOW_EXTRA, + ) + schema( + { + "name": "one", + "domain": "two", + "additional_key": "extra", + } + ) + + def test_coerce_enum(): """Test Coerce Enum""" From dcaaf3dd68be156253518a045feb1c4172dbd2d5 Mon Sep 17 00:00:00 2001 From: spacegaier Date: Tue, 2 Jul 2024 21:06:37 +0200 Subject: [PATCH 255/261] Release 0.15.2 --- CHANGELOG.md | 17 +++++++++++++++++ voluptuous/__init__.py | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e181896..8d80583d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.15.2] + +**Fixes**: + +* [522](https://github.com/alecthomas/voluptuous/pull/522) Fix regression with ALLOW_EXTRA and `Any` validator + +## [0.15.1] + +**Fixes**: + +* [515](https://github.com/alecthomas/voluptuous/pull/515) Fix `Remove` not removing keys that do not validate +* [516](https://github.com/alecthomas/voluptuous/pull/516) Improve validator typing to allow non-number formats for min and max +* [517](https://github.com/alecthomas/voluptuous/pull/517) Remove `Maybe` validator typing +* [518](https://github.com/alecthomas/voluptuous/pull/518) Use typing.Container for `In` validator +* [519](https://github.com/alecthomas/voluptuous/pull/519) Don't enforce type for unused description attribute +* [521](https://github.com/alecthomas/voluptuous/pull/521) Type schema attribute as `Any` + ## [0.15.0] **Fixes**: diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index 3725e67e..d030b351 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -84,5 +84,5 @@ # fmt: on -__version__ = '0.15.1' +__version__ = '0.15.2' __author__ = 'alecthomas' From a7a55f83b9fa7ba68b0669b3d78a61de703e0a16 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:58:21 +0200 Subject: [PATCH 256/261] Allow Generators for vol.In (#523) --- voluptuous/validators.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/voluptuous/validators.py b/voluptuous/validators.py index d385260e..3f026b19 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -812,7 +812,9 @@ class In(object): """Validate that a value is in a collection.""" def __init__( - self, container: typing.Container, msg: typing.Optional[str] = None + self, + container: typing.Container | typing.Iterable, + msg: typing.Optional[str] = None, ) -> None: self.container = container self.msg = msg From 4a9c8f8efba20622afdc68e4721787efb4f17472 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 30 Apr 2025 13:26:25 +0100 Subject: [PATCH 257/261] Fix bug with Any validator and REMOVE_EXTRA (#524) --- voluptuous/schema_builder.py | 6 ++- voluptuous/tests/tests.py | 79 ++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index cdeb5144..da207371 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -364,11 +364,13 @@ def validate_mapping(path, iterable, out): continue elif self.extra == ALLOW_EXTRA: out[key] = value + elif self.extra == REMOVE_EXTRA: + # ignore the key so it's removed from output + continue elif error: errors.append(error) - elif self.extra != REMOVE_EXTRA: + else: 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: diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 77110d8f..8d1b7929 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -8,7 +8,7 @@ import pytest from voluptuous import ( - ALLOW_EXTRA, PREVENT_EXTRA, All, AllInvalid, Any, Clamp, Coerce, Contains, + ALLOW_EXTRA, PREVENT_EXTRA, REMOVE_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, @@ -1704,7 +1704,7 @@ def as_int(a): assert str(ctx.value.errors[1]) == "expecting a number @ data['four']" -def test_key3(): +def test_any_with_extra_allow(): schema = Schema( { Any("name", "area"): str, @@ -1712,7 +1712,32 @@ def test_key3(): }, extra=ALLOW_EXTRA, ) - schema( + + result = schema( + { + "name": "one", + "domain": "two", + "additional_key": "extra", + } + ) + + assert result == { + "name": "one", + "domain": "two", + "additional_key": "extra", + } + + +def test_any_with_extra_remove(): + schema = Schema( + { + Any("name", "area"): str, + "domain": str, + }, + extra=REMOVE_EXTRA, + ) + + result = schema( { "name": "one", "domain": "two", @@ -1720,6 +1745,54 @@ def test_key3(): } ) + assert result == { + "name": "one", + "domain": "two", + } + + +def test_any_with_extra_prevent(): + schema = Schema( + { + Any("name", "area"): str, + "domain": str, + }, + extra=PREVENT_EXTRA, + ) + + with pytest.raises(MultipleInvalid) as ctx: + schema( + { + "name": "one", + "domain": "two", + "additional_key": "extra", + } + ) + + assert len(ctx.value.errors) == 1 + assert str(ctx.value.errors[0]) == "not a valid value @ data['additional_key']" + + +def test_any_with_extra_none(): + schema = Schema( + { + Any("name", "area"): str, + "domain": str, + }, + ) + + with pytest.raises(MultipleInvalid) as ctx: + schema( + { + "name": "one", + "domain": "two", + "additional_key": "extra", + } + ) + + assert len(ctx.value.errors) == 1 + assert str(ctx.value.errors[0]) == "not a valid value @ data['additional_key']" + def test_coerce_enum(): """Test Coerce Enum""" From 86bcb0347e5a3344be1073975e80fd73d7f3ab75 Mon Sep 17 00:00:00 2001 From: Manya Mittal <155902506+manyamittal25@users.noreply.github.com> Date: Sat, 2 Aug 2025 16:37:29 +0530 Subject: [PATCH 258/261] Add comprehensive tests for humanize.py module (#530) * Add comprehensive tests for humanize.py module to improve coverage from 71% to 100% - Add tests for _nested_getitem error handling (KeyError, IndexError, TypeError) - Add tests for humanize_error with long error message truncation - Add tests for validate_with_humanized_errors success and failure cases - Add tests for edge cases with None data and multiple validation errors - Improve overall test coverage from 89% to 90% This addresses the missing coverage in humanize.py lines 19-22, 45, and 54-57. * Fix import sorting with isort --- voluptuous/tests/tests.py | 158 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 7 deletions(-) diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 8d1b7929..71f8810b 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -8,13 +8,13 @@ import pytest from voluptuous import ( - ALLOW_EXTRA, PREVENT_EXTRA, REMOVE_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, + ALLOW_EXTRA, PREVENT_EXTRA, REMOVE_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 @@ -1846,3 +1846,147 @@ def test_exception(): 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']" + + +# Additional tests for humanize.py module to improve coverage +def test_humanize_error_with_nested_getitem_keyerror(): + """Test _nested_getitem with KeyError (line 19-22).""" + from voluptuous.humanize import _nested_getitem + + # Test KeyError handling + data = {'a': {'b': 1}} + path = ['a', 'c'] # 'c' doesn't exist in {'b': 1} + result = _nested_getitem(data, path) + assert result is None + + +def test_humanize_error_with_nested_getitem_indexerror(): + """Test _nested_getitem with IndexError (line 19-22).""" + from voluptuous.humanize import _nested_getitem + + # Test IndexError handling + data = {'a': [1, 2, 3]} + path = ['a', 5] # Index 5 doesn't exist in [1, 2, 3] + result = _nested_getitem(data, path) + assert result is None + + +def test_humanize_error_with_nested_getitem_typeerror(): + """Test _nested_getitem with TypeError (line 19-22).""" + from voluptuous.humanize import _nested_getitem + + # Test TypeError handling - data is not subscriptable + data = 42 # int is not subscriptable + path = ['a'] + result = _nested_getitem(data, path) + assert result is None + + +def test_humanize_error_with_long_error_message(): + """Test humanize_error with long error message that gets truncated (line 45).""" + from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH, humanize_error + + # Create a very long string that will be truncated + long_string = "x" * (MAX_VALIDATION_ERROR_ITEM_LENGTH + 10) + data = {'a': long_string} + schema = Schema({'a': int}) + + with pytest.raises(MultipleInvalid) as ctx: + schema(data) + + error_message = humanize_error(data, ctx.value, max_sub_error_length=50) + assert "..." in error_message + assert len(error_message.split("Got ")[1]) <= 53 # 50 + 3 for "..." + + +def test_validate_with_humanized_errors_success(): + """Test validate_with_humanized_errors with successful validation (line 54-57).""" + from voluptuous.humanize import validate_with_humanized_errors + + schema = Schema({'a': int, 'b': str}) + data = {'a': 42, 'b': 'hello'} + + result = validate_with_humanized_errors(data, schema) + assert result == data + + +def test_validate_with_humanized_errors_failure(): + """Test validate_with_humanized_errors with validation failure (line 54-57).""" + from voluptuous.humanize import Error, validate_with_humanized_errors + + schema = Schema({'a': int, 'b': str}) + data = {'a': 'not an int', 'b': 123} + + with pytest.raises(Error) as ctx: + validate_with_humanized_errors(data, schema) + + error_message = str(ctx.value) + assert "expected int for dictionary value @ data['a']" in error_message + assert "expected str for dictionary value @ data['b']" in error_message + assert "Got 'not an int'" in error_message + assert "Got 123" in error_message + + +def test_validate_with_humanized_errors_custom_max_length(): + """Test validate_with_humanized_errors with custom max_sub_error_length.""" + from voluptuous.humanize import Error, validate_with_humanized_errors + + schema = Schema({'a': int}) + data = {'a': 'not an int'} + + with pytest.raises(Error) as ctx: + validate_with_humanized_errors(data, schema, max_sub_error_length=10) + + error_message = str(ctx.value) + assert "..." in error_message # Should be truncated + + +def test_humanize_error_with_multiple_invalid(): + """Test humanize_error with MultipleInvalid containing multiple errors.""" + from voluptuous.humanize import humanize_error + + schema = Schema({'a': int, 'b': str, 'c': [int]}) + data = {'a': 'not an int', 'b': 123, 'c': ['not an int']} + + with pytest.raises(MultipleInvalid) as ctx: + schema(data) + + error_message = humanize_error(data, ctx.value) + # Should contain all three error messages + assert "expected int for dictionary value @ data['a']" in error_message + assert "expected str for dictionary value @ data['b']" in error_message + assert "expected int @ data['c'][0]" in error_message + + +def test_humanize_error_with_single_invalid(): + """Test humanize_error with single Invalid error.""" + from voluptuous.humanize import humanize_error + + schema = Schema({'a': int}) + data = {'a': 'not an int'} + + with pytest.raises(MultipleInvalid) as ctx: + schema(data) + + error_message = humanize_error(data, ctx.value) + assert "expected int for dictionary value @ data['a']" in error_message + assert "Got 'not an int'" in error_message + + +def test_humanize_error_with_none_data(): + """Test humanize_error with None data.""" + from voluptuous.humanize import _nested_getitem, humanize_error + + # Test _nested_getitem with None data + result = _nested_getitem(None, ['a']) + assert result is None + + # Test humanize_error with None data + schema = Schema({'a': int}) + data = None + + with pytest.raises(MultipleInvalid) as ctx: + schema(data) + + error_message = humanize_error(data, ctx.value) + assert "expected a dictionary" in error_message From c5b63f80f156deeb4ef9646555eb41413edad7f5 Mon Sep 17 00:00:00 2001 From: Oleksii Date: Sun, 31 Aug 2025 11:10:23 +0300 Subject: [PATCH 259/261] i have run isort and updated few actions to latest version (#531) --- .github/workflows/tests.yml | 4 +-- voluptuous/tests/tests.py | 61 ++++++++++++++++++++++++++++++++----- voluptuous/validators.py | 30 +++++++++++++++--- 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 81b55468..71b89199 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,10 +25,10 @@ jobs: steps: - name: Check out the repository - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 71f8810b..11d7b9e8 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -8,13 +8,60 @@ import pytest from voluptuous import ( - ALLOW_EXTRA, PREVENT_EXTRA, REMOVE_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, + ALLOW_EXTRA, + PREVENT_EXTRA, + REMOVE_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 diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 3f026b19..da754156 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -10,11 +10,31 @@ 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, + 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 4cef6cee1019741ada6145698e78daa8d73c9353 Mon Sep 17 00:00:00 2001 From: Miguel Camba Date: Sun, 14 Dec 2025 05:05:07 +0100 Subject: [PATCH 260/261] Feature: Support requiring anyOf a list of keys (#534) * Feature: Support requiring anyOf a list of keys This adds a new feature to Voluptuous, which is somewhat akin to what json-schema does with the special key `anyOf`. `Schema({Required(Any('color', 'temperature', 'brightness')): str})` will validate that AT LEAST ONE of these three values is present. That doesn't preclude any individual validation on each of those fields to still apply. That means that in the above example, if `color` is present, brightness doesn't need to be present. But if brightness is present, all other validations of brightness (like checking that its value is a number between 0 and 100) still apply. * Simplify tests * Format stuff like black wants it --- voluptuous/schema_builder.py | 59 +++++++++++++++ voluptuous/tests/tests.py | 137 +++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index da207371..895c6e98 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -248,6 +248,13 @@ def _compile_mapping(self, schema, invalid_msg=None): ) ) + # Complex required keys that need special validation + complex_required_keys = set( + key + for key in all_required_keys + if isinstance(key, Required) and key.is_complex_key + ) + # Keys that may have defaults all_default_keys = set( key @@ -300,6 +307,22 @@ def validate_mapping(path, iterable, out): key_value_map[key.schema] = key.default() errors = [] + + # Check complex required keys - at least one candidate key must be present + for complex_key in complex_required_keys: + if not any( + candidate in key_value_map + for candidate in complex_key.candidate_keys + ): + msg = ( + complex_key.msg + if hasattr(complex_key, 'msg') and complex_key.msg + else f'at least one of {complex_key.candidate_keys} is required' + ) + errors.append(er.RequiredFieldInvalid(msg, path + [complex_key])) + else: + # If at least one candidate key is present, mark this complex requirement as satisfied + required_keys.discard(complex_key) for key, value in key_value_map.items(): key_path = path + [key] remove_key = False @@ -1142,6 +1165,17 @@ class Required(Marker): >>> schema = Schema({Required('key', default=list): list}) >>> schema({}) {'key': []} + + Complex key validation - at least one of the specified keys must be present: + + >>> from voluptuous.validators import Any + >>> schema = Schema({Required(Any('color', 'temperature', 'brightness')): str}) + >>> schema({'color': 'red'}) # Valid - has color + {'color': 'red'} + >>> schema({'temperature': 'warm'}) # Valid - has temperature + {'temperature': 'warm'} + >>> schema({'color': 'blue', 'brightness': 'high'}) # Valid - has multiple + {'color': 'blue', 'brightness': 'high'} """ def __init__( @@ -1153,6 +1187,31 @@ def __init__( ) -> None: super(Required, self).__init__(schema, msg=msg, description=description) self.default = default_factory(default) + self.is_complex_key = self._is_complex_key_validator(schema) + self.candidate_keys = ( + self._extract_candidate_keys(schema) if self.is_complex_key else None + ) + + def _is_complex_key_validator(self, schema): + """Check if schema is a validator that can match multiple keys.""" + # Import here to avoid circular imports + from voluptuous.validators import Any + + return isinstance(schema, Any) + + def _extract_candidate_keys(self, schema): + """Extract possible keys from validators like Any("key1", "key2", "key3").""" + # Import here to avoid circular imports + from voluptuous.validators import Any + + if isinstance(schema, Any): + # Extract literal values (strings, ints, etc.) from Any validators + return [ + v + for v in schema.validators + if isinstance(v, (str, int, float, bool, type(None))) + ] + return [] class Remove(Marker): diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 11d7b9e8..a3dd219f 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -2037,3 +2037,140 @@ def test_humanize_error_with_none_data(): error_message = humanize_error(data, ctx.value) assert "expected a dictionary" in error_message + + +def test_required_complex_key_any(): + """Test Required with Any validator for multiple possible keys""" + schema = Schema( + {Required(Any("color", "temperature", "brightness")): str, "device_id": str} + ) + + # Should pass - defines one of the required keys + result = schema({"color": "red", "device_id": "light1"}) + assert result == {"color": "red", "device_id": "light1"} + + # Should pass - defines several of the required keys + result = schema({"color": "blue", "brightness": "50%", "device_id": "light1"}) + assert result == {"color": "blue", "brightness": "50%", "device_id": "light1"} + + # Should fail - has none of the required keys + with pytest.raises(MultipleInvalid) as ctx: + schema({"device_id": "light1"}) + + error_msg = str(ctx.value) + assert ( + "at least one of ['color', 'temperature', 'brightness'] is required" + in error_msg + ) + + +def test_required_complex_key_custom_message(): + """Test Required with Any validator and custom error message""" + schema = Schema( + { + Required( + Any("color", "temperature", "brightness"), + msg="Please specify a lighting attribute", + ): str, + "device_id": str, + } + ) + + # Should pass + schema({"color": "red", "device_id": "light1"}) + + # Should fail with custom message + with pytest.raises(MultipleInvalid) as ctx: + schema({"device_id": "light1"}) + + error_msg = str(ctx.value) + assert "Please specify a lighting attribute" in error_msg + + +def test_required_complex_key_mixed_types(): + """Test Required with Any validator containing mixed key types""" + schema = Schema({Required(Any("string_key", 123, 45.6)): str, "other": int}) + + # Should work with string key + result = schema({"string_key": "value", "other": 1}) + assert result == {"string_key": "value", "other": 1} + + # Should work with int key + result = schema({123: "value", "other": 1}) + assert result == {123: "value", "other": 1} + + # Should work with float key + result = schema({45.6: "value", "other": 1}) + assert result == {45.6: "value", "other": 1} + + # Should fail with none present + with pytest.raises(MultipleInvalid) as ctx: + schema({"other": 1}) + + error_msg = str(ctx.value) + assert "at least one of ['string_key', 123, 45.6] is required" in error_msg + + +def test_required_complex_key_multiple_complex_requirements(): + """Test multiple Required complex keys in same schema""" + schema = Schema( + { + Required(Any("color", "hue")): str, + Required(Any("brightness", "intensity")): str, + "device": str, + } + ) + + # Should pass with one from each group + result = schema({"color": "red", "brightness": "high", "device": "light"}) + assert result == {"color": "red", "brightness": "high", "device": "light"} + + # Should fail if missing on any group + with pytest.raises(MultipleInvalid) as ctx: + schema({"brightness": "high", "device": "light"}) + + error_msg = str(ctx.value) + assert "at least one of ['color', 'hue'] is required" in error_msg + + +def test_required_complex_key_value_validation(): + """Test that value validation still works with complex required keys""" + schema = Schema({Required(Any("color", "temperature")): str, "device": str}) + + # Should pass with valid string value + result = schema({"color": "red", "device": "light"}) + assert result == {"color": "red", "device": "light"} + + # Should fail with invalid value type + with pytest.raises(MultipleInvalid) as ctx: + schema({"color": 123, "device": "light"}) # color should be str, not int + + error_msg = str(ctx.value) + assert "expected str" in error_msg + + +def test_complex_required_keys_with_specific_value_validation(): + """Test complex required keys combined with specific value validation for brightness range.""" + schema = Schema( + { + Required(Any('color', 'temperature', 'brightness')): object, + 'brightness': All( + Coerce(int), Range(min=0, max=100) + ), # Additional validation for brightness specifically + 'device_id': str, + } + ) + + # Valid - color provided, no brightness validation needed + result = schema({'color': 'red', 'device_id': 'light1'}) + assert result == {'color': 'red', 'device_id': 'light1'} + + # Invalid - brightness provided but out of range (255 > 100) + # Should NOT get "required field missing" error, but should get range error + with pytest.raises(MultipleInvalid) as exc_info: + schema({'brightness': '255', 'device_id': 'light1'}) + + # Verify it's a range error, not a missing required field error + error_msg = str(exc_info.value) + assert "required" not in error_msg.lower() # No "required field missing" error + assert "value must be at most 100" in error_msg # Range validation error From 87825d6dbdab8830fcc6d559ecb3b88bdf68af6d Mon Sep 17 00:00:00 2001 From: spacegaier Date: Thu, 18 Dec 2025 20:00:01 +0100 Subject: [PATCH 261/261] Release 0.16.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++-------- voluptuous/__init__.py | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d80583d..8f61df4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,21 +1,36 @@ # Changelog +## [0.16.0] + +**New**: + +* [#534](https://github.com/alecthomas/voluptuous/pull/534): Support requiring `anyOf` a list of keys + +**Fixes**: + +* [#523](https://github.com/alecthomas/voluptuous/pull/523): Allow Generators for `vol.In` +* [#524](https://github.com/alecthomas/voluptuous/pull/524): Fix bug with `Any` validator and `REMOVE_EXTRA` + +**Changes**: + +* [#530](https://github.com/alecthomas/voluptuous/pull/530): Add comprehensive tests for `humanize.py` module + ## [0.15.2] **Fixes**: -* [522](https://github.com/alecthomas/voluptuous/pull/522) Fix regression with ALLOW_EXTRA and `Any` validator +* [522](https://github.com/alecthomas/voluptuous/pull/522): Fix regression with ALLOW_EXTRA and `Any` validator ## [0.15.1] **Fixes**: -* [515](https://github.com/alecthomas/voluptuous/pull/515) Fix `Remove` not removing keys that do not validate -* [516](https://github.com/alecthomas/voluptuous/pull/516) Improve validator typing to allow non-number formats for min and max -* [517](https://github.com/alecthomas/voluptuous/pull/517) Remove `Maybe` validator typing -* [518](https://github.com/alecthomas/voluptuous/pull/518) Use typing.Container for `In` validator -* [519](https://github.com/alecthomas/voluptuous/pull/519) Don't enforce type for unused description attribute -* [521](https://github.com/alecthomas/voluptuous/pull/521) Type schema attribute as `Any` +* [515](https://github.com/alecthomas/voluptuous/pull/515): Fix `Remove` not removing keys that do not validate +* [516](https://github.com/alecthomas/voluptuous/pull/516): Improve validator typing to allow non-number formats for min and max +* [517](https://github.com/alecthomas/voluptuous/pull/517): Remove `Maybe` validator typing +* [518](https://github.com/alecthomas/voluptuous/pull/518): Use typing.Container for `In` validator +* [519](https://github.com/alecthomas/voluptuous/pull/519): Don't enforce type for unused description attribute +* [521](https://github.com/alecthomas/voluptuous/pull/521): Type schema attribute as `Any` ## [0.15.0] @@ -58,7 +73,6 @@ * [#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')` diff --git a/voluptuous/__init__.py b/voluptuous/__init__.py index d030b351..e1980474 100644 --- a/voluptuous/__init__.py +++ b/voluptuous/__init__.py @@ -84,5 +84,5 @@ # fmt: on -__version__ = '0.15.2' +__version__ = '0.16.0' __author__ = 'alecthomas'