diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 515ee98..c611132 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 4c2fefc..cfd6717 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 0000000..af6fa6b --- /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 0048dd0..0000000 --- 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 cbd36ea..efaa50c 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 cf8ecf6..1296aab 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 e822264..ad6669b 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 992ba0d..9dab943 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 923f3d1..eabfd02 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 0ab7831..6ad8758 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 6505de2..05b7e8e 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 67021ef..0bf9302 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 229bab7..a372afe 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, + )