From 59dd901bd773ff77076a013493348ac8b4209133 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 10 Oct 2018 17:07:00 -0400 Subject: [PATCH 1/4] Fix package name canonicalization bug - Fixes #2951 - Fixes #2956 - Fixes #2963 Signed-off-by: Dan Ryan --- news/2989.bugfix | 1 + news/2989.vendor | 1 + pipenv/vendor/cerberus/__init__.py | 29 + pipenv/vendor/cerberus/errors.py | 626 +++++++ pipenv/vendor/cerberus/platform.py | 14 + pipenv/vendor/cerberus/schema.py | 482 +++++ pipenv/vendor/cerberus/tests/__init__.py | 144 ++ pipenv/vendor/cerberus/tests/conftest.py | 134 ++ pipenv/vendor/cerberus/tests/test_assorted.py | 76 + .../cerberus/tests/test_customization.py | 77 + pipenv/vendor/cerberus/tests/test_errors.py | 260 +++ pipenv/vendor/cerberus/tests/test_legacy.py | 3 + .../cerberus/tests/test_normalization.py | 485 +++++ .../vendor/cerberus/tests/test_registries.py | 82 + pipenv/vendor/cerberus/tests/test_schema.py | 111 ++ .../vendor/cerberus/tests/test_validation.py | 1579 +++++++++++++++++ pipenv/vendor/cerberus/utils.py | 119 ++ pipenv/vendor/cerberus/validator.py | 1407 +++++++++++++++ pipenv/vendor/requirementslib/__init__.py | 2 +- .../requirementslib/models/requirements.py | 5 +- pipenv/vendor/vendor.txt | 3 +- 21 files changed, 5637 insertions(+), 3 deletions(-) create mode 100644 news/2989.bugfix create mode 100644 news/2989.vendor create mode 100644 pipenv/vendor/cerberus/__init__.py create mode 100644 pipenv/vendor/cerberus/errors.py create mode 100644 pipenv/vendor/cerberus/platform.py create mode 100644 pipenv/vendor/cerberus/schema.py create mode 100644 pipenv/vendor/cerberus/tests/__init__.py create mode 100644 pipenv/vendor/cerberus/tests/conftest.py create mode 100644 pipenv/vendor/cerberus/tests/test_assorted.py create mode 100644 pipenv/vendor/cerberus/tests/test_customization.py create mode 100644 pipenv/vendor/cerberus/tests/test_errors.py create mode 100644 pipenv/vendor/cerberus/tests/test_legacy.py create mode 100644 pipenv/vendor/cerberus/tests/test_normalization.py create mode 100644 pipenv/vendor/cerberus/tests/test_registries.py create mode 100644 pipenv/vendor/cerberus/tests/test_schema.py create mode 100644 pipenv/vendor/cerberus/tests/test_validation.py create mode 100644 pipenv/vendor/cerberus/utils.py create mode 100644 pipenv/vendor/cerberus/validator.py diff --git a/news/2989.bugfix b/news/2989.bugfix new file mode 100644 index 0000000000..4e5b67d4c9 --- /dev/null +++ b/news/2989.bugfix @@ -0,0 +1 @@ +Fixed a bug which caused canonicalized package names to fail to resolve against PyPI. diff --git a/news/2989.vendor b/news/2989.vendor new file mode 100644 index 0000000000..050e57ab8f --- /dev/null +++ b/news/2989.vendor @@ -0,0 +1 @@ +Updated ``requirementslib`` to version ``1.1.9``. diff --git a/pipenv/vendor/cerberus/__init__.py b/pipenv/vendor/cerberus/__init__.py new file mode 100644 index 0000000000..4b528cffd6 --- /dev/null +++ b/pipenv/vendor/cerberus/__init__.py @@ -0,0 +1,29 @@ +""" + Extensible validation for Python dictionaries. + + :copyright: 2012-2016 by Nicola Iarocci. + :license: ISC, see LICENSE for more details. + + Full documentation is available at http://python-cerberus.org/ + +""" + +from __future__ import absolute_import + +from cerberus.validator import DocumentError, Validator +from cerberus.schema import (rules_set_registry, schema_registry, Registry, + SchemaError) +from cerberus.utils import TypeDefinition + + +__version__ = "1.2" + +__all__ = [ + DocumentError.__name__, + Registry.__name__, + SchemaError.__name__, + TypeDefinition.__name__, + Validator.__name__, + 'schema_registry', + 'rules_set_registry' +] diff --git a/pipenv/vendor/cerberus/errors.py b/pipenv/vendor/cerberus/errors.py new file mode 100644 index 0000000000..4c497eebc4 --- /dev/null +++ b/pipenv/vendor/cerberus/errors.py @@ -0,0 +1,626 @@ +# -*-: coding utf-8 -*- +""" This module contains the error-related constants and classes. """ + +from __future__ import absolute_import + +from collections import defaultdict, namedtuple, MutableMapping +from copy import copy, deepcopy +from functools import wraps +from pprint import pformat + +from cerberus.platform import PYTHON_VERSION +from cerberus.utils import compare_paths_lt, quote_string + + +ErrorDefinition = namedtuple('ErrorDefinition', 'code, rule') +""" +This class is used to define possible errors. Each distinguishable error is +defined by a *unique* error ``code`` as integer and the ``rule`` that can +cause it as string. +The instances' names do not contain a common prefix as they are supposed to be +referenced within the module namespace, e.g. ``errors.CUSTOM``. +""" + + +# custom +CUSTOM = ErrorDefinition(0x00, None) + +# existence +DOCUMENT_MISSING = ErrorDefinition(0x01, None) # issues/141 +DOCUMENT_MISSING = "document is missing" +REQUIRED_FIELD = ErrorDefinition(0x02, 'required') +UNKNOWN_FIELD = ErrorDefinition(0x03, None) +DEPENDENCIES_FIELD = ErrorDefinition(0x04, 'dependencies') +DEPENDENCIES_FIELD_VALUE = ErrorDefinition(0x05, 'dependencies') +EXCLUDES_FIELD = ErrorDefinition(0x06, 'excludes') + +# shape +DOCUMENT_FORMAT = ErrorDefinition(0x21, None) # issues/141 +DOCUMENT_FORMAT = "'{0}' is not a document, must be a dict" +EMPTY_NOT_ALLOWED = ErrorDefinition(0x22, 'empty') +NOT_NULLABLE = ErrorDefinition(0x23, 'nullable') +BAD_TYPE = ErrorDefinition(0x24, 'type') +BAD_TYPE_FOR_SCHEMA = ErrorDefinition(0x25, 'schema') +ITEMS_LENGTH = ErrorDefinition(0x26, 'items') +MIN_LENGTH = ErrorDefinition(0x27, 'minlength') +MAX_LENGTH = ErrorDefinition(0x28, 'maxlength') + + +# color +REGEX_MISMATCH = ErrorDefinition(0x41, 'regex') +MIN_VALUE = ErrorDefinition(0x42, 'min') +MAX_VALUE = ErrorDefinition(0x43, 'max') +UNALLOWED_VALUE = ErrorDefinition(0x44, 'allowed') +UNALLOWED_VALUES = ErrorDefinition(0x45, 'allowed') +FORBIDDEN_VALUE = ErrorDefinition(0x46, 'forbidden') +FORBIDDEN_VALUES = ErrorDefinition(0x47, 'forbidden') + +# other +NORMALIZATION = ErrorDefinition(0x60, None) +COERCION_FAILED = ErrorDefinition(0x61, 'coerce') +RENAMING_FAILED = ErrorDefinition(0x62, 'rename_handler') +READONLY_FIELD = ErrorDefinition(0x63, 'readonly') +SETTING_DEFAULT_FAILED = ErrorDefinition(0x64, 'default_setter') + +# groups +ERROR_GROUP = ErrorDefinition(0x80, None) +MAPPING_SCHEMA = ErrorDefinition(0x81, 'schema') +SEQUENCE_SCHEMA = ErrorDefinition(0x82, 'schema') +KEYSCHEMA = ErrorDefinition(0x83, 'keyschema') +VALUESCHEMA = ErrorDefinition(0x84, 'valueschema') +BAD_ITEMS = ErrorDefinition(0x8f, 'items') + +LOGICAL = ErrorDefinition(0x90, None) +NONEOF = ErrorDefinition(0x91, 'noneof') +ONEOF = ErrorDefinition(0x92, 'oneof') +ANYOF = ErrorDefinition(0x93, 'anyof') +ALLOF = ErrorDefinition(0x94, 'allof') + + +""" SchemaError messages """ + +SCHEMA_ERROR_DEFINITION_TYPE = \ + "schema definition for field '{0}' must be a dict" +SCHEMA_ERROR_MISSING = "validation schema missing" + + +""" Error representations """ + + +class ValidationError(object): + """ A simple class to store and query basic error information. """ + def __init__(self, document_path, schema_path, code, rule, constraint, + value, info): + self.document_path = document_path + """ The path to the field within the document that caused the error. + Type: :class:`tuple` """ + self.schema_path = schema_path + """ The path to the rule within the schema that caused the error. + Type: :class:`tuple` """ + self.code = code + """ The error's identifier code. Type: :class:`int` """ + self.rule = rule + """ The rule that failed. Type: `string` """ + self.constraint = constraint + """ The constraint that failed. """ + self.value = value + """ The value that failed. """ + self.info = info + """ May hold additional information about the error. + Type: :class:`tuple` """ + + def __eq__(self, other): + """ Assumes the errors relate to the same document and schema. """ + return hash(self) == hash(other) + + def __hash__(self): + """ Expects that all other properties are transitively determined. """ + return hash(self.document_path) ^ hash(self.schema_path) \ + ^ hash(self.code) + + def __lt__(self, other): + if self.document_path != other.document_path: + return compare_paths_lt(self.document_path, other.document_path) + else: + return compare_paths_lt(self.schema_path, other.schema_path) + + def __repr__(self): + return "{class_name} @ {memptr} ( " \ + "document_path={document_path}," \ + "schema_path={schema_path}," \ + "code={code}," \ + "constraint={constraint}," \ + "value={value}," \ + "info={info} )"\ + .format(class_name=self.__class__.__name__, memptr=hex(id(self)), # noqa: E501 + document_path=self.document_path, + schema_path=self.schema_path, + code=hex(self.code), + constraint=quote_string(self.constraint), + value=quote_string(self.value), + info=self.info) + + @property + def child_errors(self): + """ + A list that contains the individual errors of a bulk validation error. + """ + return self.info[0] if self.is_group_error else None + + @property + def definitions_errors(self): + """ Dictionary with errors of an *of-rule mapped to the index of the + definition it occurred in. Returns :obj:`None` if not applicable. + """ + if not self.is_logic_error: + return None + + result = defaultdict(list) + for error in self.child_errors: + i = error.schema_path[len(self.schema_path)] + result[i].append(error) + return result + + @property + def field(self): + """ Field of the contextual mapping, possibly :obj:`None`. """ + if self.document_path: + return self.document_path[-1] + else: + return None + + @property + def is_group_error(self): + """ ``True`` for errors of bulk validations. """ + return bool(self.code & ERROR_GROUP.code) + + @property + def is_logic_error(self): + """ ``True`` for validation errors against different schemas with + *of-rules. """ + return bool(self.code & LOGICAL.code - ERROR_GROUP.code) + + @property + def is_normalization_error(self): + """ ``True`` for normalization errors. """ + return bool(self.code & NORMALIZATION.code) + + +class ErrorList(list): + """ A list for :class:`~cerberus.errors.ValidationError` instances that + can be queried with the ``in`` keyword for a particular + :class:`~cerberus.errors.ErrorDefinition`. """ + def __contains__(self, error_definition): + for code in (x.code for x in self): + if code == error_definition.code: + return True + return False + + +class ErrorTreeNode(MutableMapping): + __slots__ = ('descendants', 'errors', 'parent_node', 'path', 'tree_root') + + def __init__(self, path, parent_node): + self.parent_node = parent_node + self.tree_root = self.parent_node.tree_root + self.path = path[:self.parent_node.depth + 1] + self.errors = ErrorList() + self.descendants = {} + + def __add__(self, error): + self.add(error) + return self + + def __contains__(self, item): + if isinstance(item, ErrorDefinition): + return item in self.errors + else: + return item in self.descendants + + def __delitem__(self, key): + del self.descendants[key] + + def __iter__(self): + return iter(self.errors) + + def __getitem__(self, item): + if isinstance(item, ErrorDefinition): + for error in self.errors: + if item.code == error.code: + return error + else: + return self.descendants.get(item) + + def __len__(self): + return len(self.errors) + + def __repr__(self): + return self.__str__() + + def __setitem__(self, key, value): + self.descendants[key] = value + + def __str__(self): + return str(self.errors) + ',' + str(self.descendants) + + @property + def depth(self): + return len(self.path) + + @property + def tree_type(self): + return self.tree_root.tree_type + + def add(self, error): + error_path = self._path_of_(error) + + key = error_path[self.depth] + if key not in self.descendants: + self[key] = ErrorTreeNode(error_path, self) + + if len(error_path) == self.depth + 1: + self[key].errors.append(error) + self[key].errors.sort() + if error.is_group_error: + for child_error in error.child_errors: + self.tree_root += child_error + else: + self[key] += error + + def _path_of_(self, error): + return getattr(error, self.tree_type + '_path') + + +class ErrorTree(ErrorTreeNode): + """ Base class for :class:`~cerberus.errors.DocumentErrorTree` and + :class:`~cerberus.errors.SchemaErrorTree`. """ + def __init__(self, errors=[]): + self.parent_node = None + self.tree_root = self + self.path = () + self.errors = ErrorList() + self.descendants = {} + for error in errors: + self += error + + def add(self, error): + """ Add an error to the tree. + + :param error: :class:`~cerberus.errors.ValidationError` + """ + if not self._path_of_(error): + self.errors.append(error) + self.errors.sort() + else: + super(ErrorTree, self).add(error) + + def fetch_errors_from(self, path): + """ Returns all errors for a particular path. + + :param path: :class:`tuple` of :term:`hashable` s. + :rtype: :class:`~cerberus.errors.ErrorList` + """ + node = self.fetch_node_from(path) + if node is not None: + return node.errors + else: + return ErrorList() + + def fetch_node_from(self, path): + """ Returns a node for a path. + + :param path: Tuple of :term:`hashable` s. + :rtype: :class:`~cerberus.errors.ErrorTreeNode` or :obj:`None` + """ + context = self + for key in path: + context = context[key] + if context is None: + break + return context + + +class DocumentErrorTree(ErrorTree): + """ Implements a dict-like class to query errors by indexes following the + structure of a validated document. """ + tree_type = 'document' + + +class SchemaErrorTree(ErrorTree): + """ Implements a dict-like class to query errors by indexes following the + structure of the used schema. """ + tree_type = 'schema' + + +class BaseErrorHandler(object): + """ Base class for all error handlers. + Subclasses are identified as error-handlers with an instance-test. """ + def __init__(self, *args, **kwargs): + """ Optionally initialize a new instance. """ + pass + + def __call__(self, errors): + """ Returns errors in a handler-specific format. + + :param errors: An object containing the errors. + :type errors: :term:`iterable` of + :class:`~cerberus.errors.ValidationError` instances or a + :class:`~cerberus.Validator` instance + """ + raise NotImplementedError + + def __iter__(self): + """ Be a superhero and implement an iterator over errors. """ + raise NotImplementedError + + def add(self, error): + """ Add an error to the errors' container object of a handler. + + :param error: The error to add. + :type error: :class:`~cerberus.errors.ValidationError` + """ + raise NotImplementedError + + def emit(self, error): + """ Optionally emits an error in the handler's format to a stream. + Or light a LED, or even shut down a power plant. + + :param error: The error to emit. + :type error: :class:`~cerberus.errors.ValidationError` + """ + pass + + def end(self, validator): + """ Gets called when a validation ends. + + :param validator: The calling validator. + :type validator: :class:`~cerberus.Validator` """ + pass + + def extend(self, errors): + """ Adds all errors to the handler's container object. + + :param errors: The errors to add. + :type errors: :term:`iterable` of + :class:`~cerberus.errors.ValidationError` instances + """ + for error in errors: + self.add(error) + + def start(self, validator): + """ Gets called when a validation starts. + + :param validator: The calling validator. + :type validator: :class:`~cerberus.Validator` + """ + pass + + +class ToyErrorHandler(BaseErrorHandler): + def __call__(self, *args, **kwargs): + raise RuntimeError('This is not supposed to happen.') + + def clear(self): + pass + + +def encode_unicode(f): + """Cerberus error messages expect regular binary strings. + If unicode is used in a ValidationError message can't be printed. + + This decorator ensures that if legacy Python is used unicode + strings are encoded before passing to a function. + """ + @wraps(f) + def wrapped(obj, error): + + def _encode(value): + """Helper encoding unicode strings into binary utf-8""" + if isinstance(value, unicode): # noqa: F821 + return value.encode('utf-8') + return value + + error = copy(error) + error.document_path = _encode(error.document_path) + error.schema_path = _encode(error.schema_path) + error.constraint = _encode(error.constraint) + error.value = _encode(error.value) + error.info = _encode(error.info) + return f(obj, error) + + return wrapped if PYTHON_VERSION < 3 else f + + +class BasicErrorHandler(BaseErrorHandler): + """ Models cerberus' legacy. Returns a :class:`dict`. When mangled + through :class:`str` a pretty-formatted representation of that + tree is returned. + """ + messages = {0x00: "{0}", + + 0x01: "document is missing", + 0x02: "required field", + 0x03: "unknown field", + 0x04: "field '{0}' is required", + 0x05: "depends on these values: {constraint}", + 0x06: "{0} must not be present with '{field}'", + + 0x21: "'{0}' is not a document, must be a dict", + 0x22: "empty values not allowed", + 0x23: "null value not allowed", + 0x24: "must be of {constraint} type", + 0x25: "must be of dict type", + 0x26: "length of list should be {constraint}, it is {0}", + 0x27: "min length is {constraint}", + 0x28: "max length is {constraint}", + + 0x41: "value does not match regex '{constraint}'", + 0x42: "min value is {constraint}", + 0x43: "max value is {constraint}", + 0x44: "unallowed value {value}", + 0x45: "unallowed values {0}", + 0x46: "unallowed value {value}", + 0x47: "unallowed values {0}", + + 0x61: "field '{field}' cannot be coerced: {0}", + 0x62: "field '{field}' cannot be renamed: {0}", + 0x63: "field is read-only", + 0x64: "default value for '{field}' cannot be set: {0}", + + 0x81: "mapping doesn't validate subschema: {0}", + 0x82: "one or more sequence-items don't validate: {0}", + 0x83: "one or more keys of a mapping don't validate: {0}", + 0x84: "one or more values in a mapping don't validate: {0}", + 0x85: "one or more sequence-items don't validate: {0}", + + 0x91: "one or more definitions validate", + 0x92: "none or more than one rule validate", + 0x93: "no definitions validate", + 0x94: "one or more definitions don't validate" + } + + def __init__(self, tree=None): + self.tree = {} if tree is None else tree + + def __call__(self, errors=None): + if errors is not None: + self.clear() + self.extend(errors) + return self.pretty_tree + + def __str__(self): + return pformat(self.pretty_tree) + + @property + def pretty_tree(self): + pretty = deepcopy(self.tree) + for field in pretty: + self._purge_empty_dicts(pretty[field]) + return pretty + + @encode_unicode + def add(self, error): + # Make sure the original error is not altered with + # error paths specific to the handler. + error = deepcopy(error) + + self._rewrite_error_path(error) + + if error.is_logic_error: + self._insert_logic_error(error) + elif error.is_group_error: + self._insert_group_error(error) + elif error.code in self.messages: + self._insert_error(error.document_path, + self._format_message(error.field, error)) + + def clear(self): + self.tree = {} + + def start(self, validator): + self.clear() + + def _format_message(self, field, error): + return self.messages[error.code].format( + *error.info, constraint=error.constraint, + field=field, value=error.value) + + def _insert_error(self, path, node): + """ Adds an error or sub-tree to :attr:tree. + + :param path: Path to the error. + :type path: Tuple of strings and integers. + :param node: An error message or a sub-tree. + :type node: String or dictionary. + """ + field = path[0] + if len(path) == 1: + if field in self.tree: + subtree = self.tree[field].pop() + self.tree[field] += [node, subtree] + else: + self.tree[field] = [node, {}] + elif len(path) >= 1: + if field not in self.tree: + self.tree[field] = [{}] + subtree = self.tree[field][-1] + + if subtree: + new = self.__class__(tree=copy(subtree)) + else: + new = self.__class__() + new._insert_error(path[1:], node) + subtree.update(new.tree) + + def _insert_group_error(self, error): + for child_error in error.child_errors: + if child_error.is_logic_error: + self._insert_logic_error(child_error) + elif child_error.is_group_error: + self._insert_group_error(child_error) + else: + self._insert_error(child_error.document_path, + self._format_message(child_error.field, + child_error)) + + def _insert_logic_error(self, error): + field = error.field + self._insert_error(error.document_path, + self._format_message(field, error)) + + for definition_errors in error.definitions_errors.values(): + for child_error in definition_errors: + if child_error.is_logic_error: + self._insert_logic_error(child_error) + elif child_error.is_group_error: + self._insert_group_error(child_error) + else: + self._insert_error(child_error.document_path, + self._format_message(field, child_error)) + + def _purge_empty_dicts(self, error_list): + subtree = error_list[-1] + if not error_list[-1]: + error_list.pop() + else: + for key in subtree: + self._purge_empty_dicts(subtree[key]) + + def _rewrite_error_path(self, error, offset=0): + """ + Recursively rewrites the error path to correctly represent logic errors + """ + if error.is_logic_error: + self._rewrite_logic_error_path(error, offset) + elif error.is_group_error: + self._rewrite_group_error_path(error, offset) + + def _rewrite_group_error_path(self, error, offset=0): + child_start = len(error.document_path) - offset + + for child_error in error.child_errors: + relative_path = child_error.document_path[child_start:] + child_error.document_path = error.document_path + relative_path + + self._rewrite_error_path(child_error, offset) + + def _rewrite_logic_error_path(self, error, offset=0): + child_start = len(error.document_path) - offset + + for i, definition_errors in error.definitions_errors.items(): + if not definition_errors: + continue + + nodename = '%s definition %s' % (error.rule, i) + path = error.document_path + (nodename,) + + for child_error in definition_errors: + rel_path = child_error.document_path[child_start:] + child_error.document_path = path + rel_path + + self._rewrite_error_path(child_error, offset + 1) + + +class SchemaErrorHandler(BasicErrorHandler): + messages = BasicErrorHandler.messages.copy() + messages[0x03] = "unknown rule" diff --git a/pipenv/vendor/cerberus/platform.py b/pipenv/vendor/cerberus/platform.py new file mode 100644 index 0000000000..eca9858d5e --- /dev/null +++ b/pipenv/vendor/cerberus/platform.py @@ -0,0 +1,14 @@ +""" Platform-dependent objects """ + +import sys + + +PYTHON_VERSION = float(sys.version_info[0]) + float(sys.version_info[1]) / 10 + + +if PYTHON_VERSION < 3: + _str_type = basestring # noqa: F821 + _int_types = (int, long) # noqa: F821 +else: + _str_type = str + _int_types = (int,) diff --git a/pipenv/vendor/cerberus/schema.py b/pipenv/vendor/cerberus/schema.py new file mode 100644 index 0000000000..3ddce17241 --- /dev/null +++ b/pipenv/vendor/cerberus/schema.py @@ -0,0 +1,482 @@ +from __future__ import absolute_import + +from collections import (Callable, Hashable, Iterable, Mapping, + MutableMapping, Sequence) +from copy import copy + +from cerberus import errors +from cerberus.platform import _str_type +from cerberus.utils import (get_Validator_class, validator_factory, + mapping_hash, TypeDefinition) + + +class _Abort(Exception): + pass + + +class SchemaError(Exception): + """ Raised when the validation schema is missing, has the wrong format or + contains errors. """ + pass + + +class DefinitionSchema(MutableMapping): + """ A dict-subclass for caching of validated schemas. """ + + def __new__(cls, *args, **kwargs): + if 'SchemaValidator' not in globals(): + global SchemaValidator + SchemaValidator = validator_factory('SchemaValidator', + SchemaValidatorMixin) + types_mapping = SchemaValidator.types_mapping.copy() + types_mapping.update({ + 'callable': TypeDefinition('callable', (Callable,), ()), + 'hashable': TypeDefinition('hashable', (Hashable,), ()) + }) + SchemaValidator.types_mapping = types_mapping + + return super(DefinitionSchema, cls).__new__(cls) + + def __init__(self, validator, schema={}): + """ + :param validator: An instance of Validator-(sub-)class that uses this + schema. + :param schema: A definition-schema as ``dict``. Defaults to an empty + one. + """ + if not isinstance(validator, get_Validator_class()): + raise RuntimeError('validator argument must be a Validator-' + 'instance.') + self.validator = validator + + if isinstance(schema, _str_type): + schema = validator.schema_registry.get(schema, schema) + + if not isinstance(schema, Mapping): + try: + schema = dict(schema) + except Exception: + raise SchemaError( + errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema)) + + self.validation_schema = SchemaValidationSchema(validator) + self.schema_validator = SchemaValidator( + None, allow_unknown=self.validation_schema, + error_handler=errors.SchemaErrorHandler, + target_schema=schema, target_validator=validator) + + schema = self.expand(schema) + self.validate(schema) + self.schema = schema + + def __delitem__(self, key): + _new_schema = self.schema.copy() + try: + del _new_schema[key] + except ValueError: + raise SchemaError("Schema has no field '%s' defined" % key) + except Exception as e: + raise e + else: + del self.schema[key] + + def __getitem__(self, item): + return self.schema[item] + + def __iter__(self): + return iter(self.schema) + + def __len__(self): + return len(self.schema) + + def __repr__(self): + return str(self) + + def __setitem__(self, key, value): + value = self.expand({0: value})[0] + self.validate({key: value}) + self.schema[key] = value + + def __str__(self): + return str(self.schema) + + def copy(self): + return self.__class__(self.validator, self.schema.copy()) + + @classmethod + def expand(cls, schema): + try: + schema = cls._expand_logical_shortcuts(schema) + schema = cls._expand_subschemas(schema) + except Exception: + pass + return schema + + @classmethod + def _expand_logical_shortcuts(cls, schema): + """ Expand agglutinated rules in a definition-schema. + + :param schema: The schema-definition to expand. + :return: The expanded schema-definition. + """ + def is_of_rule(x): + return isinstance(x, _str_type) and \ + x.startswith(('allof_', 'anyof_', 'noneof_', 'oneof_')) + + for field in schema: + for of_rule in (x for x in schema[field] if is_of_rule(x)): + operator, rule = of_rule.split('_') + schema[field].update({operator: []}) + for value in schema[field][of_rule]: + schema[field][operator].append({rule: value}) + del schema[field][of_rule] + return schema + + @classmethod + def _expand_subschemas(cls, schema): + def has_schema_rule(): + return isinstance(schema[field], Mapping) and \ + 'schema' in schema[field] + + def has_mapping_schema(): + """ Tries to determine heuristically if the schema-constraints are + aimed to mappings. """ + try: + return all(isinstance(x, Mapping) for x + in schema[field]['schema'].values()) + except TypeError: + return False + + for field in schema: + if not has_schema_rule(): + pass + elif has_mapping_schema(): + schema[field]['schema'] = cls.expand(schema[field]['schema']) + else: # assumes schema-constraints for a sequence + schema[field]['schema'] = \ + cls.expand({0: schema[field]['schema']})[0] + + for rule in ('keyschema', 'valueschema'): + if rule in schema[field]: + schema[field][rule] = \ + cls.expand({0: schema[field][rule]})[0] + + for rule in ('allof', 'anyof', 'items', 'noneof', 'oneof'): + if rule in schema[field]: + if not isinstance(schema[field][rule], Sequence): + continue + new_rules_definition = [] + for item in schema[field][rule]: + new_rules_definition.append(cls.expand({0: item})[0]) + schema[field][rule] = new_rules_definition + return schema + + def update(self, schema): + try: + schema = self.expand(schema) + _new_schema = self.schema.copy() + _new_schema.update(schema) + self.validate(_new_schema) + except ValueError: + raise SchemaError(errors.SCHEMA_ERROR_DEFINITION_TYPE + .format(schema)) + except Exception as e: + raise e + else: + self.schema = _new_schema + + def regenerate_validation_schema(self): + self.validation_schema = SchemaValidationSchema(self.validator) + + def validate(self, schema=None): + if schema is None: + schema = self.schema + _hash = (mapping_hash(schema), + mapping_hash(self.validator.types_mapping)) + if _hash not in self.validator._valid_schemas: + self._validate(schema) + self.validator._valid_schemas.add(_hash) + + def _validate(self, schema): + """ Validates a schema that defines rules against supported rules. + + :param schema: The schema to be validated as a legal cerberus schema + according to the rules of this Validator object. + """ + if isinstance(schema, _str_type): + schema = self.validator.schema_registry.get(schema, schema) + + if schema is None: + raise SchemaError(errors.SCHEMA_ERROR_MISSING) + + schema = copy(schema) + for field in schema: + if isinstance(schema[field], _str_type): + schema[field] = rules_set_registry.get(schema[field], + schema[field]) + + if not self.schema_validator(schema, normalize=False): + raise SchemaError(self.schema_validator.errors) + + +class UnvalidatedSchema(DefinitionSchema): + def __init__(self, schema={}): + if not isinstance(schema, Mapping): + schema = dict(schema) + self.schema = schema + + def validate(self, schema): + pass + + def copy(self): + # Override ancestor's copy, because + # UnvalidatedSchema does not have .validator: + return self.__class__(self.schema.copy()) + + +class SchemaValidationSchema(UnvalidatedSchema): + def __init__(self, validator): + self.schema = {'allow_unknown': False, + 'schema': validator.rules, + 'type': 'dict'} + + +class SchemaValidatorMixin(object): + """ This validator is extended to validate schemas passed to a Cerberus + validator. """ + @property + def known_rules_set_refs(self): + """ The encountered references to rules set registry items. """ + return self._config.get('known_rules_set_refs', ()) + + @known_rules_set_refs.setter + def known_rules_set_refs(self, value): + self._config['known_rules_set_refs'] = value + + @property + def known_schema_refs(self): + """ The encountered references to schema registry items. """ + return self._config.get('known_schema_refs', ()) + + @known_schema_refs.setter + def known_schema_refs(self, value): + self._config['known_schema_refs'] = value + + @property + def target_schema(self): + """ The schema that is being validated. """ + return self._config['target_schema'] + + @property + def target_validator(self): + """ The validator whose schema is being validated. """ + return self._config['target_validator'] + + def _validate_logical(self, rule, field, value): + """ {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """ + if not isinstance(value, Sequence): + self._error(field, errors.BAD_TYPE) + return + + validator = self._get_child_validator( + document_crumb=rule, allow_unknown=False, + schema=self.target_validator.validation_rules) + + for constraints in value: + _hash = (mapping_hash({'turing': constraints}), + mapping_hash(self.target_validator.types_mapping)) + if _hash in self.target_validator._valid_schemas: + continue + + validator(constraints, normalize=False) + if validator._errors: + self._error(validator._errors) + else: + self.target_validator._valid_schemas.add(_hash) + + def _validator_bulk_schema(self, field, value): + # resolve schema registry reference + if isinstance(value, _str_type): + if value in self.known_rules_set_refs: + return + else: + self.known_rules_set_refs += (value,) + definition = self.target_validator.rules_set_registry.get(value) + if definition is None: + self._error(field, 'Rules set definition %s not found.' % value) + return + else: + value = definition + + _hash = (mapping_hash({'turing': value}), + mapping_hash(self.target_validator.types_mapping)) + if _hash in self.target_validator._valid_schemas: + return + + validator = self._get_child_validator( + document_crumb=field, allow_unknown=False, + schema=self.target_validator.rules) + validator(value, normalize=False) + if validator._errors: + self._error(validator._errors) + else: + self.target_validator._valid_schemas.add(_hash) + + def _validator_dependencies(self, field, value): + if isinstance(value, _str_type): + pass + elif isinstance(value, Mapping): + validator = self._get_child_validator( + document_crumb=field, + schema={'valueschema': {'type': 'list'}}, + allow_unknown=True + ) + if not validator(value, normalize=False): + self._error(validator._errors) + elif isinstance(value, Sequence): + if not all(isinstance(x, Hashable) for x in value): + path = self.document_path + (field,) + self._error(path, 'All dependencies must be a hashable type.') + + def _validator_handler(self, field, value): + if isinstance(value, Callable): + return + if isinstance(value, _str_type): + if value not in self.target_validator.validators + \ + self.target_validator.coercers: + self._error(field, '%s is no valid coercer' % value) + elif isinstance(value, Iterable): + for handler in value: + self._validator_handler(field, handler) + + def _validator_items(self, field, value): + for i, schema in enumerate(value): + self._validator_bulk_schema((field, i), schema) + + def _validator_schema(self, field, value): + try: + value = self._handle_schema_reference_for_validator(field, value) + except _Abort: + return + + _hash = (mapping_hash(value), + mapping_hash(self.target_validator.types_mapping)) + if _hash in self.target_validator._valid_schemas: + return + + validator = self._get_child_validator( + document_crumb=field, + schema=None, allow_unknown=self.root_allow_unknown) + validator(self._expand_rules_set_refs(value), normalize=False) + if validator._errors: + self._error(validator._errors) + else: + self.target_validator._valid_schemas.add(_hash) + + def _handle_schema_reference_for_validator(self, field, value): + if not isinstance(value, _str_type): + return value + if value in self.known_schema_refs: + raise _Abort + + self.known_schema_refs += (value,) + definition = self.target_validator.schema_registry.get(value) + if definition is None: + path = self.document_path + (field,) + self._error(path, 'Schema definition {} not found.'.format(value)) + raise _Abort + return definition + + def _expand_rules_set_refs(self, schema): + result = {} + for k, v in schema.items(): + if isinstance(v, _str_type): + result[k] = self.target_validator.rules_set_registry.get(v) + else: + result[k] = v + return result + + def _validator_type(self, field, value): + value = (value,) if isinstance(value, _str_type) else value + invalid_constraints = () + for constraint in value: + if constraint not in self.target_validator.types: + invalid_constraints += (constraint,) + if invalid_constraints: + path = self.document_path + (field,) + self._error(path, 'Unsupported types: %s' % invalid_constraints) + +#### + + +class Registry(object): + """ A registry to store and retrieve schemas and parts of it by a name + that can be used in validation schemas. + + :param definitions: Optional, initial definitions. + :type definitions: any :term:`mapping` """ + + def __init__(self, definitions={}): + self._storage = {} + self.extend(definitions) + + def add(self, name, definition): + """ Register a definition to the registry. Existing definitions are + replaced silently. + + :param name: The name which can be used as reference in a validation + schema. + :type name: :class:`str` + :param definition: The definition. + :type definition: any :term:`mapping` """ + self._storage[name] = self._expand_definition(definition) + + def all(self): + """ Returns a :class:`dict` with all registered definitions mapped to + their name. """ + return self._storage + + def clear(self): + """ Purge all definitions in the registry. """ + self._storage.clear() + + def extend(self, definitions): + """ Add several definitions at once. Existing definitions are + replaced silently. + + :param definitions: The names and definitions. + :type definitions: a :term:`mapping` or an :term:`iterable` with + two-value :class:`tuple` s """ + for name, definition in dict(definitions).items(): + self.add(name, definition) + + def get(self, name, default=None): + """ Retrieve a definition from the registry. + + :param name: The reference that points to the definition. + :type name: :class:`str` + :param default: Return value if the reference isn't registered. """ + return self._storage.get(name, default) + + def remove(self, *names): + """ Unregister definitions from the registry. + + :param names: The names of the definitions that are to be + unregistered. """ + for name in names: + self._storage.pop(name, None) + + +class SchemaRegistry(Registry): + @classmethod + def _expand_definition(cls, definition): + return DefinitionSchema.expand(definition) + + +class RulesSetRegistry(Registry): + @classmethod + def _expand_definition(cls, definition): + return DefinitionSchema.expand({0: definition})[0] + + +schema_registry, rules_set_registry = SchemaRegistry(), RulesSetRegistry() diff --git a/pipenv/vendor/cerberus/tests/__init__.py b/pipenv/vendor/cerberus/tests/__init__.py new file mode 100644 index 0000000000..cc1c27dcc2 --- /dev/null +++ b/pipenv/vendor/cerberus/tests/__init__.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- + +import pytest + +from cerberus import errors, Validator, SchemaError, DocumentError +from cerberus.tests.conftest import sample_schema + + +def assert_exception(exception, document={}, schema=None, validator=None, + msg=None): + """ Tests whether a specific exception is raised. Optionally also tests + whether the exception message is as expected. """ + if validator is None: + validator = Validator() + if msg is None: + with pytest.raises(exception) as excinfo: + validator(document, schema) + else: + with pytest.raises(exception, message=msg) as excinfo: # noqa: F841 + validator(document, schema) + + +def assert_schema_error(*args): + """ Tests whether a validation raises an exception due to a malformed + schema. """ + assert_exception(SchemaError, *args) + + +def assert_document_error(*args): + """ Tests whether a validation raises an exception due to a malformed + document. """ + assert_exception(DocumentError, *args) + + +def assert_fail(document, schema=None, validator=None, update=False, + error=None, errors=None, child_errors=None): + """ Tests whether a validation fails. """ + if validator is None: + validator = Validator(sample_schema) + result = validator(document, schema, update) + assert isinstance(result, bool) + assert not result + + actual_errors = validator._errors + + assert not (error is not None and errors is not None) + assert not (errors is not None and child_errors is not None), ( + 'child_errors can only be tested in ' + 'conjunction with the error parameter' + ) + assert not (child_errors is not None and error is None) + if error is not None: + assert len(actual_errors) == 1 + assert_has_error(actual_errors, *error) + + if child_errors is not None: + assert len(actual_errors[0].child_errors) == len(child_errors) + assert_has_errors(actual_errors[0].child_errors, child_errors) + + elif errors is not None: + assert len(actual_errors) == len(errors) + assert_has_errors(actual_errors, errors) + + return actual_errors + + +def assert_success(document, schema=None, validator=None, update=False): + """ Tests whether a validation succeeds. """ + if validator is None: + validator = Validator(sample_schema) + result = validator(document, schema, update) + assert isinstance(result, bool) + if not result: + raise AssertionError(validator.errors) + + +def assert_has_error(_errors, d_path, s_path, error_def, constraint, info=()): + if not isinstance(d_path, tuple): + d_path = (d_path,) + if not isinstance(info, tuple): + info = (info,) + + assert isinstance(_errors, errors.ErrorList) + + for i, error in enumerate(_errors): + assert isinstance(error, errors.ValidationError) + try: + assert error.document_path == d_path + assert error.schema_path == s_path + assert error.code == error_def.code + assert error.rule == error_def.rule + assert error.constraint == constraint + if not error.is_group_error: + assert error.info == info + except AssertionError: + pass + except Exception: + raise + else: + break + else: + raise AssertionError(""" + Error with properties: + document_path={doc_path} + schema_path={schema_path} + code={code} + constraint={constraint} + info={info} + not found in errors: + {errors} + """.format(doc_path=d_path, schema_path=s_path, + code=hex(error.code), info=info, + constraint=constraint, errors=_errors)) + return i + + +def assert_has_errors(_errors, _exp_errors): + assert isinstance(_exp_errors, list) + for error in _exp_errors: + assert isinstance(error, tuple) + assert_has_error(_errors, *error) + + +def assert_not_has_error(_errors, *args, **kwargs): + try: + assert_has_error(_errors, *args, **kwargs) + except AssertionError: + pass + except Exception as e: + raise e + else: + raise AssertionError('An unexpected error occurred.') + + +def assert_bad_type(field, data_type, value): + assert_fail({field: value}, + error=(field, (field, 'type'), errors.BAD_TYPE, data_type)) + + +def assert_normalized(document, expected, schema=None, validator=None): + if validator is None: + validator = Validator(sample_schema) + assert_success(document, schema, validator) + assert validator.document == expected diff --git a/pipenv/vendor/cerberus/tests/conftest.py b/pipenv/vendor/cerberus/tests/conftest.py new file mode 100644 index 0000000000..3b4395ea75 --- /dev/null +++ b/pipenv/vendor/cerberus/tests/conftest.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +from copy import deepcopy + +import pytest + +from cerberus import Validator + + +@pytest.fixture +def document(): + return deepcopy(sample_document) + + +@pytest.fixture +def schema(): + return deepcopy(sample_schema) + + +@pytest.fixture +def validator(): + return Validator(sample_schema) + + +sample_schema = { + 'a_string': { + 'type': 'string', + 'minlength': 2, + 'maxlength': 10 + }, + 'a_binary': { + 'type': 'binary', + 'minlength': 2, + 'maxlength': 10 + }, + 'a_nullable_integer': { + 'type': 'integer', + 'nullable': True + }, + 'an_integer': { + 'type': 'integer', + 'min': 1, + 'max': 100, + }, + 'a_restricted_integer': { + 'type': 'integer', + 'allowed': [-1, 0, 1], + }, + 'a_boolean': { + 'type': 'boolean', + }, + 'a_datetime': { + 'type': 'datetime', + }, + 'a_float': { + 'type': 'float', + 'min': 1, + 'max': 100, + }, + 'a_number': { + 'type': 'number', + 'min': 1, + 'max': 100, + }, + 'a_set': { + 'type': 'set', + }, + 'one_or_more_strings': { + 'type': ['string', 'list'], + 'schema': {'type': 'string'} + }, + 'a_regex_email': { + 'type': 'string', + 'regex': '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' + }, + 'a_readonly_string': { + 'type': 'string', + 'readonly': True, + }, + 'a_restricted_string': { + 'type': 'string', + 'allowed': ["agent", "client", "vendor"], + }, + 'an_array': { + 'type': 'list', + 'allowed': ["agent", "client", "vendor"], + }, + 'a_list_of_dicts': { + 'type': 'list', + 'schema': { + 'type': 'dict', + 'schema': { + 'sku': {'type': 'string'}, + 'price': {'type': 'integer', 'required': True}, + }, + }, + }, + 'a_list_of_values': { + 'type': 'list', + 'items': [{'type': 'string'}, {'type': 'integer'}, ] + }, + 'a_list_of_integers': { + 'type': 'list', + 'schema': {'type': 'integer'}, + }, + 'a_dict': { + 'type': 'dict', + 'schema': { + 'address': {'type': 'string'}, + 'city': {'type': 'string', 'required': True} + }, + }, + 'a_dict_with_valueschema': { + 'type': 'dict', + 'valueschema': {'type': 'integer'} + }, + 'a_dict_with_keyschema': { + 'type': 'dict', + 'keyschema': {'type': 'string', 'regex': '[a-z]+'} + }, + 'a_list_length': { + 'type': 'list', + 'schema': {'type': 'integer'}, + 'minlength': 2, + 'maxlength': 5, + }, + 'a_nullable_field_without_type': { + 'nullable': True + }, + 'a_not_nullable_field_without_type': { + }, +} + +sample_document = {'name': 'john doe'} diff --git a/pipenv/vendor/cerberus/tests/test_assorted.py b/pipenv/vendor/cerberus/tests/test_assorted.py new file mode 100644 index 0000000000..641adb7e64 --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_assorted.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +from decimal import Decimal + +from pytest import mark + +from cerberus import TypeDefinition, Validator +from cerberus.tests import assert_fail, assert_success +from cerberus.utils import validator_factory +from cerberus.validator import BareValidator + + +def test_clear_cache(validator): + assert len(validator._valid_schemas) > 0 + validator.clear_caches() + assert len(validator._valid_schemas) == 0 + + +def test_docstring(validator): + assert validator.__doc__ + + +# Test that testing with the sample schema works as expected +# as there might be rules with side-effects in it + +@mark.parametrize('test,document', ((assert_fail, {'an_integer': 60}), + (assert_success, {'an_integer': 110}))) +def test_that_test_fails(test, document): + try: + test(document) + except AssertionError: + pass + else: + raise AssertionError("test didn't fail") + + +def test_dynamic_types(): + decimal_type = TypeDefinition('decimal', (Decimal,), ()) + document = {'measurement': Decimal(0)} + schema = {'measurement': {'type': 'decimal'}} + + validator = Validator() + validator.types_mapping['decimal'] = decimal_type + assert_success(document, schema, validator) + + class MyValidator(Validator): + types_mapping = Validator.types_mapping.copy() + types_mapping['decimal'] = decimal_type + validator = MyValidator() + assert_success(document, schema, validator) + + +def test_mro(): + assert Validator.__mro__ == (Validator, BareValidator, object), \ + Validator.__mro__ + + +def test_mixin_init(): + class Mixin(object): + def __init__(self, *args, **kwargs): + kwargs['test'] = True + super(Mixin, self).__init__(*args, **kwargs) + + MyValidator = validator_factory('MyValidator', Mixin) + validator = MyValidator() + assert validator._config['test'] + + +def test_sub_init(): + class MyValidator(Validator): + def __init__(self, *args, **kwargs): + kwargs['test'] = True + super(MyValidator, self).__init__(*args, **kwargs) + + validator = MyValidator() + assert validator._config['test'] diff --git a/pipenv/vendor/cerberus/tests/test_customization.py b/pipenv/vendor/cerberus/tests/test_customization.py new file mode 100644 index 0000000000..6055894d4b --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_customization.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +import cerberus +from cerberus.tests import assert_fail, assert_success +from cerberus.tests.conftest import sample_schema + + +def test_contextual_data_preservation(): + + class InheritedValidator(cerberus.Validator): + def __init__(self, *args, **kwargs): + if 'working_dir' in kwargs: + self.working_dir = kwargs['working_dir'] + super(InheritedValidator, self).__init__(*args, **kwargs) + + def _validate_type_test(self, value): + if self.working_dir: + return True + + assert 'test' in InheritedValidator.types + v = InheritedValidator({'test': {'type': 'list', + 'schema': {'type': 'test'}}}, + working_dir='/tmp') + assert_success({'test': ['foo']}, validator=v) + + +def test_docstring_parsing(): + class CustomValidator(cerberus.Validator): + def _validate_foo(self, argument, field, value): + """ {'type': 'zap'} """ + pass + + def _validate_bar(self, value): + """ Test the barreness of a value. + + The rule's arguments are validated against this schema: + {'type': 'boolean'} + """ + pass + + assert 'foo' in CustomValidator.validation_rules + assert 'bar' in CustomValidator.validation_rules + + +def test_issue_265(): + class MyValidator(cerberus.Validator): + def _validator_oddity(self, field, value): + if not value & 1: + self._error(field, "Must be an odd number") + + v = MyValidator(schema={'amount': {'validator': 'oddity'}}) + assert_success(document={'amount': 1}, validator=v) + assert_fail(document={'amount': 2}, validator=v, + error=('amount', (), cerberus.errors.CUSTOM, None, + ('Must be an odd number',))) + + +def test_schema_validation_can_be_disabled_in_schema_setter(): + + class NonvalidatingValidator(cerberus.Validator): + """ + Skips schema validation to speed up initialization + """ + @cerberus.Validator.schema.setter + def schema(self, schema): + if schema is None: + self._schema = None + elif self.is_child: + self._schema = schema + elif isinstance(schema, cerberus.schema.DefinitionSchema): + self._schema = schema + else: + self._schema = cerberus.schema.UnvalidatedSchema(schema) + + v = NonvalidatingValidator(schema=sample_schema) + assert v.validate(document={'an_integer': 1}) + assert not v.validate(document={'an_integer': 'a'}) diff --git a/pipenv/vendor/cerberus/tests/test_errors.py b/pipenv/vendor/cerberus/tests/test_errors.py new file mode 100644 index 0000000000..df33964f9b --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_errors.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- + +from cerberus import Validator, errors +from cerberus.tests import assert_fail + + +ValidationError = errors.ValidationError + + +def test__error_1(): + v = Validator(schema={'foo': {'type': 'string'}}) + v.document = {'foo': 42} + v._error('foo', errors.BAD_TYPE, 'string') + error = v._errors[0] + assert error.document_path == ('foo',) + assert error.schema_path == ('foo', 'type') + assert error.code == 0x24 + assert error.rule == 'type' + assert error.constraint == 'string' + assert error.value == 42 + assert error.info == ('string',) + assert not error.is_group_error + assert not error.is_logic_error + + +def test__error_2(): + v = Validator(schema={'foo': {'keyschema': {'type': 'integer'}}}) + v.document = {'foo': {'0': 'bar'}} + v._error('foo', errors.KEYSCHEMA, ()) + error = v._errors[0] + assert error.document_path == ('foo',) + assert error.schema_path == ('foo', 'keyschema') + assert error.code == 0x83 + assert error.rule == 'keyschema' + assert error.constraint == {'type': 'integer'} + assert error.value == {'0': 'bar'} + assert error.info == ((),) + assert error.is_group_error + assert not error.is_logic_error + + +def test__error_3(): + valids = [{'type': 'string', 'regex': '0x[0-9a-f]{2}'}, + {'type': 'integer', 'min': 0, 'max': 255}] + v = Validator(schema={'foo': {'oneof': valids}}) + v.document = {'foo': '0x100'} + v._error('foo', errors.ONEOF, (), 0, 2) + error = v._errors[0] + assert error.document_path == ('foo',) + assert error.schema_path == ('foo', 'oneof') + assert error.code == 0x92 + assert error.rule == 'oneof' + assert error.constraint == valids + assert error.value == '0x100' + assert error.info == ((), 0, 2) + assert error.is_group_error + assert error.is_logic_error + + +def test_error_tree_from_subschema(validator): + schema = {'foo': {'schema': {'bar': {'type': 'string'}}}} + document = {'foo': {'bar': 0}} + assert_fail(document, schema, validator=validator) + d_error_tree = validator.document_error_tree + s_error_tree = validator.schema_error_tree + + assert 'foo' in d_error_tree + + assert len(d_error_tree['foo'].errors) == 1, d_error_tree['foo'] + assert d_error_tree['foo'].errors[0].code == errors.MAPPING_SCHEMA.code + assert 'bar' in d_error_tree['foo'] + assert d_error_tree['foo']['bar'].errors[0].value == 0 + assert d_error_tree.fetch_errors_from(('foo', 'bar'))[0].value == 0 + + assert 'foo' in s_error_tree + assert 'schema' in s_error_tree['foo'] + assert 'bar' in s_error_tree['foo']['schema'] + assert 'type' in s_error_tree['foo']['schema']['bar'] + assert s_error_tree['foo']['schema']['bar']['type'].errors[0].value == 0 + assert s_error_tree.fetch_errors_from( + ('foo', 'schema', 'bar', 'type'))[0].value == 0 + + +def test_error_tree_from_anyof(validator): + schema = {'foo': {'anyof': [{'type': 'string'}, {'type': 'integer'}]}} + document = {'foo': []} + assert_fail(document, schema, validator=validator) + d_error_tree = validator.document_error_tree + s_error_tree = validator.schema_error_tree + assert 'foo' in d_error_tree + assert d_error_tree['foo'].errors[0].value == [] + assert 'foo' in s_error_tree + assert 'anyof' in s_error_tree['foo'] + assert 0 in s_error_tree['foo']['anyof'] + assert 1 in s_error_tree['foo']['anyof'] + assert 'type' in s_error_tree['foo']['anyof'][0] + assert s_error_tree['foo']['anyof'][0]['type'].errors[0].value == [] + + +def test_nested_error_paths(validator): + schema = {'a_dict': {'keyschema': {'type': 'integer'}, + 'valueschema': {'regex': '[a-z]*'}}, + 'a_list': {'schema': {'type': 'string', + 'oneof_regex': ['[a-z]*$', '[A-Z]*']}}} + document = {'a_dict': {0: 'abc', 'one': 'abc', 2: 'aBc', 'three': 'abC'}, + 'a_list': [0, 'abc', 'abC']} + assert_fail(document, schema, validator=validator) + + _det = validator.document_error_tree + _set = validator.schema_error_tree + + assert len(_det.errors) == 0 + assert len(_set.errors) == 0 + + assert len(_det['a_dict'].errors) == 2 + assert len(_set['a_dict'].errors) == 0 + + assert _det['a_dict'][0] is None + assert len(_det['a_dict']['one'].errors) == 1 + assert len(_det['a_dict'][2].errors) == 1 + assert len(_det['a_dict']['three'].errors) == 2 + + assert len(_set['a_dict']['keyschema'].errors) == 1 + assert len(_set['a_dict']['valueschema'].errors) == 1 + + assert len(_set['a_dict']['keyschema']['type'].errors) == 2 + assert len(_set['a_dict']['valueschema']['regex'].errors) == 2 + + _ref_err = ValidationError( + ('a_dict', 'one'), ('a_dict', 'keyschema', 'type'), + errors.BAD_TYPE.code, 'type', 'integer', 'one', ()) + assert _det['a_dict']['one'].errors[0] == _ref_err + assert _set['a_dict']['keyschema']['type'].errors[0] == _ref_err + + _ref_err = ValidationError( + ('a_dict', 2), ('a_dict', 'valueschema', 'regex'), + errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'aBc', ()) + assert _det['a_dict'][2].errors[0] == _ref_err + assert _set['a_dict']['valueschema']['regex'].errors[0] == _ref_err + + _ref_err = ValidationError( + ('a_dict', 'three'), ('a_dict', 'keyschema', 'type'), + errors.BAD_TYPE.code, 'type', 'integer', 'three', ()) + assert _det['a_dict']['three'].errors[0] == _ref_err + assert _set['a_dict']['keyschema']['type'].errors[1] == _ref_err + + _ref_err = ValidationError( + ('a_dict', 'three'), ('a_dict', 'valueschema', 'regex'), + errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ()) + assert _det['a_dict']['three'].errors[1] == _ref_err + assert _set['a_dict']['valueschema']['regex'].errors[1] == _ref_err + + assert len(_det['a_list'].errors) == 1 + assert len(_det['a_list'][0].errors) == 1 + assert _det['a_list'][1] is None + assert len(_det['a_list'][2].errors) == 3 + assert len(_set['a_list'].errors) == 0 + assert len(_set['a_list']['schema'].errors) == 1 + assert len(_set['a_list']['schema']['type'].errors) == 1 + assert len(_set['a_list']['schema']['oneof'][0]['regex'].errors) == 1 + assert len(_set['a_list']['schema']['oneof'][1]['regex'].errors) == 1 + + _ref_err = ValidationError( + ('a_list', 0), ('a_list', 'schema', 'type'), errors.BAD_TYPE.code, + 'type', 'string', 0, ()) + assert _det['a_list'][0].errors[0] == _ref_err + assert _set['a_list']['schema']['type'].errors[0] == _ref_err + + _ref_err = ValidationError( + ('a_list', 2), ('a_list', 'schema', 'oneof'), errors.ONEOF.code, + 'oneof', 'irrelevant_at_this_point', 'abC', ()) + assert _det['a_list'][2].errors[0] == _ref_err + assert _set['a_list']['schema']['oneof'].errors[0] == _ref_err + + _ref_err = ValidationError( + ('a_list', 2), ('a_list', 'schema', 'oneof', 0, 'regex'), + errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ()) + assert _det['a_list'][2].errors[1] == _ref_err + assert _set['a_list']['schema']['oneof'][0]['regex'].errors[0] == _ref_err + + _ref_err = ValidationError( + ('a_list', 2), ('a_list', 'schema', 'oneof', 1, 'regex'), + errors.REGEX_MISMATCH.code, 'regex', '[a-z]*$', 'abC', ()) + assert _det['a_list'][2].errors[2] == _ref_err + assert _set['a_list']['schema']['oneof'][1]['regex'].errors[0] == _ref_err + + +def test_queries(): + schema = {'foo': {'type': 'dict', + 'schema': + {'bar': {'type': 'number'}}}} + document = {'foo': {'bar': 'zero'}} + validator = Validator(schema) + validator(document) + + assert 'foo' in validator.document_error_tree + assert 'bar' in validator.document_error_tree['foo'] + assert 'foo' in validator.schema_error_tree + assert 'schema' in validator.schema_error_tree['foo'] + + assert errors.MAPPING_SCHEMA in validator.document_error_tree['foo'].errors + assert errors.MAPPING_SCHEMA in validator.document_error_tree['foo'] + assert errors.BAD_TYPE in validator.document_error_tree['foo']['bar'] + assert errors.MAPPING_SCHEMA in validator.schema_error_tree['foo']['schema'] + assert errors.BAD_TYPE in \ + validator.schema_error_tree['foo']['schema']['bar']['type'] + + assert (validator.document_error_tree['foo'][errors.MAPPING_SCHEMA] + .child_errors[0].code == errors.BAD_TYPE.code) + + +def test_basic_error_handler(): + handler = errors.BasicErrorHandler() + _errors, ref = [], {} + + _errors.append(ValidationError( + ['foo'], ['foo'], 0x63, 'readonly', True, None, ())) + ref.update({'foo': [handler.messages[0x63]]}) + assert handler(_errors) == ref + + _errors.append(ValidationError( + ['bar'], ['foo'], 0x42, 'min', 1, 2, ())) + ref.update({'bar': [handler.messages[0x42].format(constraint=1)]}) + assert handler(_errors) == ref + + _errors.append(ValidationError( + ['zap', 'foo'], ['zap', 'schema', 'foo'], 0x24, 'type', 'string', + True, ())) + ref.update({'zap': [{'foo': [handler.messages[0x24].format( + constraint='string')]}]}) + assert handler(_errors) == ref + + _errors.append(ValidationError( + ['zap', 'foo'], ['zap', 'schema', 'foo'], 0x41, 'regex', + '^p[äe]ng$', 'boom', ())) + ref['zap'][0]['foo'].append( + handler.messages[0x41].format(constraint='^p[äe]ng$')) + assert handler(_errors) == ref + + +def test_basic_error_of_errors(validator): + schema = {'foo': {'oneof': [ + {'type': 'integer'}, + {'type': 'string'} + ]}} + document = {'foo': 23.42} + error = ('foo', ('foo', 'oneof'), errors.ONEOF, + schema['foo']['oneof'], ()) + child_errors = [ + (error[0], error[1] + (0, 'type'), errors.BAD_TYPE, 'integer'), + (error[0], error[1] + (1, 'type'), errors.BAD_TYPE, 'string') + ] + assert_fail(document, schema, validator=validator, + error=error, child_errors=child_errors) + assert validator.errors == { + 'foo': [errors.BasicErrorHandler.messages[0x92], + {'oneof definition 0': ['must be of integer type'], + 'oneof definition 1': ['must be of string type']} + ] + } diff --git a/pipenv/vendor/cerberus/tests/test_legacy.py b/pipenv/vendor/cerberus/tests/test_legacy.py new file mode 100644 index 0000000000..59bd7b845e --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_legacy.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +pass diff --git a/pipenv/vendor/cerberus/tests/test_normalization.py b/pipenv/vendor/cerberus/tests/test_normalization.py new file mode 100644 index 0000000000..6e06f553b8 --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_normalization.py @@ -0,0 +1,485 @@ +# -*- coding: utf-8 -*- + +from tempfile import NamedTemporaryFile + +from cerberus import Validator, errors +from cerberus.tests import (assert_fail, assert_has_error, assert_normalized, + assert_success) + + +def test_coerce(): + schema = {'amount': {'coerce': int}} + document = {'amount': '1'} + expected = {'amount': 1} + assert_normalized(document, expected, schema) + + +def test_coerce_in_dictschema(): + schema = {'thing': {'type': 'dict', + 'schema': {'amount': {'coerce': int}}}} + document = {'thing': {'amount': '2'}} + expected = {'thing': {'amount': 2}} + assert_normalized(document, expected, schema) + + +def test_coerce_in_listschema(): + schema = {'things': {'type': 'list', + 'schema': {'coerce': int}}} + document = {'things': ['1', '2', '3']} + expected = {'things': [1, 2, 3]} + assert_normalized(document, expected, schema) + + +def test_coerce_in_dictschema_in_listschema(): + item_schema = {'type': 'dict', 'schema': {'amount': {'coerce': int}}} + schema = {'things': {'type': 'list', 'schema': item_schema}} + document = {'things': [{'amount': '2'}]} + expected = {'things': [{'amount': 2}]} + assert_normalized(document, expected, schema) + + +def test_coerce_not_destructive(): + schema = { + 'amount': {'coerce': int} + } + v = Validator(schema) + doc = {'amount': '1'} + v.validate(doc) + assert v.document is not doc + + +def test_coerce_catches_ValueError(): + schema = {'amount': {'coerce': int}} + _errors = assert_fail({'amount': 'not_a_number'}, schema) + _errors[0].info = () # ignore exception message here + assert_has_error(_errors, 'amount', ('amount', 'coerce'), + errors.COERCION_FAILED, int) + + +def test_coerce_catches_TypeError(): + schema = {'name': {'coerce': str.lower}} + _errors = assert_fail({'name': 1234}, schema) + _errors[0].info = () # ignore exception message here + assert_has_error(_errors, 'name', ('name', 'coerce'), + errors.COERCION_FAILED, str.lower) + + +def test_coerce_unknown(): + schema = {'foo': {'schema': {}, 'allow_unknown': {'coerce': int}}} + document = {'foo': {'bar': '0'}} + expected = {'foo': {'bar': 0}} + assert_normalized(document, expected, schema) + + +def test_custom_coerce_and_rename(): + class MyNormalizer(Validator): + def __init__(self, multiplier, *args, **kwargs): + super(MyNormalizer, self).__init__(*args, **kwargs) + self.multiplier = multiplier + + def _normalize_coerce_multiply(self, value): + return value * self.multiplier + + v = MyNormalizer(2, {'foo': {'coerce': 'multiply'}}) + assert v.normalized({'foo': 2})['foo'] == 4 + + v = MyNormalizer(3, allow_unknown={'rename_handler': 'multiply'}) + assert v.normalized({3: None}) == {9: None} + + +def test_coerce_chain(): + drop_prefix = lambda x: x[2:] + upper = lambda x: x.upper() + schema = {'foo': {'coerce': [hex, drop_prefix, upper]}} + assert_normalized({'foo': 15}, {'foo': 'F'}, schema) + + +def test_coerce_chain_aborts(validator): + def dont_do_me(value): + raise AssertionError('The coercion chain did not abort after an ' + 'error.') + schema = {'foo': {'coerce': [hex, dont_do_me]}} + validator({'foo': '0'}, schema) + assert errors.COERCION_FAILED in validator._errors + + +def test_coerce_non_digit_in_sequence(validator): + # https://github.com/pyeve/cerberus/issues/211 + schema = {'data': {'type': 'list', + 'schema': {'type': 'integer', 'coerce': int}}} + document = {'data': ['q']} + assert validator.validated(document, schema) is None + assert (validator.validated(document, schema, always_return_document=True) + == document) # noqa: W503 + + +def test_nullables_dont_fail_coerce(): + schema = {'foo': {'coerce': int, 'nullable': True, 'type': 'integer'}} + document = {'foo': None} + assert_normalized(document, document, schema) + + +def test_normalized(): + schema = {'amount': {'coerce': int}} + document = {'amount': '2'} + expected = {'amount': 2} + assert_normalized(document, expected, schema) + + +def test_rename(validator): + schema = {'foo': {'rename': 'bar'}} + document = {'foo': 0} + expected = {'bar': 0} + # We cannot use assertNormalized here since there is bug where + # Cerberus says that the renamed field is an unknown field: + # {'bar': 'unknown field'} + validator(document, schema, False) + assert validator.document == expected + + +def test_rename_handler(): + validator = Validator(allow_unknown={'rename_handler': int}) + schema = {} + document = {'0': 'foo'} + expected = {0: 'foo'} + assert_normalized(document, expected, schema, validator) + + +def test_purge_unknown(): + validator = Validator(purge_unknown=True) + schema = {'foo': {'type': 'string'}} + document = {'bar': 'foo'} + expected = {} + assert_normalized(document, expected, schema, validator) + + +def test_purge_unknown_in_subschema(): + schema = {'foo': {'type': 'dict', + 'schema': {'foo': {'type': 'string'}}, + 'purge_unknown': True}} + document = {'foo': {'bar': ''}} + expected = {'foo': {}} + assert_normalized(document, expected, schema) + + +def test_issue_147_complex(): + schema = {'revision': {'coerce': int}} + document = {'revision': '5', 'file': NamedTemporaryFile(mode='w+')} + document['file'].write(r'foobar') + document['file'].seek(0) + normalized = Validator(schema, allow_unknown=True).normalized(document) + assert normalized['revision'] == 5 + assert normalized['file'].read() == 'foobar' + document['file'].close() + normalized['file'].close() + + +def test_issue_147_nested_dict(): + schema = {'thing': {'type': 'dict', + 'schema': {'amount': {'coerce': int}}}} + ref_obj = '2' + document = {'thing': {'amount': ref_obj}} + normalized = Validator(schema).normalized(document) + assert document is not normalized + assert normalized['thing']['amount'] == 2 + assert ref_obj == '2' + assert document['thing']['amount'] is ref_obj + + +def test_coerce_in_valueschema(): + # https://github.com/pyeve/cerberus/issues/155 + schema = {'thing': {'type': 'dict', + 'valueschema': {'coerce': int, + 'type': 'integer'}}} + document = {'thing': {'amount': '2'}} + expected = {'thing': {'amount': 2}} + assert_normalized(document, expected, schema) + + +def test_coerce_in_keyschema(): + # https://github.com/pyeve/cerberus/issues/155 + schema = {'thing': {'type': 'dict', + 'keyschema': {'coerce': int, 'type': 'integer'}}} + document = {'thing': {'5': 'foo'}} + expected = {'thing': {5: 'foo'}} + assert_normalized(document, expected, schema) + + +def test_coercion_of_sequence_items(validator): + # https://github.com/pyeve/cerberus/issues/161 + schema = {'a_list': {'type': 'list', 'schema': {'type': 'float', + 'coerce': float}}} + document = {'a_list': [3, 4, 5]} + expected = {'a_list': [3.0, 4.0, 5.0]} + assert_normalized(document, expected, schema, validator) + for x in validator.document['a_list']: + assert isinstance(x, float) + + +def test_default_missing(): + _test_default_missing({'default': 'bar_value'}) + + +def test_default_setter_missing(): + _test_default_missing({'default_setter': lambda doc: 'bar_value'}) + + +def _test_default_missing(default): + bar_schema = {'type': 'string'} + bar_schema.update(default) + schema = {'foo': {'type': 'string'}, + 'bar': bar_schema} + document = {'foo': 'foo_value'} + expected = {'foo': 'foo_value', 'bar': 'bar_value'} + assert_normalized(document, expected, schema) + + +def test_default_existent(): + _test_default_existent({'default': 'bar_value'}) + + +def test_default_setter_existent(): + def raise_error(doc): + raise RuntimeError('should not be called') + _test_default_existent({'default_setter': raise_error}) + + +def _test_default_existent(default): + bar_schema = {'type': 'string'} + bar_schema.update(default) + schema = {'foo': {'type': 'string'}, + 'bar': bar_schema} + document = {'foo': 'foo_value', 'bar': 'non_default'} + assert_normalized(document, document.copy(), schema) + + +def test_default_none_nullable(): + _test_default_none_nullable({'default': 'bar_value'}) + + +def test_default_setter_none_nullable(): + def raise_error(doc): + raise RuntimeError('should not be called') + _test_default_none_nullable({'default_setter': raise_error}) + + +def _test_default_none_nullable(default): + bar_schema = {'type': 'string', + 'nullable': True} + bar_schema.update(default) + schema = {'foo': {'type': 'string'}, + 'bar': bar_schema} + document = {'foo': 'foo_value', 'bar': None} + assert_normalized(document, document.copy(), schema) + + +def test_default_none_nonnullable(): + _test_default_none_nullable({'default': 'bar_value'}) + + +def test_default_setter_none_nonnullable(): + _test_default_none_nullable( + {'default_setter': lambda doc: 'bar_value'}) + + +def _test_default_none_nonnullable(default): + bar_schema = {'type': 'string', + 'nullable': False} + bar_schema.update(default) + schema = {'foo': {'type': 'string'}, + 'bar': bar_schema} + document = {'foo': 'foo_value', 'bar': 'bar_value'} + assert_normalized(document, document.copy(), schema) + + +def test_default_none_default_value(): + schema = {'foo': {'type': 'string'}, + 'bar': {'type': 'string', + 'nullable': True, + 'default': None}} + document = {'foo': 'foo_value'} + expected = {'foo': 'foo_value', 'bar': None} + assert_normalized(document, expected, schema) + + +def test_default_missing_in_subschema(): + _test_default_missing_in_subschema({'default': 'bar_value'}) + + +def test_default_setter_missing_in_subschema(): + _test_default_missing_in_subschema( + {'default_setter': lambda doc: 'bar_value'}) + + +def _test_default_missing_in_subschema(default): + bar_schema = {'type': 'string'} + bar_schema.update(default) + schema = {'thing': {'type': 'dict', + 'schema': {'foo': {'type': 'string'}, + 'bar': bar_schema}}} + document = {'thing': {'foo': 'foo_value'}} + expected = {'thing': {'foo': 'foo_value', + 'bar': 'bar_value'}} + assert_normalized(document, expected, schema) + + +def test_depending_default_setters(): + schema = { + 'a': {'type': 'integer'}, + 'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1}, + 'c': {'type': 'integer', 'default_setter': lambda d: d['b'] * 2}, + 'd': {'type': 'integer', + 'default_setter': lambda d: d['b'] + d['c']} + } + document = {'a': 1} + expected = {'a': 1, 'b': 2, 'c': 4, 'd': 6} + assert_normalized(document, expected, schema) + + +def test_circular_depending_default_setters(validator): + schema = { + 'a': {'type': 'integer', 'default_setter': lambda d: d['b'] + 1}, + 'b': {'type': 'integer', 'default_setter': lambda d: d['a'] + 1} + } + validator({}, schema) + assert errors.SETTING_DEFAULT_FAILED in validator._errors + + +def test_issue_250(): + # https://github.com/pyeve/cerberus/issues/250 + schema = { + 'list': { + 'type': 'list', + 'schema': { + 'type': 'dict', + 'allow_unknown': True, + 'schema': {'a': {'type': 'string'}} + } + } + } + document = {'list': {'is_a': 'mapping'}} + assert_fail(document, schema, + error=('list', ('list', 'type'), errors.BAD_TYPE, + schema['list']['type'])) + + +def test_issue_250_no_type_pass_on_list(): + # https://github.com/pyeve/cerberus/issues/250 + schema = { + 'list': { + 'schema': { + 'allow_unknown': True, + 'type': 'dict', + 'schema': {'a': {'type': 'string'}} + } + } + } + document = {'list': [{'a': 'known', 'b': 'unknown'}]} + assert_normalized(document, document, schema) + + +def test_issue_250_no_type_fail_on_dict(): + # https://github.com/pyeve/cerberus/issues/250 + schema = { + 'list': { + 'schema': { + 'allow_unknown': True, + 'schema': {'a': {'type': 'string'}} + } + } + } + document = {'list': {'a': {'a': 'known'}}} + assert_fail(document, schema, + error=('list', ('list', 'schema'), errors.BAD_TYPE_FOR_SCHEMA, + schema['list']['schema'])) + + +def test_issue_250_no_type_fail_pass_on_other(): + # https://github.com/pyeve/cerberus/issues/250 + schema = { + 'list': { + 'schema': { + 'allow_unknown': True, + 'schema': {'a': {'type': 'string'}} + } + } + } + document = {'list': 1} + assert_normalized(document, document, schema) + + +def test_allow_unknown_with_of_rules(): + # https://github.com/pyeve/cerberus/issues/251 + schema = { + 'test': { + 'oneof': [ + { + 'type': 'dict', + 'allow_unknown': True, + 'schema': {'known': {'type': 'string'}} + }, + { + 'type': 'dict', + 'schema': {'known': {'type': 'string'}} + }, + ] + } + } + # check regression and that allow unknown does not cause any different + # than expected behaviour for one-of. + document = {'test': {'known': 's'}} + assert_fail(document, schema, + error=('test', ('test', 'oneof'), + errors.ONEOF, schema['test']['oneof'])) + # check that allow_unknown is actually applied + document = {'test': {'known': 's', 'unknown': 'asd'}} + assert_success(document, schema) + + +def test_271_normalising_tuples(): + # https://github.com/pyeve/cerberus/issues/271 + schema = { + 'my_field': { + 'type': 'list', + 'schema': {'type': ('string', 'number', 'dict')} + } + } + document = {'my_field': ('foo', 'bar', 42, 'albert', + 'kandinsky', {'items': 23})} + assert_success(document, schema) + + normalized = Validator(schema).normalized(document) + assert normalized['my_field'] == ('foo', 'bar', 42, 'albert', + 'kandinsky', {'items': 23}) + + +def test_allow_unknown_wo_schema(): + # https://github.com/pyeve/cerberus/issues/302 + v = Validator({'a': {'type': 'dict', 'allow_unknown': True}}) + v({'a': {}}) + + +def test_allow_unknown_with_purge_unknown(): + validator = Validator(purge_unknown=True) + schema = {'foo': {'type': 'dict', 'allow_unknown': True}} + document = {'foo': {'bar': True}, 'bar': 'foo'} + expected = {'foo': {'bar': True}} + assert_normalized(document, expected, schema, validator) + + +def test_allow_unknown_with_purge_unknown_subdocument(): + validator = Validator(purge_unknown=True) + schema = { + 'foo': { + 'type': 'dict', + 'schema': { + 'bar': { + 'type': 'string' + } + }, + 'allow_unknown': True + } + } + document = {'foo': {'bar': 'baz', 'corge': False}, 'thud': 'xyzzy'} + expected = {'foo': {'bar': 'baz', 'corge': False}} + assert_normalized(document, expected, schema, validator) diff --git a/pipenv/vendor/cerberus/tests/test_registries.py b/pipenv/vendor/cerberus/tests/test_registries.py new file mode 100644 index 0000000000..05f01c52c1 --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_registries.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +from cerberus import schema_registry, rules_set_registry, Validator +from cerberus.tests import (assert_fail, assert_normalized, + assert_schema_error, assert_success) + + +def test_schema_registry_simple(): + schema_registry.add('foo', {'bar': {'type': 'string'}}) + schema = {'a': {'schema': 'foo'}, + 'b': {'schema': 'foo'}} + document = {'a': {'bar': 'a'}, 'b': {'bar': 'b'}} + assert_success(document, schema) + + +def test_top_level_reference(): + schema_registry.add('peng', {'foo': {'type': 'integer'}}) + document = {'foo': 42} + assert_success(document, 'peng') + + +def test_rules_set_simple(): + rules_set_registry.add('foo', {'type': 'integer'}) + assert_success({'bar': 1}, {'bar': 'foo'}) + assert_fail({'bar': 'one'}, {'bar': 'foo'}) + + +def test_allow_unknown_as_reference(): + rules_set_registry.add('foo', {'type': 'number'}) + v = Validator(allow_unknown='foo') + assert_success({0: 1}, {}, v) + assert_fail({0: 'one'}, {}, v) + + +def test_recursion(): + rules_set_registry.add('self', + {'type': 'dict', 'allow_unknown': 'self'}) + v = Validator(allow_unknown='self') + assert_success({0: {1: {2: {}}}}, {}, v) + + +def test_references_remain_unresolved(validator): + rules_set_registry.extend((('boolean', {'type': 'boolean'}), + ('booleans', {'valueschema': 'boolean'}))) + validator.schema = {'foo': 'booleans'} + assert 'booleans' == validator.schema['foo'] + assert 'boolean' == rules_set_registry._storage['booleans']['valueschema'] + + +def test_rules_registry_with_anyof_type(): + rules_set_registry.add('string_or_integer', + {'anyof_type': ['string', 'integer']}) + schema = {'soi': 'string_or_integer'} + assert_success({'soi': 'hello'}, schema) + + +def test_schema_registry_with_anyof_type(): + schema_registry.add('soi_id', {'id': {'anyof_type': ['string', 'integer']}}) + schema = {'soi': {'schema': 'soi_id'}} + assert_success({'soi': {'id': 'hello'}}, schema) + + +def test_normalization_with_rules_set(): + # https://github.com/pyeve/cerberus/issues/283 + rules_set_registry.add('foo', {'default': 42}) + assert_normalized({}, {'bar': 42}, {'bar': 'foo'}) + rules_set_registry.add('foo', {'default_setter': lambda _: 42}) + assert_normalized({}, {'bar': 42}, {'bar': 'foo'}) + rules_set_registry.add('foo', {'type': 'integer', 'nullable': True}) + assert_success({'bar': None}, {'bar': 'foo'}) + + +def test_rules_set_with_dict_field(): + document = {'a_dict': {'foo': 1}} + schema = {'a_dict': {'type': 'dict', 'schema': {'foo': 'rule'}}} + + # the schema's not yet added to the valid ones, so test the faulty first + rules_set_registry.add('rule', {'tüpe': 'integer'}) + assert_schema_error(document, schema) + + rules_set_registry.add('rule', {'type': 'integer'}) + assert_success(document, schema) diff --git a/pipenv/vendor/cerberus/tests/test_schema.py b/pipenv/vendor/cerberus/tests/test_schema.py new file mode 100644 index 0000000000..1776cae3a1 --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_schema.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +import pytest + +from cerberus import Validator, errors, SchemaError +from cerberus.schema import UnvalidatedSchema +from cerberus.tests import assert_schema_error + + +def test_empty_schema(): + validator = Validator() + with pytest.raises(SchemaError, message=errors.SCHEMA_ERROR_MISSING): + validator({}, schema=None) + + +def test_bad_schema_type(validator): + schema = "this string should really be dict" + exp_msg = errors.SCHEMA_ERROR_DEFINITION_TYPE.format(schema) + with pytest.raises(SchemaError, message=exp_msg): + validator.schema = schema + + +def test_bad_schema_type_field(validator): + field = 'foo' + schema = {field: {'schema': {'bar': {'type': 'strong'}}}} + with pytest.raises(SchemaError): + validator.schema = schema + + +def test_unknown_rule(validator): + message = "{'foo': [{'unknown': ['unknown rule']}]}" + with pytest.raises(SchemaError, message=message): + validator.schema = {'foo': {'unknown': 'rule'}} + + +def test_unknown_type(validator): + field = 'name' + value = 'catch_me' + message = str({field: [{'type': ['unallowed value %s' % value]}]}) + with pytest.raises(SchemaError, message=message): + validator.schema = {'foo': {'unknown': 'rule'}} + + +def test_bad_schema_definition(validator): + field = 'name' + message = str({field: ['must be of dict type']}) + with pytest.raises(SchemaError, message=message): + validator.schema = {field: 'this should really be a dict'} + + +def test_bad_of_rules(): + schema = {'foo': {'anyof': {'type': 'string'}}} + assert_schema_error({}, schema) + + +def test_normalization_rules_are_invalid_in_of_rules(): + schema = {0: {'anyof': [{'coerce': lambda x: x}]}} + assert_schema_error({}, schema) + + +def test_anyof_allof_schema_validate(): + # make sure schema with 'anyof' and 'allof' constraints are checked + # correctly + schema = {'doc': {'type': 'dict', + 'anyof': [ + {'schema': [{'param': {'type': 'number'}}]}]}} + assert_schema_error({'doc': 'this is my document'}, schema) + + schema = {'doc': {'type': 'dict', + 'allof': [ + {'schema': [{'param': {'type': 'number'}}]}]}} + assert_schema_error({'doc': 'this is my document'}, schema) + + +def test_repr(): + v = Validator({'foo': {'type': 'string'}}) + assert repr(v.schema) == "{'foo': {'type': 'string'}}" + + +def test_validated_schema_cache(): + v = Validator({'foozifix': {'coerce': int}}) + cache_size = len(v._valid_schemas) + + v = Validator({'foozifix': {'type': 'integer'}}) + cache_size += 1 + assert len(v._valid_schemas) == cache_size + + v = Validator({'foozifix': {'coerce': int}}) + assert len(v._valid_schemas) == cache_size + + max_cache_size = 147 + assert cache_size <= max_cache_size, \ + "There's an unexpected high amount (%s) of cached valid " \ + "definition schemas. Unless you added further tests, " \ + "there are good chances that something is wrong. " \ + "If you added tests with new schemas, you can try to " \ + "adjust the variable `max_cache_size` according to " \ + "the added schemas." % cache_size + + +def test_expansion_in_nested_schema(): + schema = {'detroit': {'schema': {'anyof_regex': ['^Aladdin', 'Sane$']}}} + v = Validator(schema) + assert (v.schema['detroit']['schema'] == + {'anyof': [{'regex': '^Aladdin'}, {'regex': 'Sane$'}]}) + + +def test_unvalidated_schema_can_be_copied(): + schema = UnvalidatedSchema() + schema_copy = schema.copy() + assert schema_copy == schema diff --git a/pipenv/vendor/cerberus/tests/test_validation.py b/pipenv/vendor/cerberus/tests/test_validation.py new file mode 100644 index 0000000000..1f828fac5b --- /dev/null +++ b/pipenv/vendor/cerberus/tests/test_validation.py @@ -0,0 +1,1579 @@ +# -*- coding: utf-8 -*- + +import re +import sys +from datetime import datetime, date +from random import choice +from string import ascii_lowercase + +from pytest import mark + +from cerberus import errors, Validator +from cerberus.tests import ( + assert_bad_type, assert_document_error, assert_fail, assert_has_error, + assert_not_has_error, assert_success +) +from cerberus.tests.conftest import sample_schema + + +def test_empty_document(): + assert_document_error(None, sample_schema, None, + errors.DOCUMENT_MISSING) + + +def test_bad_document_type(): + document = "not a dict" + assert_document_error( + document, sample_schema, None, + errors.DOCUMENT_FORMAT.format(document) + ) + + +def test_unknown_field(validator): + field = 'surname' + assert_fail({field: 'doe'}, validator=validator, + error=(field, (), errors.UNKNOWN_FIELD, None)) + assert validator.errors == {field: ['unknown field']} + + +def test_empty_field_definition(document): + field = 'name' + schema = {field: {}} + assert_success(document, schema) + + +def test_required_field(schema): + field = 'a_required_string' + required_string_extension = { + 'a_required_string': {'type': 'string', + 'minlength': 2, + 'maxlength': 10, + 'required': True}} + schema.update(required_string_extension) + assert_fail({'an_integer': 1}, schema, + error=(field, (field, 'required'), errors.REQUIRED_FIELD, + True)) + + +def test_nullable_field(): + assert_success({'a_nullable_integer': None}) + assert_success({'a_nullable_integer': 3}) + assert_success({'a_nullable_field_without_type': None}) + assert_fail({'a_nullable_integer': "foo"}) + assert_fail({'an_integer': None}) + assert_fail({'a_not_nullable_field_without_type': None}) + + +def test_readonly_field(): + field = 'a_readonly_string' + assert_fail({field: 'update me if you can'}, + error=(field, (field, 'readonly'), errors.READONLY_FIELD, True)) + + +def test_readonly_field_first_rule(): + # test that readonly rule is checked before any other rule, and blocks. + # See #63. + schema = { + 'a_readonly_number': { + 'type': 'integer', + 'readonly': True, + 'max': 1 + } + } + v = Validator(schema) + v.validate({'a_readonly_number': 2}) + # it would be a list if there's more than one error; we get a dict + # instead. + assert 'read-only' in v.errors['a_readonly_number'][0] + + +def test_readonly_field_with_default_value(): + schema = { + 'created': { + 'type': 'string', + 'readonly': True, + 'default': 'today' + }, + 'modified': { + 'type': 'string', + 'readonly': True, + 'default_setter': lambda d: d['created'] + } + } + assert_success({}, schema) + expected_errors = [('created', ('created', 'readonly'), + errors.READONLY_FIELD, + schema['created']['readonly']), + ('modified', ('modified', 'readonly'), + errors.READONLY_FIELD, + schema['modified']['readonly'])] + assert_fail({'created': 'tomorrow', 'modified': 'today'}, + schema, errors=expected_errors) + assert_fail({'created': 'today', 'modified': 'today'}, + schema, errors=expected_errors) + + +def test_nested_readonly_field_with_default_value(): + schema = { + 'some_field': { + 'type': 'dict', + 'schema': { + 'created': { + 'type': 'string', + 'readonly': True, + 'default': 'today' + }, + 'modified': { + 'type': 'string', + 'readonly': True, + 'default_setter': lambda d: d['created'] + } + } + } + } + assert_success({'some_field': {}}, schema) + expected_errors = [ + (('some_field', 'created'), + ('some_field', 'schema', 'created', 'readonly'), + errors.READONLY_FIELD, + schema['some_field']['schema']['created']['readonly']), + (('some_field', 'modified'), + ('some_field', 'schema', 'modified', 'readonly'), + errors.READONLY_FIELD, + schema['some_field']['schema']['modified']['readonly'])] + assert_fail({'some_field': {'created': 'tomorrow', 'modified': 'now'}}, + schema, errors=expected_errors) + assert_fail({'some_field': {'created': 'today', 'modified': 'today'}}, + schema, errors=expected_errors) + + +def test_repeated_readonly(validator): + # https://github.com/pyeve/cerberus/issues/311 + validator.schema = {'id': {'readonly': True}} + assert_fail({'id': 0}, validator=validator) + assert_fail({'id': 0}, validator=validator) + + +def test_not_a_string(): + assert_bad_type('a_string', 'string', 1) + + +def test_not_a_binary(): + # 'u' literal prefix produces type `str` in Python 3 + assert_bad_type('a_binary', 'binary', u"i'm not a binary") + + +def test_not_a_integer(): + assert_bad_type('an_integer', 'integer', "i'm not an integer") + + +def test_not_a_boolean(): + assert_bad_type('a_boolean', 'boolean', "i'm not a boolean") + + +def test_not_a_datetime(): + assert_bad_type('a_datetime', 'datetime', "i'm not a datetime") + + +def test_not_a_float(): + assert_bad_type('a_float', 'float', "i'm not a float") + + +def test_not_a_number(): + assert_bad_type('a_number', 'number', "i'm not a number") + + +def test_not_a_list(): + assert_bad_type('a_list_of_values', 'list', "i'm not a list") + + +def test_not_a_dict(): + assert_bad_type('a_dict', 'dict', "i'm not a dict") + + +def test_bad_max_length(schema): + field = 'a_string' + max_length = schema[field]['maxlength'] + value = "".join(choice(ascii_lowercase) for i in range(max_length + 1)) + assert_fail({field: value}, + error=(field, (field, 'maxlength'), errors.MAX_LENGTH, + max_length, (len(value),))) + + +def test_bad_max_length_binary(schema): + field = 'a_binary' + max_length = schema[field]['maxlength'] + value = b'\x00' * (max_length + 1) + assert_fail({field: value}, + error=(field, (field, 'maxlength'), errors.MAX_LENGTH, + max_length, (len(value),))) + + +def test_bad_min_length(schema): + field = 'a_string' + min_length = schema[field]['minlength'] + value = "".join(choice(ascii_lowercase) for i in range(min_length - 1)) + assert_fail({field: value}, + error=(field, (field, 'minlength'), errors.MIN_LENGTH, + min_length, (len(value),))) + + +def test_bad_min_length_binary(schema): + field = 'a_binary' + min_length = schema[field]['minlength'] + value = b'\x00' * (min_length - 1) + assert_fail({field: value}, + error=(field, (field, 'minlength'), errors.MIN_LENGTH, + min_length, (len(value),))) + + +def test_bad_max_value(schema): + def assert_bad_max_value(field, inc): + max_value = schema[field]['max'] + value = max_value + inc + assert_fail({field: value}, + error=(field, (field, 'max'), errors.MAX_VALUE, max_value)) + + field = 'an_integer' + assert_bad_max_value(field, 1) + field = 'a_float' + assert_bad_max_value(field, 1.0) + field = 'a_number' + assert_bad_max_value(field, 1) + + +def test_bad_min_value(schema): + def assert_bad_min_value(field, inc): + min_value = schema[field]['min'] + value = min_value - inc + assert_fail({field: value}, + error=(field, (field, 'min'), + errors.MIN_VALUE, min_value)) + + field = 'an_integer' + assert_bad_min_value(field, 1) + field = 'a_float' + assert_bad_min_value(field, 1.0) + field = 'a_number' + assert_bad_min_value(field, 1) + + +def test_bad_schema(): + field = 'a_dict' + subschema_field = 'address' + schema = {field: {'type': 'dict', + 'schema': {subschema_field: {'type': 'string'}, + 'city': {'type': 'string', 'required': True}} + }} + document = {field: {subschema_field: 34}} + validator = Validator(schema) + + assert_fail( + document, validator=validator, + error=(field, (field, 'schema'), errors.MAPPING_SCHEMA, + validator.schema['a_dict']['schema']), + child_errors=[ + ((field, subschema_field), + (field, 'schema', subschema_field, 'type'), + errors.BAD_TYPE, 'string'), + ((field, 'city'), (field, 'schema', 'city', 'required'), + errors.REQUIRED_FIELD, True)] + ) + + handler = errors.BasicErrorHandler + assert field in validator.errors + assert subschema_field in validator.errors[field][-1] + assert handler.messages[errors.BAD_TYPE.code].format(constraint='string') \ + in validator.errors[field][-1][subschema_field] + assert 'city' in validator.errors[field][-1] + assert (handler.messages[errors.REQUIRED_FIELD.code] + in validator.errors[field][-1]['city']) + + +def test_bad_valueschema(): + field = 'a_dict_with_valueschema' + schema_field = 'a_string' + value = {schema_field: 'not an integer'} + + exp_child_errors = [ + ((field, schema_field), (field, 'valueschema', 'type'), errors.BAD_TYPE, + 'integer')] + assert_fail({field: value}, + error=(field, (field, 'valueschema'), errors.VALUESCHEMA, + {'type': 'integer'}), child_errors=exp_child_errors) + + +def test_bad_list_of_values(validator): + field = 'a_list_of_values' + value = ['a string', 'not an integer'] + assert_fail({field: value}, validator=validator, + error=(field, (field, 'items'), errors.BAD_ITEMS, + [{'type': 'string'}, {'type': 'integer'}]), + child_errors=[((field, 1), (field, 'items', 1, 'type'), + errors.BAD_TYPE, 'integer')]) + + assert (errors.BasicErrorHandler.messages[errors.BAD_TYPE.code]. + format(constraint='integer') + in validator.errors[field][-1][1]) + + value = ['a string', 10, 'an extra item'] + assert_fail({field: value}, + error=(field, (field, 'items'), errors.ITEMS_LENGTH, + [{'type': 'string'}, {'type': 'integer'}], (2, 3))) + + +def test_bad_list_of_integers(): + field = 'a_list_of_integers' + value = [34, 'not an integer'] + assert_fail({field: value}) + + +def test_bad_list_of_dicts(): + field = 'a_list_of_dicts' + map_schema = {'sku': {'type': 'string'}, + 'price': {'type': 'integer', 'required': True}} + seq_schema = {'type': 'dict', 'schema': map_schema} + schema = {field: {'type': 'list', 'schema': seq_schema}} + validator = Validator(schema) + value = [{'sku': 'KT123', 'price': '100'}] + document = {field: value} + + assert_fail(document, validator=validator, + error=(field, (field, 'schema'), errors.SEQUENCE_SCHEMA, + seq_schema), + child_errors=[((field, 0), (field, 'schema', 'schema'), + errors.MAPPING_SCHEMA, map_schema)]) + + assert field in validator.errors + assert 0 in validator.errors[field][-1] + assert 'price' in validator.errors[field][-1][0][-1] + exp_msg = errors.BasicErrorHandler.messages[errors.BAD_TYPE.code] \ + .format(constraint='integer') + assert exp_msg in validator.errors[field][-1][0][-1]['price'] + + value = ["not a dict"] + exp_child_errors = [((field, 0), (field, 'schema', 'type'), + errors.BAD_TYPE, 'dict', ())] + assert_fail({field: value}, + error=(field, (field, 'schema'), errors.SEQUENCE_SCHEMA, + seq_schema), + child_errors=exp_child_errors) + + +def test_array_unallowed(): + field = 'an_array' + value = ['agent', 'client', 'profit'] + assert_fail({field: value}, + error=(field, (field, 'allowed'), errors.UNALLOWED_VALUES, + ['agent', 'client', 'vendor'], ['profit'])) + + +def test_string_unallowed(): + field = 'a_restricted_string' + value = 'profit' + assert_fail({field: value}, + error=(field, (field, 'allowed'), errors.UNALLOWED_VALUE, + ['agent', 'client', 'vendor'], value)) + + +def test_integer_unallowed(): + field = 'a_restricted_integer' + value = 2 + assert_fail({field: value}, + error=(field, (field, 'allowed'), errors.UNALLOWED_VALUE, + [-1, 0, 1], value)) + + +def test_integer_allowed(): + assert_success({'a_restricted_integer': -1}) + + +def test_validate_update(): + assert_success({'an_integer': 100, + 'a_dict': {'address': 'adr'}, + 'a_list_of_dicts': [{'sku': 'let'}] + }, update=True) + + +def test_string(): + assert_success({'a_string': 'john doe'}) + + +def test_string_allowed(): + assert_success({'a_restricted_string': 'client'}) + + +def test_integer(): + assert_success({'an_integer': 50}) + + +def test_boolean(): + assert_success({'a_boolean': True}) + + +def test_datetime(): + assert_success({'a_datetime': datetime.now()}) + + +def test_float(): + assert_success({'a_float': 3.5}) + assert_success({'a_float': 1}) + + +def test_number(): + assert_success({'a_number': 3.5}) + assert_success({'a_number': 3}) + + +def test_array(): + assert_success({'an_array': ['agent', 'client']}) + + +def test_set(): + assert_success({'a_set': set(['hello', 1])}) + + +def test_one_of_two_types(validator): + field = 'one_or_more_strings' + assert_success({field: 'foo'}) + assert_success({field: ['foo', 'bar']}) + exp_child_errors = [((field, 1), (field, 'schema', 'type'), + errors.BAD_TYPE, 'string')] + assert_fail({field: ['foo', 23]}, validator=validator, + error=(field, (field, 'schema'), errors.SEQUENCE_SCHEMA, + {'type': 'string'}), + child_errors=exp_child_errors) + assert_fail({field: 23}, + error=((field,), (field, 'type'), errors.BAD_TYPE, + ['string', 'list'])) + assert validator.errors == {field: [{1: ['must be of string type']}]} + + +def test_regex(validator): + field = 'a_regex_email' + assert_success({field: 'valid.email@gmail.com'}, validator=validator) + assert_fail({field: 'invalid'}, update=True, + error=(field, (field, 'regex'), errors.REGEX_MISMATCH, + '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')) + + +def test_a_list_of_dicts(): + assert_success( + { + 'a_list_of_dicts': [ + {'sku': 'AK345', 'price': 100}, + {'sku': 'YZ069', 'price': 25} + ] + } + ) + + +def test_a_list_of_values(): + assert_success({'a_list_of_values': ['hello', 100]}) + + +def test_a_list_of_integers(): + assert_success({'a_list_of_integers': [99, 100]}) + + +def test_a_dict(schema): + assert_success({'a_dict': {'address': 'i live here', + 'city': 'in my own town'}}) + assert_fail( + {'a_dict': {'address': 8545}}, + error=('a_dict', ('a_dict', 'schema'), errors.MAPPING_SCHEMA, + schema['a_dict']['schema']), + child_errors=[(('a_dict', 'address'), + ('a_dict', 'schema', 'address', 'type'), + errors.BAD_TYPE, 'string'), + (('a_dict', 'city'), + ('a_dict', 'schema', 'city', 'required'), + errors.REQUIRED_FIELD, True)] + ) + + +def test_a_dict_with_valueschema(validator): + assert_success({'a_dict_with_valueschema': + {'an integer': 99, 'another integer': 100}}) + + error = ( + 'a_dict_with_valueschema', ('a_dict_with_valueschema', 'valueschema'), + errors.VALUESCHEMA, {'type': 'integer'}) + child_errors = [ + (('a_dict_with_valueschema', 'a string'), + ('a_dict_with_valueschema', 'valueschema', 'type'), + errors.BAD_TYPE, 'integer')] + + assert_fail({'a_dict_with_valueschema': {'a string': '99'}}, + validator=validator, error=error, child_errors=child_errors) + + assert 'valueschema' in \ + validator.schema_error_tree['a_dict_with_valueschema'] + v = validator.schema_error_tree + assert len(v['a_dict_with_valueschema']['valueschema'].descendants) == 1 + + +def test_a_dict_with_keyschema(): + assert_success({'a_dict_with_keyschema': {'key': 'value'}}) + assert_fail({'a_dict_with_keyschema': {'KEY': 'value'}}) + + +def test_a_list_length(schema): + field = 'a_list_length' + min_length = schema[field]['minlength'] + max_length = schema[field]['maxlength'] + + assert_fail({field: [1] * (min_length - 1)}, + error=(field, (field, 'minlength'), errors.MIN_LENGTH, + min_length, (min_length - 1,))) + + for i in range(min_length, max_length): + value = [1] * i + assert_success({field: value}) + + assert_fail({field: [1] * (max_length + 1)}, + error=(field, (field, 'maxlength'), errors.MAX_LENGTH, + max_length, (max_length + 1,))) + + +def test_custom_datatype(): + class MyValidator(Validator): + def _validate_type_objectid(self, value): + if re.match('[a-f0-9]{24}', value): + return True + + schema = {'test_field': {'type': 'objectid'}} + validator = MyValidator(schema) + assert_success({'test_field': '50ad188438345b1049c88a28'}, + validator=validator) + assert_fail({'test_field': 'hello'}, validator=validator, + error=('test_field', ('test_field', 'type'), errors.BAD_TYPE, + 'objectid')) + + +def test_custom_datatype_rule(): + class MyValidator(Validator): + def _validate_min_number(self, min_number, field, value): + """ {'type': 'number'} """ + if value < min_number: + self._error(field, 'Below the min') + + # TODO replace with TypeDefintion in next major release + def _validate_type_number(self, value): + if isinstance(value, int): + return True + + schema = {'test_field': {'min_number': 1, 'type': 'number'}} + validator = MyValidator(schema) + assert_fail({'test_field': '0'}, validator=validator, + error=('test_field', ('test_field', 'type'), errors.BAD_TYPE, + 'number')) + assert_fail({'test_field': 0}, validator=validator, + error=('test_field', (), errors.CUSTOM, None, + ('Below the min',))) + assert validator.errors == {'test_field': ['Below the min']} + + +def test_custom_validator(): + class MyValidator(Validator): + def _validate_isodd(self, isodd, field, value): + """ {'type': 'boolean'} """ + if isodd and not bool(value & 1): + self._error(field, 'Not an odd number') + + schema = {'test_field': {'isodd': True}} + validator = MyValidator(schema) + assert_success({'test_field': 7}, validator=validator) + assert_fail({'test_field': 6}, validator=validator, + error=('test_field', (), errors.CUSTOM, None, + ('Not an odd number',))) + assert validator.errors == {'test_field': ['Not an odd number']} + + +@mark.parametrize('value, _type', + (('', 'string'), ((), 'list'), ({}, 'dict'), ([], 'list'))) +def test_empty_values(value, _type): + field = 'test' + schema = {field: {'type': _type}} + document = {field: value} + + assert_success(document, schema) + + schema[field]['empty'] = False + assert_fail(document, schema, + error=(field, (field, 'empty'), + errors.EMPTY_NOT_ALLOWED, False)) + + schema[field]['empty'] = True + assert_success(document, schema) + + +def test_empty_skips_regex(validator): + schema = {'foo': {'empty': True, 'regex': r'\d?\d\.\d\d', + 'type': 'string'}} + assert validator({'foo': ''}, schema) + + +def test_ignore_none_values(): + field = 'test' + schema = {field: {'type': 'string', 'empty': False, 'required': False}} + document = {field: None} + + # Test normal behaviour + validator = Validator(schema, ignore_none_values=False) + assert_fail(document, validator=validator) + validator.schema[field]['required'] = True + validator.schema.validate() + _errors = assert_fail(document, validator=validator) + assert_not_has_error(_errors, field, (field, 'required'), + errors.REQUIRED_FIELD, True) + + # Test ignore None behaviour + validator = Validator(schema, ignore_none_values=True) + validator.schema[field]['required'] = False + validator.schema.validate() + assert_success(document, validator=validator) + validator.schema[field]['required'] = True + _errors = assert_fail(schema=schema, document=document, validator=validator) + assert_has_error(_errors, field, (field, 'required'), errors.REQUIRED_FIELD, + True) + assert_not_has_error(_errors, field, (field, 'type'), errors.BAD_TYPE, + 'string') + + +def test_unknown_keys(): + schema = {} + + # test that unknown fields are allowed when allow_unknown is True. + v = Validator(allow_unknown=True, schema=schema) + assert_success({"unknown1": True, "unknown2": "yes"}, validator=v) + + # test that unknown fields are allowed only if they meet the + # allow_unknown schema when provided. + v.allow_unknown = {'type': 'string'} + assert_success(document={'name': 'mark'}, validator=v) + assert_fail({"name": 1}, validator=v) + + # test that unknown fields are not allowed if allow_unknown is False + v.allow_unknown = False + assert_fail({'name': 'mark'}, validator=v) + + +def test_unknown_key_dict(validator): + # https://github.com/pyeve/cerberus/issues/177 + validator.allow_unknown = True + document = {'a_dict': {'foo': 'foo_value', 'bar': 25}} + assert_success(document, {}, validator=validator) + + +def test_unknown_key_list(validator): + # https://github.com/pyeve/cerberus/issues/177 + validator.allow_unknown = True + document = {'a_dict': ['foo', 'bar']} + assert_success(document, {}, validator=validator) + + +def test_unknown_keys_list_of_dicts(validator): + # test that allow_unknown is honored even for subdicts in lists. + # https://github.com/pyeve/cerberus/issues/67. + validator.allow_unknown = True + document = {'a_list_of_dicts': [{'sku': 'YZ069', 'price': 25, + 'extra': True}]} + assert_success(document, validator=validator) + + +def test_unknown_keys_retain_custom_rules(): + # test that allow_unknown schema respect custom validation rules. + # https://github.com/pyeve/cerberus/issues/#66. + class CustomValidator(Validator): + def _validate_type_foo(self, value): + if value == "foo": + return True + + validator = CustomValidator({}) + validator.allow_unknown = {"type": "foo"} + assert_success(document={"fred": "foo", "barney": "foo"}, + validator=validator) + + +def test_nested_unknown_keys(): + schema = { + 'field1': { + 'type': 'dict', + 'allow_unknown': True, + 'schema': {'nested1': {'type': 'string'}} + } + } + document = { + 'field1': { + 'nested1': 'foo', + 'arb1': 'bar', + 'arb2': 42 + } + } + assert_success(document=document, schema=schema) + + schema['field1']['allow_unknown'] = {'type': 'string'} + assert_fail(document=document, schema=schema) + + +def test_novalidate_noerrors(validator): + """ + In v0.1.0 and below `self.errors` raised an exception if no + validation had been performed yet. + """ + assert validator.errors == {} + + +def test_callable_validator(): + """ + Validator instance is callable, functions as a shorthand + passthrough to validate() + """ + schema = {'test_field': {'type': 'string'}} + v = Validator(schema) + assert v.validate({'test_field': 'foo'}) + assert v({'test_field': 'foo'}) + assert not v.validate({'test_field': 1}) + assert not v({'test_field': 1}) + + +def test_dependencies_field(): + schema = {'test_field': {'dependencies': 'foo'}, + 'foo': {'type': 'string'}} + assert_success({'test_field': 'foobar', 'foo': 'bar'}, schema) + assert_fail({'test_field': 'foobar'}, schema) + + +def test_dependencies_list(): + schema = { + 'test_field': {'dependencies': ['foo', 'bar']}, + 'foo': {'type': 'string'}, + 'bar': {'type': 'string'} + } + assert_success({'test_field': 'foobar', 'foo': 'bar', 'bar': 'foo'}, + schema) + assert_fail({'test_field': 'foobar', 'foo': 'bar'}, schema) + + +def test_dependencies_list_with_required_field(): + schema = { + 'test_field': {'required': True, 'dependencies': ['foo', 'bar']}, + 'foo': {'type': 'string'}, + 'bar': {'type': 'string'} + } + # False: all dependencies missing + assert_fail({'test_field': 'foobar'}, schema) + # False: one of dependencies missing + assert_fail({'test_field': 'foobar', 'foo': 'bar'}, schema) + # False: one of dependencies missing + assert_fail({'test_field': 'foobar', 'bar': 'foo'}, schema) + # False: dependencies are validated and field is required + assert_fail({'foo': 'bar', 'bar': 'foo'}, schema) + # False: All dependencies are optional but field is still required + assert_fail({}, schema) + # True: dependency missing + assert_fail({'foo': 'bar'}, schema) + # True: dependencies are validated but field is not required + schema['test_field']['required'] = False + assert_success({'foo': 'bar', 'bar': 'foo'}, schema) + + +def test_dependencies_list_with_subodcuments_fields(): + schema = { + 'test_field': {'dependencies': ['a_dict.foo', 'a_dict.bar']}, + 'a_dict': { + 'type': 'dict', + 'schema': { + 'foo': {'type': 'string'}, + 'bar': {'type': 'string'} + } + } + } + assert_success({'test_field': 'foobar', + 'a_dict': {'foo': 'foo', 'bar': 'bar'}}, schema) + assert_fail({'test_field': 'foobar', 'a_dict': {}}, schema) + assert_fail({'test_field': 'foobar', + 'a_dict': {'foo': 'foo'}}, schema) + + +def test_dependencies_dict(): + schema = { + 'test_field': {'dependencies': {'foo': 'foo', 'bar': 'bar'}}, + 'foo': {'type': 'string'}, + 'bar': {'type': 'string'} + } + assert_success({'test_field': 'foobar', 'foo': 'foo', 'bar': 'bar'}, + schema) + assert_fail({'test_field': 'foobar', 'foo': 'foo'}, schema) + assert_fail({'test_field': 'foobar', 'foo': 'bar'}, schema) + assert_fail({'test_field': 'foobar', 'bar': 'bar'}, schema) + assert_fail({'test_field': 'foobar', 'bar': 'foo'}, schema) + assert_fail({'test_field': 'foobar'}, schema) + + +def test_dependencies_dict_with_required_field(): + schema = { + 'test_field': { + 'required': True, + 'dependencies': {'foo': 'foo', 'bar': 'bar'} + }, + 'foo': {'type': 'string'}, + 'bar': {'type': 'string'} + } + # False: all dependencies missing + assert_fail({'test_field': 'foobar'}, schema) + # False: one of dependencies missing + assert_fail({'test_field': 'foobar', 'foo': 'foo'}, schema) + assert_fail({'test_field': 'foobar', 'bar': 'bar'}, schema) + # False: dependencies are validated and field is required + assert_fail({'foo': 'foo', 'bar': 'bar'}, schema) + # False: All dependencies are optional, but field is still required + assert_fail({}, schema) + # False: dependency missing + assert_fail({'foo': 'bar'}, schema) + + assert_success({'test_field': 'foobar', 'foo': 'foo', 'bar': 'bar'}, + schema) + + # True: dependencies are validated but field is not required + schema['test_field']['required'] = False + assert_success({'foo': 'bar', 'bar': 'foo'}, schema) + + +def test_dependencies_field_satisfy_nullable_field(): + # https://github.com/pyeve/cerberus/issues/305 + schema = { + 'foo': {'nullable': True}, + 'bar': {'dependencies': 'foo'} + } + + assert_success({'foo': None, 'bar': 1}, schema) + assert_success({'foo': None}, schema) + assert_fail({'bar': 1}, schema) + + +def test_dependencies_field_with_mutually_dependent_nullable_fields(): + # https://github.com/pyeve/cerberus/pull/306 + schema = { + 'foo': {'dependencies': 'bar', 'nullable': True}, + 'bar': {'dependencies': 'foo', 'nullable': True} + } + assert_success({'foo': None, 'bar': None}, schema) + assert_success({'foo': 1, 'bar': 1}, schema) + assert_success({'foo': None, 'bar': 1}, schema) + assert_fail({'foo': None}, schema) + assert_fail({'foo': 1}, schema) + + +def test_dependencies_dict_with_subdocuments_fields(): + schema = { + 'test_field': {'dependencies': {'a_dict.foo': ['foo', 'bar'], + 'a_dict.bar': 'bar'}}, + 'a_dict': { + 'type': 'dict', + 'schema': { + 'foo': {'type': 'string'}, + 'bar': {'type': 'string'} + } + } + } + assert_success({'test_field': 'foobar', + 'a_dict': {'foo': 'foo', 'bar': 'bar'}}, schema) + assert_success({'test_field': 'foobar', + 'a_dict': {'foo': 'bar', 'bar': 'bar'}}, schema) + assert_fail({'test_field': 'foobar', 'a_dict': {}}, schema) + assert_fail({'test_field': 'foobar', + 'a_dict': {'foo': 'foo', 'bar': 'foo'}}, schema) + assert_fail({'test_field': 'foobar', 'a_dict': {'bar': 'foo'}}, + schema) + assert_fail({'test_field': 'foobar', 'a_dict': {'bar': 'bar'}}, + schema) + + +def test_root_relative_dependencies(): + # https://github.com/pyeve/cerberus/issues/288 + subschema = {'version': {'dependencies': '^repo'}} + schema = {'package': {'allow_unknown': True, 'schema': subschema}, + 'repo': {}} + assert_fail( + {'package': {'repo': 'somewhere', 'version': 0}}, schema, + error=('package', ('package', 'schema'), + errors.MAPPING_SCHEMA, subschema), + child_errors=[( + ('package', 'version'), + ('package', 'schema', 'version', 'dependencies'), + errors.DEPENDENCIES_FIELD, '^repo', ('^repo',) + )] + ) + assert_success({'repo': 'somewhere', 'package': {'version': 1}}, schema) + + +def test_dependencies_errors(): + v = Validator({'field1': {'required': False}, + 'field2': {'required': True, + 'dependencies': {'field1': ['one', 'two']}}}) + assert_fail({'field1': 'three', 'field2': 7}, validator=v, + error=('field2', ('field2', 'dependencies'), + errors.DEPENDENCIES_FIELD_VALUE, + {'field1': ['one', 'two']}, ({'field1': 'three'},))) + + +def test_options_passed_to_nested_validators(validator): + validator.schema = {'sub_dict': {'type': 'dict', + 'schema': {'foo': {'type': 'string'}}}} + validator.allow_unknown = True + assert_success({'sub_dict': {'foo': 'bar', 'unknown': True}}, + validator=validator) + + +def test_self_root_document(): + """ Make sure self.root_document is always the root document. + See: + * https://github.com/pyeve/cerberus/pull/42 + * https://github.com/pyeve/eve/issues/295 + """ + + class MyValidator(Validator): + def _validate_root_doc(self, root_doc, field, value): + """ {'type': 'boolean'} """ + if ('sub' not in self.root_document or + len(self.root_document['sub']) != 2): + self._error(field, 'self.context is not the root doc!') + + schema = { + 'sub': { + 'type': 'list', + 'root_doc': True, + 'schema': { + 'type': 'dict', + 'schema': { + 'foo': { + 'type': 'string', + 'root_doc': True + } + } + } + } + } + assert_success({'sub': [{'foo': 'bar'}, {'foo': 'baz'}]}, + validator=MyValidator(schema)) + + +def test_validator_rule(validator): + def validate_name(field, value, error): + if not value.islower(): + error(field, 'must be lowercase') + + validator.schema = { + 'name': {'validator': validate_name}, + 'age': {'type': 'integer'} + } + + assert_fail({'name': 'ItsMe', 'age': 2}, validator=validator, + error=('name', (), errors.CUSTOM, None, ('must be lowercase',))) + assert validator.errors == {'name': ['must be lowercase']} + assert_success({'name': 'itsme', 'age': 2}, validator=validator) + + +def test_validated(validator): + validator.schema = {'property': {'type': 'string'}} + document = {'property': 'string'} + assert validator.validated(document) == document + document = {'property': 0} + assert validator.validated(document) is None + + +def test_anyof(): + # prop1 must be either a number between 0 and 10 + schema = {'prop1': {'min': 0, 'max': 10}} + doc = {'prop1': 5} + + assert_success(doc, schema) + + # prop1 must be either a number between 0 and 10 or 100 and 110 + schema = {'prop1': {'anyof': + [{'min': 0, 'max': 10}, {'min': 100, 'max': 110}]}} + doc = {'prop1': 105} + + assert_success(doc, schema) + + # prop1 must be either a number between 0 and 10 or 100 and 110 + schema = {'prop1': {'anyof': + [{'min': 0, 'max': 10}, {'min': 100, 'max': 110}]}} + doc = {'prop1': 50} + + assert_fail(doc, schema) + + # prop1 must be an integer that is either be + # greater than or equal to 0, or greater than or equal to 10 + schema = {'prop1': {'type': 'integer', + 'anyof': [{'min': 0}, {'min': 10}]}} + assert_success({'prop1': 10}, schema) + # test that intermediate schemas do not sustain + assert 'type' not in schema['prop1']['anyof'][0] + assert 'type' not in schema['prop1']['anyof'][1] + assert 'allow_unknown' not in schema['prop1']['anyof'][0] + assert 'allow_unknown' not in schema['prop1']['anyof'][1] + assert_success({'prop1': 5}, schema) + + exp_child_errors = [ + (('prop1',), ('prop1', 'anyof', 0, 'min'), errors.MIN_VALUE, 0), + (('prop1',), ('prop1', 'anyof', 1, 'min'), errors.MIN_VALUE, 10) + ] + assert_fail({'prop1': -1}, schema, + error=(('prop1',), ('prop1', 'anyof'), errors.ANYOF, + [{'min': 0}, {'min': 10}]), + child_errors=exp_child_errors) + doc = {'prop1': 5.5} + assert_fail(doc, schema) + doc = {'prop1': '5.5'} + assert_fail(doc, schema) + + +def test_allof(): + # prop1 has to be a float between 0 and 10 + schema = {'prop1': {'allof': [ + {'type': 'float'}, {'min': 0}, {'max': 10}]}} + doc = {'prop1': -1} + assert_fail(doc, schema) + doc = {'prop1': 5} + assert_success(doc, schema) + doc = {'prop1': 11} + assert_fail(doc, schema) + + # prop1 has to be a float and an integer + schema = {'prop1': {'allof': [{'type': 'float'}, {'type': 'integer'}]}} + doc = {'prop1': 11} + assert_success(doc, schema) + doc = {'prop1': 11.5} + assert_fail(doc, schema) + doc = {'prop1': '11'} + assert_fail(doc, schema) + + +def test_unicode_allowed(): + # issue 280 + doc = {'letters': u'♄εℓł☺'} + + schema = {'letters': {'type': 'string', 'allowed': ['a', 'b', 'c']}} + assert_fail(doc, schema) + + schema = {'letters': {'type': 'string', 'allowed': [u'♄εℓł☺']}} + assert_success(doc, schema) + + schema = {'letters': {'type': 'string', 'allowed': ['♄εℓł☺']}} + doc = {'letters': '♄εℓł☺'} + assert_success(doc, schema) + + +@mark.skipif(sys.version_info[0] < 3, + reason='requires python 3.x') +def test_unicode_allowed_py3(): + """ All strings are unicode in Python 3.x. Input doc and schema + have equal strings and validation yield success.""" + + # issue 280 + doc = {'letters': u'♄εℓł☺'} + schema = {'letters': {'type': 'string', 'allowed': ['♄εℓł☺']}} + assert_success(doc, schema) + + +@mark.skipif(sys.version_info[0] > 2, + reason='requires python 2.x') +def test_unicode_allowed_py2(): + """ Python 2.x encodes value of allowed using default encoding if + the string includes characters outside ASCII range. Produced string + does not match input which is an unicode string.""" + + # issue 280 + doc = {'letters': u'♄εℓł☺'} + schema = {'letters': {'type': 'string', 'allowed': ['♄εℓł☺']}} + assert_fail(doc, schema) + + +def test_oneof(): + # prop1 can only only be: + # - greater than 10 + # - greater than 0 + # - equal to -5, 5, or 15 + + schema = {'prop1': {'type': 'integer', 'oneof': [ + {'min': 0}, + {'min': 10}, + {'allowed': [-5, 5, 15]}]}} + + # document is not valid + # prop1 not greater than 0, 10 or equal to -5 + doc = {'prop1': -1} + assert_fail(doc, schema) + + # document is valid + # prop1 is less then 0, but is -5 + doc = {'prop1': -5} + assert_success(doc, schema) + + # document is valid + # prop1 greater than 0 + doc = {'prop1': 1} + assert_success(doc, schema) + + # document is not valid + # prop1 is greater than 0 + # and equal to 5 + doc = {'prop1': 5} + assert_fail(doc, schema) + + # document is not valid + # prop1 is greater than 0 + # and greater than 10 + doc = {'prop1': 11} + assert_fail(doc, schema) + + # document is not valid + # prop1 is greater than 0 + # and greater than 10 + # and equal to 15 + doc = {'prop1': 15} + assert_fail(doc, schema) + + +def test_noneof(): + # prop1 can not be: + # - greater than 10 + # - greater than 0 + # - equal to -5, 5, or 15 + + schema = {'prop1': {'type': 'integer', 'noneof': [ + {'min': 0}, + {'min': 10}, + {'allowed': [-5, 5, 15]}]}} + + # document is valid + doc = {'prop1': -1} + assert_success(doc, schema) + + # document is not valid + # prop1 is equal to -5 + doc = {'prop1': -5} + assert_fail(doc, schema) + + # document is not valid + # prop1 greater than 0 + doc = {'prop1': 1} + assert_fail(doc, schema) + + # document is not valid + doc = {'prop1': 5} + assert_fail(doc, schema) + + # document is not valid + doc = {'prop1': 11} + assert_fail(doc, schema) + + # document is not valid + # and equal to 15 + doc = {'prop1': 15} + assert_fail(doc, schema) + + +def test_anyof_allof(): + # prop1 can be any number outside of [0-10] + schema = {'prop1': {'allof': [{'anyof': [{'type': 'float'}, + {'type': 'integer'}]}, + {'anyof': [{'min': 10}, + {'max': 0}]} + ]}} + + doc = {'prop1': 11} + assert_success(doc, schema) + doc = {'prop1': -1} + assert_success(doc, schema) + doc = {'prop1': 5} + assert_fail(doc, schema) + + doc = {'prop1': 11.5} + assert_success(doc, schema) + doc = {'prop1': -1.5} + assert_success(doc, schema) + doc = {'prop1': 5.5} + assert_fail(doc, schema) + + doc = {'prop1': '5.5'} + assert_fail(doc, schema) + + +def test_anyof_schema(validator): + # test that a list of schemas can be specified. + + valid_parts = [{'schema': {'model number': {'type': 'string'}, + 'count': {'type': 'integer'}}}, + {'schema': {'serial number': {'type': 'string'}, + 'count': {'type': 'integer'}}}] + valid_item = {'type': ['dict', 'string'], 'anyof': valid_parts} + schema = {'parts': {'type': 'list', 'schema': valid_item}} + document = {'parts': [{'model number': 'MX-009', 'count': 100}, + {'serial number': '898-001'}, + 'misc']} + + # document is valid. each entry in 'parts' matches a type or schema + assert_success(document, schema, validator=validator) + + document['parts'].append({'product name': "Monitors", 'count': 18}) + # document is invalid. 'product name' does not match any valid schemas + assert_fail(document, schema, validator=validator) + + document['parts'].pop() + # document is valid again + assert_success(document, schema, validator=validator) + + document['parts'].append({'product name': "Monitors", 'count': 18}) + document['parts'].append(10) + # and invalid. numbers are not allowed. + + exp_child_errors = [ + (('parts', 3), ('parts', 'schema', 'anyof'), errors.ANYOF, + valid_parts), + (('parts', 4), ('parts', 'schema', 'type'), errors.BAD_TYPE, + ['dict', 'string']) + ] + + _errors = assert_fail(document, schema, validator=validator, + error=('parts', ('parts', 'schema'), + errors.SEQUENCE_SCHEMA, valid_item), + child_errors=exp_child_errors) + assert_not_has_error(_errors, ('parts', 4), ('parts', 'schema', 'anyof'), + errors.ANYOF, valid_parts) + + # tests errors.BasicErrorHandler's tree representation + v_errors = validator.errors + assert 'parts' in v_errors + assert 3 in v_errors['parts'][-1] + assert v_errors['parts'][-1][3][0] == "no definitions validate" + scope = v_errors['parts'][-1][3][-1] + assert 'anyof definition 0' in scope + assert 'anyof definition 1' in scope + assert scope['anyof definition 0'] == [{"product name": ["unknown field"]}] + assert scope['anyof definition 1'] == [{"product name": ["unknown field"]}] + assert v_errors['parts'][-1][4] == ["must be of ['dict', 'string'] type"] + + +def test_anyof_2(): + # these two schema should be the same + schema1 = {'prop': {'anyof': [{'type': 'dict', + 'schema': { + 'val': {'type': 'integer'}}}, + {'type': 'dict', + 'schema': { + 'val': {'type': 'string'}}}]}} + schema2 = {'prop': {'type': 'dict', 'anyof': [ + {'schema': {'val': {'type': 'integer'}}}, + {'schema': {'val': {'type': 'string'}}}]}} + + doc = {'prop': {'val': 0}} + assert_success(doc, schema1) + assert_success(doc, schema2) + + doc = {'prop': {'val': '0'}} + assert_success(doc, schema1) + assert_success(doc, schema2) + + doc = {'prop': {'val': 1.1}} + assert_fail(doc, schema1) + assert_fail(doc, schema2) + + +def test_anyof_type(): + schema = {'anyof_type': {'anyof_type': ['string', 'integer']}} + assert_success({'anyof_type': 'bar'}, schema) + assert_success({'anyof_type': 23}, schema) + + +def test_oneof_schema(): + schema = {'oneof_schema': {'type': 'dict', + 'oneof_schema': + [{'digits': {'type': 'integer', + 'min': 0, 'max': 99}}, + {'text': {'type': 'string', + 'regex': '^[0-9]{2}$'}}]}} + assert_success({'oneof_schema': {'digits': 19}}, schema) + assert_success({'oneof_schema': {'text': '84'}}, schema) + assert_fail({'oneof_schema': {'digits': 19, 'text': '84'}}, schema) + + +def test_nested_oneof_type(): + schema = {'nested_oneof_type': + {'valueschema': {'oneof_type': ['string', 'integer']}}} + assert_success({'nested_oneof_type': {'foo': 'a'}}, schema) + assert_success({'nested_oneof_type': {'bar': 3}}, schema) + + +def test_nested_oneofs(validator): + validator.schema = {'abc': { + 'type': 'dict', + 'oneof_schema': [ + {'foo': { + 'type': 'dict', + 'schema': {'bar': {'oneof_type': ['integer', 'float']}} + }}, + {'baz': {'type': 'string'}} + ]}} + + document = {'abc': {'foo': {'bar': 'bad'}}} + + expected_errors = { + 'abc': [ + 'none or more than one rule validate', + {'oneof definition 0': [ + {'foo': [{'bar': [ + 'none or more than one rule validate', + {'oneof definition 0': ['must be of integer type'], + 'oneof definition 1': ['must be of float type']} + ]}]}], + 'oneof definition 1': [{'foo': ['unknown field']}]} + ] + } + + assert_fail(document, validator=validator) + assert validator.errors == expected_errors + + +def test_no_of_validation_if_type_fails(validator): + valid_parts = [{'schema': {'model number': {'type': 'string'}, + 'count': {'type': 'integer'}}}, + {'schema': {'serial number': {'type': 'string'}, + 'count': {'type': 'integer'}}}] + validator.schema = {'part': {'type': ['dict', 'string'], + 'anyof': valid_parts}} + document = {'part': 10} + _errors = assert_fail(document, validator=validator) + assert len(_errors) == 1 + + +def test_issue_107(validator): + schema = {'info': {'type': 'dict', + 'schema': {'name': {'type': 'string', + 'required': True}}}} + document = {'info': {'name': 'my name'}} + assert_success(document, schema, validator=validator) + + v = Validator(schema) + assert_success(document, schema, v) + # it once was observed that this behaves other than the previous line + assert v.validate(document) + + +def test_dont_type_validate_nulled_values(validator): + assert_fail({'an_integer': None}, validator=validator) + assert validator.errors == {'an_integer': ['null value not allowed']} + + +def test_dependencies_error(validator): + schema = {'field1': {'required': False}, + 'field2': {'required': True, + 'dependencies': {'field1': ['one', 'two']}}} + validator.validate({'field2': 7}, schema) + exp_msg = errors.BasicErrorHandler \ + .messages[errors.DEPENDENCIES_FIELD_VALUE.code] \ + .format(field='field2', constraint={'field1': ['one', 'two']}) + assert validator.errors == {'field2': [exp_msg]} + + +def test_dependencies_on_boolean_field_with_one_value(): + # https://github.com/pyeve/cerberus/issues/138 + schema = {'deleted': {'type': 'boolean'}, + 'text': {'dependencies': {'deleted': False}}} + try: + assert_success({'text': 'foo', 'deleted': False}, schema) + assert_fail({'text': 'foo', 'deleted': True}, schema) + assert_fail({'text': 'foo'}, schema) + except TypeError as e: + if str(e) == "argument of type 'bool' is not iterable": + raise AssertionError( + "Bug #138 still exists, couldn't use boolean in dependency " + "without putting it in a list.\n" + "'some_field': True vs 'some_field: [True]") + else: + raise + + +def test_dependencies_on_boolean_field_with_value_in_list(): + # https://github.com/pyeve/cerberus/issues/138 + schema = {'deleted': {'type': 'boolean'}, + 'text': {'dependencies': {'deleted': [False]}}} + + assert_success({'text': 'foo', 'deleted': False}, schema) + assert_fail({'text': 'foo', 'deleted': True}, schema) + assert_fail({'text': 'foo'}, schema) + + +def test_document_path(): + class DocumentPathTester(Validator): + def _validate_trail(self, constraint, field, value): + """ {'type': 'boolean'} """ + test_doc = self.root_document + for crumb in self.document_path: + test_doc = test_doc[crumb] + assert test_doc == self.document + + v = DocumentPathTester() + schema = {'foo': {'schema': {'bar': {'trail': True}}}} + document = {'foo': {'bar': {}}} + assert_success(document, schema, validator=v) + + +def test_excludes(): + schema = {'this_field': {'type': 'dict', + 'excludes': 'that_field'}, + 'that_field': {'type': 'dict'}} + assert_success({'this_field': {}}, schema) + assert_success({'that_field': {}}, schema) + assert_success({}, schema) + assert_fail({'that_field': {}, 'this_field': {}}, schema) + + +def test_mutual_excludes(): + schema = {'this_field': {'type': 'dict', + 'excludes': 'that_field'}, + 'that_field': {'type': 'dict', + 'excludes': 'this_field'}} + assert_success({'this_field': {}}, schema) + assert_success({'that_field': {}}, schema) + assert_success({}, schema) + assert_fail({'that_field': {}, 'this_field': {}}, schema) + + +def test_required_excludes(): + schema = {'this_field': {'type': 'dict', + 'excludes': 'that_field', + 'required': True}, + 'that_field': {'type': 'dict', + 'excludes': 'this_field', + 'required': True}} + assert_success({'this_field': {}}, schema, update=False) + assert_success({'that_field': {}}, schema, update=False) + assert_fail({}, schema) + assert_fail({'that_field': {}, 'this_field': {}}, schema) + + +def test_multiples_exclusions(): + schema = {'this_field': {'type': 'dict', + 'excludes': ['that_field', 'bazo_field']}, + 'that_field': {'type': 'dict', + 'excludes': 'this_field'}, + 'bazo_field': {'type': 'dict'}} + assert_success({'this_field': {}}, schema) + assert_success({'that_field': {}}, schema) + assert_fail({'this_field': {}, 'that_field': {}}, schema) + assert_fail({'this_field': {}, 'bazo_field': {}}, schema) + assert_fail({'that_field': {}, 'this_field': {}, 'bazo_field': {}}, schema) + assert_success({'that_field': {}, 'bazo_field': {}}, schema) + + +def test_bad_excludes_fields(validator): + validator.schema = {'this_field': {'type': 'dict', + 'excludes': ['that_field', 'bazo_field'], + 'required': True}, + 'that_field': {'type': 'dict', + 'excludes': 'this_field', + 'required': True}} + assert_fail({'that_field': {}, 'this_field': {}}, validator=validator) + handler = errors.BasicErrorHandler + assert (validator.errors == + {'that_field': + [handler.messages[errors.EXCLUDES_FIELD.code].format( + "'this_field'", field="that_field")], + 'this_field': + [handler.messages[errors.EXCLUDES_FIELD.code].format( + "'that_field', 'bazo_field'", field="this_field")]}) + + +def test_boolean_is_not_a_number(): + # https://github.com/pyeve/cerberus/issues/144 + assert_fail({'value': True}, {'value': {'type': 'number'}}) + + +def test_min_max_date(): + schema = {'date': {'min': date(1900, 1, 1), 'max': date(1999, 12, 31)}} + assert_success({'date': date(1945, 5, 8)}, schema) + assert_fail({'date': date(1871, 5, 10)}, schema) + + +def test_dict_length(): + schema = {'dict': {'minlength': 1}} + assert_fail({'dict': {}}, schema) + assert_success({'dict': {'foo': 'bar'}}, schema) + + +def test_forbidden(): + schema = {'user': {'forbidden': ['root', 'admin']}} + assert_fail({'user': 'admin'}, schema) + assert_success({'user': 'alice'}, schema) + + +def test_mapping_with_sequence_schema(): + schema = {'list': {'schema': {'allowed': ['a', 'b', 'c']}}} + document = {'list': {'is_a': 'mapping'}} + assert_fail(document, schema, + error=('list', ('list', 'schema'), errors.BAD_TYPE_FOR_SCHEMA, + schema['list']['schema'])) + + +def test_sequence_with_mapping_schema(): + schema = {'list': {'schema': {'foo': {'allowed': ['a', 'b', 'c']}}, + 'type': 'dict'}} + document = {'list': ['a', 'b', 'c']} + assert_fail(document, schema) + + +def test_type_error_aborts_validation(): + schema = {'foo': {'type': 'string', 'allowed': ['a']}} + document = {'foo': 0} + assert_fail(document, schema, + error=('foo', ('foo', 'type'), errors.BAD_TYPE, 'string')) + + +def test_dependencies_in_oneof(): + # https://github.com/pyeve/cerberus/issues/241 + schema = {'a': {'type': 'integer', + 'oneof': [ + {'allowed': [1], 'dependencies': 'b'}, + {'allowed': [2], 'dependencies': 'c'} + ]}, + 'b': {}, + 'c': {}} + assert_success({'a': 1, 'b': 'foo'}, schema) + assert_success({'a': 2, 'c': 'bar'}, schema) + assert_fail({'a': 1, 'c': 'foo'}, schema) + assert_fail({'a': 2, 'b': 'bar'}, schema) + + +def test_allow_unknown_with_oneof_rules(validator): + # https://github.com/pyeve/cerberus/issues/251 + schema = { + 'test': { + 'oneof': [ + { + 'type': 'dict', + 'allow_unknown': True, + 'schema': {'known': {'type': 'string'}} + }, + { + 'type': 'dict', + 'schema': {'known': {'type': 'string'}} + }, + ] + } + } + # check regression and that allow unknown does not cause any different + # than expected behaviour for one-of. + document = {'test': {'known': 's'}} + validator(document, schema) + _errors = validator._errors + assert len(_errors) == 1 + assert_has_error(_errors, 'test', ('test', 'oneof'), + errors.ONEOF, schema['test']['oneof']) + assert len(_errors[0].child_errors) == 0 + # check that allow_unknown is actually applied + document = {'test': {'known': 's', 'unknown': 'asd'}} + assert_success(document, validator=validator) diff --git a/pipenv/vendor/cerberus/utils.py b/pipenv/vendor/cerberus/utils.py new file mode 100644 index 0000000000..f10d39761b --- /dev/null +++ b/pipenv/vendor/cerberus/utils.py @@ -0,0 +1,119 @@ +from __future__ import absolute_import + +from collections import Mapping, namedtuple, Sequence + +from cerberus.platform import _int_types, _str_type + + +TypeDefinition = namedtuple('TypeDefinition', + 'name,included_types,excluded_types') +""" +This class is used to define types that can be used as value in the +:attr:`~cerberus.Validator.types_mapping` property. +The ``name`` should be descriptive and match the key it is going to be assigned +to. +A value that is validated against such definition must be an instance of any of +the types contained in ``included_types`` and must not match any of the types +contained in ``excluded_types``. +""" + + +def compare_paths_lt(x, y): + for i in range(min(len(x), len(y))): + if isinstance(x[i], type(y[i])): + if x[i] != y[i]: + return x[i] < y[i] + elif isinstance(x[i], _int_types): + return True + elif isinstance(y[i], _int_types): + return False + return len(x) < len(y) + + +def drop_item_from_tuple(t, i): + return t[:i] + t[i + 1:] + + +def get_Validator_class(): + global Validator + if 'Validator' not in globals(): + from cerberus.validator import Validator + return Validator + + +def mapping_hash(schema): + return hash(mapping_to_frozenset(schema)) + + +def mapping_to_frozenset(mapping): + """ Be aware that this treats any sequence type with the equal members as + equal. As it is used to identify equality of schemas, this can be + considered okay as definitions are semantically equal regardless the + container type. """ + mapping = mapping.copy() + for key, value in mapping.items(): + if isinstance(value, Mapping): + mapping[key] = mapping_to_frozenset(value) + elif isinstance(value, Sequence): + value = list(value) + for i, item in enumerate(value): + if isinstance(item, Mapping): + value[i] = mapping_to_frozenset(item) + mapping[key] = tuple(value) + return frozenset(mapping.items()) + + +def isclass(obj): + try: + issubclass(obj, object) + except TypeError: + return False + else: + return True + + +def quote_string(value): + if isinstance(value, _str_type): + return '"%s"' % value + else: + return value + + +class readonly_classproperty(property): + def __get__(self, instance, owner): + return super(readonly_classproperty, self).__get__(owner) + + def __set__(self, instance, value): + raise RuntimeError('This is a readonly class property.') + + def __delete__(self, instance): + raise RuntimeError('This is a readonly class property.') + + +def validator_factory(name, bases=None, namespace={}): + """ Dynamically create a :class:`~cerberus.Validator` subclass. + Docstrings of mixin-classes will be added to the resulting + class' one if ``__doc__`` is not in :obj:`namespace`. + + :param name: The name of the new class. + :type name: :class:`str` + :param bases: Class(es) with additional and overriding attributes. + :type bases: :class:`tuple` of or a single :term:`class` + :param namespace: Attributes for the new class. + :type namespace: :class:`dict` + :return: The created class. + """ + Validator = get_Validator_class() + + if bases is None: + bases = (Validator,) + elif isinstance(bases, tuple): + bases += (Validator,) + else: + bases = (bases, Validator) + + docstrings = [x.__doc__ for x in bases if x.__doc__] + if len(docstrings) > 1 and '__doc__' not in namespace: + namespace.update({'__doc__': '\n'.join(docstrings)}) + + return type(name, bases, namespace) diff --git a/pipenv/vendor/cerberus/validator.py b/pipenv/vendor/cerberus/validator.py new file mode 100644 index 0000000000..27a2905323 --- /dev/null +++ b/pipenv/vendor/cerberus/validator.py @@ -0,0 +1,1407 @@ +""" + Extensible validation for Python dictionaries. + This module implements Cerberus Validator class + + :copyright: 2012-2016 by Nicola Iarocci. + :license: ISC, see LICENSE for more details. + + Full documentation is available at http://python-cerberus.org +""" + +from __future__ import absolute_import + +from ast import literal_eval +from collections import Hashable, Iterable, Mapping, Sequence +from copy import copy +from datetime import date, datetime +import re +from warnings import warn + +from cerberus import errors +from cerberus.platform import _int_types, _str_type +from cerberus.schema import (schema_registry, rules_set_registry, + DefinitionSchema, SchemaError) +from cerberus.utils import (drop_item_from_tuple, isclass, + readonly_classproperty, TypeDefinition) + + +toy_error_handler = errors.ToyErrorHandler() + + +def dummy_for_rule_validation(rule_constraints): + def dummy(self, constraint, field, value): + raise RuntimeError('Dummy method called. Its purpose is to hold just' + 'validation constraints for a rule in its ' + 'docstring.') + f = dummy + f.__doc__ = rule_constraints + return f + + +class DocumentError(Exception): + """ Raised when the target document is missing or has the wrong format """ + pass + + +class _SchemaRuleTypeError(Exception): + """ Raised when a schema (list) validation encounters a mapping. + Not supposed to be used outside this module. """ + pass + + +class BareValidator(object): + """ Validator class. Normalizes and/or validates any mapping against a + validation-schema which is provided as an argument at class instantiation + or upon calling the :meth:`~cerberus.Validator.validate`, + :meth:`~cerberus.Validator.validated` or + :meth:`~cerberus.Validator.normalized` method. An instance itself is + callable and executes a validation. + + All instantiation parameters are optional. + + There are the introspective properties :attr:`types`, :attr:`validators`, + :attr:`coercers`, :attr:`default_setters`, :attr:`rules`, + :attr:`normalization_rules` and :attr:`validation_rules`. + + The attributes reflecting the available rules are assembled considering + constraints that are defined in the docstrings of rules' methods and is + effectively used as validation schema for :attr:`schema`. + + :param schema: See :attr:`~cerberus.Validator.schema`. + Defaults to :obj:`None`. + :type schema: any :term:`mapping` + :param ignore_none_values: See :attr:`~cerberus.Validator.ignore_none_values`. + Defaults to ``False``. + :type ignore_none_values: :class:`bool` + :param allow_unknown: See :attr:`~cerberus.Validator.allow_unknown`. + Defaults to ``False``. + :type allow_unknown: :class:`bool` or any :term:`mapping` + :param purge_unknown: See :attr:`~cerberus.Validator.purge_unknown`. + Defaults to to ``False``. + :type purge_unknown: :class:`bool` + :param error_handler: The error handler that formats the result of + :attr:`~cerberus.Validator.errors`. + When given as two-value tuple with an error-handler + class and a dictionary, the latter is passed to the + initialization of the error handler. + Default: :class:`~cerberus.errors.BasicErrorHandler`. + :type error_handler: class or instance based on + :class:`~cerberus.errors.BaseErrorHandler` or + :class:`tuple` + """ # noqa: E501 + + mandatory_validations = ('nullable',) + """ Rules that are evaluated on any field, regardless whether defined in + the schema or not. + Type: :class:`tuple` """ + priority_validations = ('nullable', 'readonly', 'type', 'empty') + """ Rules that will be processed in that order before any other. + Type: :class:`tuple` """ + types_mapping = { + 'binary': + TypeDefinition('binary', (bytes, bytearray), ()), + 'boolean': + TypeDefinition('boolean', (bool,), ()), + 'date': + TypeDefinition('date', (date,), ()), + 'datetime': + TypeDefinition('datetime', (datetime,), ()), + 'dict': + TypeDefinition('dict', (Mapping,), ()), + 'float': + TypeDefinition('float', (float, _int_types), ()), + 'integer': + TypeDefinition('integer', (_int_types,), ()), + 'list': + TypeDefinition('list', (Sequence,), (_str_type,)), + 'number': + TypeDefinition('number', (_int_types, float), (bool,)), + 'set': + TypeDefinition('set', (set,), ()), + 'string': + TypeDefinition('string', (_str_type), ()) + } + """ This mapping holds all available constraints for the type rule and + their assigned :class:`~cerberus.TypeDefinition`. """ + _valid_schemas = set() + """ A :class:`set` of hashes derived from validation schemas that are + legit for a particular ``Validator`` class. """ + + def __init__(self, *args, **kwargs): + """ The arguments will be treated as with this signature: + + __init__(self, schema=None, ignore_none_values=False, + allow_unknown=False, purge_unknown=False, + error_handler=errors.BasicErrorHandler) + """ + + self.document = None + """ The document that is or was recently processed. + Type: any :term:`mapping` """ + self._errors = errors.ErrorList() + """ The list of errors that were encountered since the last document + processing was invoked. + Type: :class:`~cerberus.errors.ErrorList` """ + self.recent_error = None + """ The last individual error that was submitted. + Type: :class:`~cerberus.errors.ValidationError` """ + self.document_error_tree = errors.DocumentErrorTree() + """ A tree representiation of encountered errors following the + structure of the document. + Type: :class:`~cerberus.errors.DocumentErrorTree` """ + self.schema_error_tree = errors.SchemaErrorTree() + """ A tree representiation of encountered errors following the + structure of the schema. + Type: :class:`~cerberus.errors.SchemaErrorTree` """ + self.document_path = () + """ The path within the document to the current sub-document. + Type: :class:`tuple` """ + self.schema_path = () + """ The path within the schema to the current sub-schema. + Type: :class:`tuple` """ + self.update = False + self.error_handler = self.__init_error_handler(kwargs) + """ The error handler used to format :attr:`~cerberus.Validator.errors` + and process submitted errors with + :meth:`~cerberus.Validator._error`. + Type: :class:`~cerberus.errors.BaseErrorHandler` """ + self.__store_config(args, kwargs) + self.schema = kwargs.get('schema', None) + self.allow_unknown = kwargs.get('allow_unknown', False) + self._remaining_rules = [] + """ Keeps track of the rules that are next in line to be evaluated + during the validation of a field. + Type: :class:`list` """ + + super(BareValidator, self).__init__() + + @staticmethod + def __init_error_handler(kwargs): + error_handler = kwargs.pop('error_handler', errors.BasicErrorHandler) + if isinstance(error_handler, tuple): + error_handler, eh_config = error_handler + else: + eh_config = {} + if isclass(error_handler) and \ + issubclass(error_handler, errors.BaseErrorHandler): + return error_handler(**eh_config) + elif isinstance(error_handler, errors.BaseErrorHandler): + return error_handler + else: + raise RuntimeError('Invalid error_handler.') + + def __store_config(self, args, kwargs): + """ Assign args to kwargs and store configuration. """ + signature = ('schema', 'ignore_none_values', 'allow_unknown', + 'purge_unknown') + for i, p in enumerate(signature[:len(args)]): + if p in kwargs: + raise TypeError("__init__ got multiple values for argument " + "'%s'" % p) + else: + kwargs[p] = args[i] + self._config = kwargs + """ This dictionary holds the configuration arguments that were used to + initialize the :class:`Validator` instance except the + ``error_handler``. """ + + @classmethod + def clear_caches(cls): + """ Purge the cache of known valid schemas. """ + cls._valid_schemas.clear() + + def _error(self, *args): + """ Creates and adds one or multiple errors. + + :param args: Accepts different argument's signatures. + + *1. Bulk addition of errors:* + + - :term:`iterable` of + :class:`~cerberus.errors.ValidationError`-instances + + The errors will be added to + :attr:`~cerberus.Validator._errors`. + + *2. Custom error:* + + - the invalid field's name + + - the error message + + A custom error containing the message will be created and + added to :attr:`~cerberus.Validator._errors`. + There will however be fewer information contained in the + error (no reference to the violated rule and its + constraint). + + *3. Defined error:* + + - the invalid field's name + + - the error-reference, see :mod:`cerberus.errors` + + - arbitrary, supplemental information about the error + + A :class:`~cerberus.errors.ValidationError` instance will + be created and added to + :attr:`~cerberus.Validator._errors`. + """ + if len(args) == 1: + self._errors.extend(args[0]) + self._errors.sort() + for error in args[0]: + self.document_error_tree += error + self.schema_error_tree += error + self.error_handler.emit(error) + elif len(args) == 2 and isinstance(args[1], _str_type): + self._error(args[0], errors.CUSTOM, args[1]) + elif len(args) >= 2: + field = args[0] + code = args[1].code + rule = args[1].rule + info = args[2:] + + document_path = self.document_path + (field, ) + + schema_path = self.schema_path + if code != errors.UNKNOWN_FIELD.code and rule is not None: + schema_path += (field, rule) + + if not rule: + constraint = None + else: + field_definitions = self._resolve_rules_set(self.schema[field]) + if rule == 'nullable': + constraint = field_definitions.get(rule, False) + else: + constraint = field_definitions[rule] + + value = self.document.get(field) + + self.recent_error = errors.ValidationError( + document_path, schema_path, code, rule, constraint, value, info + ) + self._error([self.recent_error]) + + def _get_child_validator(self, document_crumb=None, schema_crumb=None, + **kwargs): + """ Creates a new instance of Validator-(sub-)class. All initial + parameters of the parent are passed to the initialization, unless + a parameter is given as an explicit *keyword*-parameter. + + :param document_crumb: Extends the + :attr:`~cerberus.Validator.document_path` + of the child-validator. + :type document_crumb: :class:`tuple` or :term:`hashable` + :param schema_crumb: Extends the + :attr:`~cerberus.Validator.schema_path` + of the child-validator. + :type schema_crumb: :class:`tuple` or hashable + :param kwargs: Overriding keyword-arguments for initialization. + :type kwargs: :class:`dict` + + :return: an instance of ``self.__class__`` + """ + child_config = self._config.copy() + child_config.update(kwargs) + if not self.is_child: + child_config['is_child'] = True + child_config['error_handler'] = toy_error_handler + child_config['root_allow_unknown'] = self.allow_unknown + child_config['root_document'] = self.document + child_config['root_schema'] = self.schema + + child_validator = self.__class__(**child_config) + + if document_crumb is None: + child_validator.document_path = self.document_path + else: + if not isinstance(document_crumb, tuple): + document_crumb = (document_crumb, ) + child_validator.document_path = self.document_path + document_crumb + + if schema_crumb is None: + child_validator.schema_path = self.schema_path + else: + if not isinstance(schema_crumb, tuple): + schema_crumb = (schema_crumb, ) + child_validator.schema_path = self.schema_path + schema_crumb + + return child_validator + + def __get_rule_handler(self, domain, rule): + methodname = '_{0}_{1}'.format(domain, rule.replace(' ', '_')) + result = getattr(self, methodname, None) + if result is None: + raise RuntimeError("There's no handler for '{}' in the '{}' " + "domain.".format(rule, domain)) + return result + + def _drop_nodes_from_errorpaths(self, _errors, dp_items, sp_items): + """ Removes nodes by index from an errorpath, relatively to the + basepaths of self. + + :param errors: A list of :class:`errors.ValidationError` instances. + :param dp_items: A list of integers, pointing at the nodes to drop from + the :attr:`document_path`. + :param sp_items: Alike ``dp_items``, but for :attr:`schema_path`. + """ + dp_basedepth = len(self.document_path) + sp_basedepth = len(self.schema_path) + for error in _errors: + for i in sorted(dp_items, reverse=True): + error.document_path = \ + drop_item_from_tuple(error.document_path, dp_basedepth + i) + for i in sorted(sp_items, reverse=True): + error.schema_path = \ + drop_item_from_tuple(error.schema_path, sp_basedepth + i) + if error.child_errors: + self._drop_nodes_from_errorpaths(error.child_errors, + dp_items, sp_items) + + def _lookup_field(self, path): + """ Searches for a field as defined by path. This method is used by the + ``dependency`` evaluation logic. + + :param path: Path elements are separated by a ``.``. A leading ``^`` + indicates that the path relates to the document root, + otherwise it relates to the currently evaluated document, + which is possibly a subdocument. + The sequence ``^^`` at the start will be interpreted as a + literal ``^``. + :type path: :class:`str` + :returns: Either the found field name and its value or :obj:`None` for + both. + :rtype: A two-value :class:`tuple`. + """ + if path.startswith('^'): + path = path[1:] + context = self.document if path.startswith('^') \ + else self.root_document + else: + context = self.document + + parts = path.split('.') + for part in parts: + if part not in context: + return None, None + context = context.get(part) + + return parts[-1], context + + def _resolve_rules_set(self, rules_set): + if isinstance(rules_set, Mapping): + return rules_set + elif isinstance(rules_set, _str_type): + return self.rules_set_registry.get(rules_set) + return None + + def _resolve_schema(self, schema): + if isinstance(schema, Mapping): + return schema + elif isinstance(schema, _str_type): + return self.schema_registry.get(schema) + return None + + # Properties + + @property + def allow_unknown(self): + """ If ``True`` unknown fields that are not defined in the schema will + be ignored. If a mapping with a validation schema is given, any + undefined field will be validated against its rules. + Also see :ref:`allowing-the-unknown`. + Type: :class:`bool` or any :term:`mapping` """ + return self._config.get('allow_unknown', False) + + @allow_unknown.setter + def allow_unknown(self, value): + if not (self.is_child or isinstance(value, (bool, DefinitionSchema))): + DefinitionSchema(self, {'allow_unknown': value}) + self._config['allow_unknown'] = value + + @property + def errors(self): + """ The errors of the last processing formatted by the handler that is + bound to :attr:`~cerberus.Validator.error_handler`. """ + return self.error_handler(self._errors) + + @property + def ignore_none_values(self): + """ Whether to not process :obj:`None`-values in a document or not. + Type: :class:`bool` """ + return self._config.get('ignore_none_values', False) + + @ignore_none_values.setter + def ignore_none_values(self, value): + self._config['ignore_none_values'] = value + + @property + def is_child(self): + """ ``True`` for child-validators obtained with + :meth:`~cerberus.Validator._get_child_validator`. + Type: :class:`bool` """ + return self._config.get('is_child', False) + + @property + def _is_normalized(self): + """ ``True`` if the document is already normalized. """ + return self._config.get('_is_normalized', False) + + @_is_normalized.setter + def _is_normalized(self, value): + self._config['_is_normalized'] = value + + @property + def purge_unknown(self): + """ If ``True`` unknown fields will be deleted from the document + unless a validation is called with disabled normalization. + Also see :ref:`purging-unknown-fields`. Type: :class:`bool` """ + return self._config.get('purge_unknown', False) + + @purge_unknown.setter + def purge_unknown(self, value): + self._config['purge_unknown'] = value + + @property + def root_allow_unknown(self): + """ The :attr:`~cerberus.Validator.allow_unknown` attribute of the + first level ancestor of a child validator. """ + return self._config.get('root_allow_unknown', self.allow_unknown) + + @property + def root_document(self): + """ The :attr:`~cerberus.Validator.document` attribute of the + first level ancestor of a child validator. """ + return self._config.get('root_document', self.document) + + @property + def rules_set_registry(self): + """ The registry that holds referenced rules sets. + Type: :class:`~cerberus.Registry` """ + return self._config.get('rules_set_registry', rules_set_registry) + + @rules_set_registry.setter + def rules_set_registry(self, registry): + self._config['rules_set_registry'] = registry + + @property + def root_schema(self): + """ The :attr:`~cerberus.Validator.schema` attribute of the + first level ancestor of a child validator. """ + return self._config.get('root_schema', self.schema) + + @property + def schema(self): + """ The validation schema of a validator. When a schema is passed to + a method, it replaces this attribute. + Type: any :term:`mapping` or :obj:`None` """ + return self._schema + + @schema.setter + def schema(self, schema): + if schema is None: + self._schema = None + elif self.is_child or isinstance(schema, DefinitionSchema): + self._schema = schema + else: + self._schema = DefinitionSchema(self, schema) + + @property + def schema_registry(self): + """ The registry that holds referenced schemas. + Type: :class:`~cerberus.Registry` """ + return self._config.get('schema_registry', schema_registry) + + @schema_registry.setter + def schema_registry(self, registry): + self._config['schema_registry'] = registry + + # FIXME the returned method has the correct docstring, but doesn't appear + # in the API docs + @readonly_classproperty + def types(cls): + """ The constraints that can be used for the 'type' rule. + Type: A tuple of strings. """ + redundant_types = \ + set(cls.types_mapping) & set(cls._types_from_methods) + if redundant_types: + warn("These types are defined both with a method and in the" + "'types_mapping' property of this validator: %s" + % redundant_types) + + return tuple(cls.types_mapping) + cls._types_from_methods + + # Document processing + + def __init_processing(self, document, schema=None): + self._errors = errors.ErrorList() + self.recent_error = None + self.document_error_tree = errors.DocumentErrorTree() + self.schema_error_tree = errors.SchemaErrorTree() + self.document = copy(document) + if not self.is_child: + self._is_normalized = False + + if schema is not None: + self.schema = DefinitionSchema(self, schema) + elif self.schema is None: + if isinstance(self.allow_unknown, Mapping): + self._schema = {} + else: + raise SchemaError(errors.SCHEMA_ERROR_MISSING) + if document is None: + raise DocumentError(errors.DOCUMENT_MISSING) + if not isinstance(document, Mapping): + raise DocumentError( + errors.DOCUMENT_FORMAT.format(document)) + self.error_handler.start(self) + + def _drop_remaining_rules(self, *rules): + """ Drops rules from the queue of the rules that still need to be + evaluated for the currently processed field. + If no arguments are given, the whole queue is emptied. + """ + if rules: + for rule in rules: + try: + self._remaining_rules.remove(rule) + except ValueError: + pass + else: + self._remaining_rules = [] + + # # Normalizing + + def normalized(self, document, schema=None, always_return_document=False): + """ Returns the document normalized according to the specified rules + of a schema. + + :param document: The document to normalize. + :type document: any :term:`mapping` + :param schema: The validation schema. Defaults to :obj:`None`. If not + provided here, the schema must have been provided at + class instantiation. + :type schema: any :term:`mapping` + :param always_return_document: Return the document, even if an error + occurred. Defaults to: ``False``. + :type always_return_document: :class:`bool` + :return: A normalized copy of the provided mapping or :obj:`None` if an + error occurred during normalization. + """ + self.__init_processing(document, schema) + self.__normalize_mapping(self.document, self.schema) + self.error_handler.end(self) + if self._errors and not always_return_document: + return None + else: + return self.document + + def __normalize_mapping(self, mapping, schema): + if isinstance(schema, _str_type): + schema = self._resolve_schema(schema) + schema = schema.copy() + for field in schema: + schema[field] = self._resolve_rules_set(schema[field]) + + self.__normalize_rename_fields(mapping, schema) + if self.purge_unknown and not self.allow_unknown: + self._normalize_purge_unknown(mapping, schema) + # Check `readonly` fields before applying default values because + # a field's schema definition might contain both `readonly` and + # `default`. + self.__validate_readonly_fields(mapping, schema) + self.__normalize_default_fields(mapping, schema) + self._normalize_coerce(mapping, schema) + self.__normalize_containers(mapping, schema) + self._is_normalized = True + return mapping + + def _normalize_coerce(self, mapping, schema): + """ {'oneof': [ + {'type': 'callable'}, + {'type': 'list', + 'schema': {'oneof': [{'type': 'callable'}, + {'type': 'string'}]}}, + {'type': 'string'} + ]} """ + + error = errors.COERCION_FAILED + for field in mapping: + if field in schema and 'coerce' in schema[field]: + mapping[field] = self.__normalize_coerce( + schema[field]['coerce'], field, mapping[field], + schema[field].get('nullable', False), error) + elif isinstance(self.allow_unknown, Mapping) and \ + 'coerce' in self.allow_unknown: + mapping[field] = self.__normalize_coerce( + self.allow_unknown['coerce'], field, mapping[field], + self.allow_unknown.get('nullable', False), error) + + def __normalize_coerce(self, processor, field, value, nullable, error): + if isinstance(processor, _str_type): + processor = self.__get_rule_handler('normalize_coerce', processor) + + elif isinstance(processor, Iterable): + result = value + for p in processor: + result = self.__normalize_coerce(p, field, result, + nullable, error) + if errors.COERCION_FAILED in \ + self.document_error_tree.fetch_errors_from( + self.document_path + (field,)): + break + return result + + try: + return processor(value) + except Exception as e: + if not nullable and e is not TypeError: + self._error(field, error, str(e)) + return value + + def __normalize_containers(self, mapping, schema): + for field in mapping: + if field not in schema: + continue + # TODO: This check conflates validation and normalization + if isinstance(mapping[field], Mapping): + if 'keyschema' in schema[field]: + self.__normalize_mapping_per_keyschema( + field, mapping, schema[field]['keyschema']) + if 'valueschema' in schema[field]: + self.__normalize_mapping_per_valueschema( + field, mapping, schema[field]['valueschema']) + if set(schema[field]) & set(('allow_unknown', 'purge_unknown', + 'schema')): + try: + self.__normalize_mapping_per_schema( + field, mapping, schema) + except _SchemaRuleTypeError: + pass + elif isinstance(mapping[field], _str_type): + continue + elif isinstance(mapping[field], Sequence) and \ + 'schema' in schema[field]: + self.__normalize_sequence(field, mapping, schema) + + def __normalize_mapping_per_keyschema(self, field, mapping, property_rules): + schema = dict(((k, property_rules) for k in mapping[field])) + document = dict(((k, k) for k in mapping[field])) + validator = self._get_child_validator( + document_crumb=field, schema_crumb=(field, 'keyschema'), + schema=schema) + result = validator.normalized(document, always_return_document=True) + if validator._errors: + self._drop_nodes_from_errorpaths(validator._errors, [], [2, 4]) + self._error(validator._errors) + for k in result: + if k == result[k]: + continue + if result[k] in mapping[field]: + warn("Normalizing keys of {path}: {key} already exists, " + "its value is replaced." + .format(path='.'.join(self.document_path + (field,)), + key=k)) + mapping[field][result[k]] = mapping[field][k] + else: + mapping[field][result[k]] = mapping[field][k] + del mapping[field][k] + + def __normalize_mapping_per_valueschema(self, field, mapping, value_rules): + schema = dict(((k, value_rules) for k in mapping[field])) + validator = self._get_child_validator( + document_crumb=field, schema_crumb=(field, 'valueschema'), + schema=schema) + mapping[field] = validator.normalized(mapping[field], + always_return_document=True) + if validator._errors: + self._drop_nodes_from_errorpaths(validator._errors, [], [2]) + self._error(validator._errors) + + def __normalize_mapping_per_schema(self, field, mapping, schema): + validator = self._get_child_validator( + document_crumb=field, schema_crumb=(field, 'schema'), + schema=schema[field].get('schema', {}), + allow_unknown=schema[field].get('allow_unknown', self.allow_unknown), # noqa: E501 + purge_unknown=schema[field].get('purge_unknown', self.purge_unknown)) # noqa: E501 + value_type = type(mapping[field]) + result_value = validator.normalized(mapping[field], + always_return_document=True) + mapping[field] = value_type(result_value) + if validator._errors: + self._error(validator._errors) + + def __normalize_sequence(self, field, mapping, schema): + schema = dict(((k, schema[field]['schema']) + for k in range(len(mapping[field])))) + document = dict((k, v) for k, v in enumerate(mapping[field])) + validator = self._get_child_validator( + document_crumb=field, schema_crumb=(field, 'schema'), + schema=schema) + value_type = type(mapping[field]) + result = validator.normalized(document, always_return_document=True) + mapping[field] = value_type(result.values()) + if validator._errors: + self._drop_nodes_from_errorpaths(validator._errors, [], [2]) + self._error(validator._errors) + + @staticmethod + def _normalize_purge_unknown(mapping, schema): + """ {'type': 'boolean'} """ + for field in tuple(mapping): + if field not in schema: + del mapping[field] + return mapping + + def __normalize_rename_fields(self, mapping, schema): + for field in tuple(mapping): + if field in schema: + self._normalize_rename(mapping, schema, field) + self._normalize_rename_handler(mapping, schema, field) + elif isinstance(self.allow_unknown, Mapping) and \ + 'rename_handler' in self.allow_unknown: + self._normalize_rename_handler( + mapping, {field: self.allow_unknown}, field) + return mapping + + def _normalize_rename(self, mapping, schema, field): + """ {'type': 'hashable'} """ + if 'rename' in schema[field]: + mapping[schema[field]['rename']] = mapping[field] + del mapping[field] + + def _normalize_rename_handler(self, mapping, schema, field): + """ {'oneof': [ + {'type': 'callable'}, + {'type': 'list', + 'schema': {'oneof': [{'type': 'callable'}, + {'type': 'string'}]}}, + {'type': 'string'} + ]} """ + if 'rename_handler' not in schema[field]: + return + new_name = self.__normalize_coerce( + schema[field]['rename_handler'], field, field, + False, errors.RENAMING_FAILED) + if new_name != field: + mapping[new_name] = mapping[field] + del mapping[field] + + def __validate_readonly_fields(self, mapping, schema): + for field in (x for x in schema if x in mapping and + self._resolve_rules_set(schema[x]).get('readonly')): + self._validate_readonly(schema[field]['readonly'], field, + mapping[field]) + + def __normalize_default_fields(self, mapping, schema): + fields = [x for x in schema if x not in mapping or + mapping[x] is None and not schema[x].get('nullable', False)] + try: + fields_with_default = [x for x in fields if 'default' in schema[x]] + except TypeError: + raise _SchemaRuleTypeError + for field in fields_with_default: + self._normalize_default(mapping, schema, field) + + known_fields_states = set() + fields = [x for x in fields if 'default_setter' in schema[x]] + while fields: + field = fields.pop(0) + try: + self._normalize_default_setter(mapping, schema, field) + except KeyError: + fields.append(field) + except Exception as e: + self._error(field, errors.SETTING_DEFAULT_FAILED, str(e)) + + fields_state = tuple(fields) + if fields_state in known_fields_states: + for field in fields: + self._error(field, errors.SETTING_DEFAULT_FAILED, + 'Circular dependencies of default setters.') + break + else: + known_fields_states.add(fields_state) + + def _normalize_default(self, mapping, schema, field): + """ {'nullable': True} """ + mapping[field] = schema[field]['default'] + + def _normalize_default_setter(self, mapping, schema, field): + """ {'oneof': [ + {'type': 'callable'}, + {'type': 'string'} + ]} """ + if 'default_setter' in schema[field]: + setter = schema[field]['default_setter'] + if isinstance(setter, _str_type): + setter = self.__get_rule_handler('normalize_default_setter', + setter) + mapping[field] = setter(mapping) + + # # Validating + + def validate(self, document, schema=None, update=False, normalize=True): + """ Normalizes and validates a mapping against a validation-schema of + defined rules. + + :param document: The document to normalize. + :type document: any :term:`mapping` + :param schema: The validation schema. Defaults to :obj:`None`. If not + provided here, the schema must have been provided at + class instantiation. + :type schema: any :term:`mapping` + :param update: If ``True``, required fields won't be checked. + :type update: :class:`bool` + :param normalize: If ``True``, normalize the document before validation. + :type normalize: :class:`bool` + + :return: ``True`` if validation succeeds, otherwise ``False``. Check + the :func:`errors` property for a list of processing errors. + :rtype: :class:`bool` + """ + self.update = update + self._unrequired_by_excludes = set() + + self.__init_processing(document, schema) + if normalize: + self.__normalize_mapping(self.document, self.schema) + + for field in self.document: + if self.ignore_none_values and self.document[field] is None: + continue + definitions = self.schema.get(field) + if definitions is not None: + self.__validate_definitions(definitions, field) + else: + self.__validate_unknown_fields(field) + + if not self.update: + self.__validate_required_fields(self.document) + + self.error_handler.end(self) + + return not bool(self._errors) + + __call__ = validate + + def validated(self, *args, **kwargs): + """ Wrapper around :meth:`~cerberus.Validator.validate` that returns + the normalized and validated document or :obj:`None` if validation + failed. """ + always_return_document = kwargs.pop('always_return_document', False) + self.validate(*args, **kwargs) + if self._errors and not always_return_document: + return None + else: + return self.document + + def __validate_unknown_fields(self, field): + if self.allow_unknown: + value = self.document[field] + if isinstance(self.allow_unknown, (Mapping, _str_type)): + # validate that unknown fields matches the schema + # for unknown_fields + schema_crumb = 'allow_unknown' if self.is_child \ + else '__allow_unknown__' + validator = self._get_child_validator( + schema_crumb=schema_crumb, + schema={field: self.allow_unknown}) + if not validator({field: value}, normalize=False): + self._error(validator._errors) + else: + self._error(field, errors.UNKNOWN_FIELD) + + def __validate_definitions(self, definitions, field): + """ Validate a field's value against its defined rules. """ + + def validate_rule(rule): + validator = self.__get_rule_handler('validate', rule) + return validator(definitions.get(rule, None), field, value) + + definitions = self._resolve_rules_set(definitions) + value = self.document[field] + + rules_queue = [x for x in self.priority_validations + if x in definitions or x in self.mandatory_validations] + rules_queue.extend(x for x in self.mandatory_validations + if x not in rules_queue) + rules_queue.extend(x for x in definitions + if x not in rules_queue and + x not in self.normalization_rules and + x not in ('allow_unknown', 'required')) + self._remaining_rules = rules_queue + + while self._remaining_rules: + rule = self._remaining_rules.pop(0) + try: + result = validate_rule(rule) + # TODO remove on next breaking release + if result: + break + except _SchemaRuleTypeError: + break + + self._drop_remaining_rules() + + # Remember to keep the validation methods below this line + # sorted alphabetically + + _validate_allow_unknown = dummy_for_rule_validation( + """ {'oneof': [{'type': 'boolean'}, + {'type': ['dict', 'string'], + 'validator': 'bulk_schema'}]} """) + + def _validate_allowed(self, allowed_values, field, value): + """ {'type': 'list'} """ + if isinstance(value, Iterable) and not isinstance(value, _str_type): + unallowed = set(value) - set(allowed_values) + if unallowed: + self._error(field, errors.UNALLOWED_VALUES, list(unallowed)) + else: + if value not in allowed_values: + self._error(field, errors.UNALLOWED_VALUE, value) + + def _validate_dependencies(self, dependencies, field, value): + """ {'type': ('dict', 'hashable', 'list'), + 'validator': 'dependencies'} """ + if isinstance(dependencies, _str_type): + dependencies = (dependencies,) + + if isinstance(dependencies, Sequence): + self.__validate_dependencies_sequence(dependencies, field) + elif isinstance(dependencies, Mapping): + self.__validate_dependencies_mapping(dependencies, field) + + if self.document_error_tree.fetch_node_from( + self.schema_path + (field, 'dependencies')) is not None: + return True + + def __validate_dependencies_mapping(self, dependencies, field): + validated_dependencies_counter = 0 + error_info = {} + for dependency_name, dependency_values in dependencies.items(): + if (not isinstance(dependency_values, Sequence) or + isinstance(dependency_values, _str_type)): + dependency_values = [dependency_values] + + wanted_field, wanted_field_value = \ + self._lookup_field(dependency_name) + if wanted_field_value in dependency_values: + validated_dependencies_counter += 1 + else: + error_info.update({dependency_name: wanted_field_value}) + + if validated_dependencies_counter != len(dependencies): + self._error(field, errors.DEPENDENCIES_FIELD_VALUE, error_info) + + def __validate_dependencies_sequence(self, dependencies, field): + for dependency in dependencies: + if self._lookup_field(dependency)[0] is None: + self._error(field, errors.DEPENDENCIES_FIELD, dependency) + + def _validate_empty(self, empty, field, value): + """ {'type': 'boolean'} """ + if isinstance(value, Iterable) and len(value) == 0: + self._drop_remaining_rules( + 'allowed', 'forbidden', 'items', 'minlength', 'maxlength', + 'regex', 'validator') + if not empty: + self._error(field, errors.EMPTY_NOT_ALLOWED) + + def _validate_excludes(self, excludes, field, value): + """ {'type': ('hashable', 'list'), + 'schema': {'type': 'hashable'}} """ + if isinstance(excludes, Hashable): + excludes = [excludes] + + # Save required field to be checked latter + if 'required' in self.schema[field] and self.schema[field]['required']: + self._unrequired_by_excludes.add(field) + for exclude in excludes: + if (exclude in self.schema and + 'required' in self.schema[exclude] and + self.schema[exclude]['required']): + + self._unrequired_by_excludes.add(exclude) + + if [True for key in excludes if key in self.document]: + # Wrap each field in `excludes` list between quotes + exclusion_str = ', '.join("'{0}'" + .format(word) for word in excludes) + self._error(field, errors.EXCLUDES_FIELD, exclusion_str) + + def _validate_forbidden(self, forbidden_values, field, value): + """ {'type': 'list'} """ + if isinstance(value, _str_type): + if value in forbidden_values: + self._error(field, errors.FORBIDDEN_VALUE, value) + elif isinstance(value, Sequence): + forbidden = set(value) & set(forbidden_values) + if forbidden: + self._error(field, errors.FORBIDDEN_VALUES, list(forbidden)) + elif isinstance(value, int): + if value in forbidden_values: + self._error(field, errors.FORBIDDEN_VALUE, value) + + def _validate_items(self, items, field, values): + """ {'type': 'list', 'validator': 'items'} """ + if len(items) != len(values): + self._error(field, errors.ITEMS_LENGTH, len(items), len(values)) + else: + schema = dict((i, definition) for i, definition in enumerate(items)) # noqa: E501 + validator = self._get_child_validator(document_crumb=field, + schema_crumb=(field, 'items'), # noqa: E501 + schema=schema) + if not validator(dict((i, value) for i, value in enumerate(values)), + update=self.update, normalize=False): + self._error(field, errors.BAD_ITEMS, validator._errors) + + def __validate_logical(self, operator, definitions, field, value): + """ Validates value against all definitions and logs errors according + to the operator. """ + valid_counter = 0 + _errors = errors.ErrorList() + + for i, definition in enumerate(definitions): + schema = {field: definition.copy()} + for rule in ('allow_unknown', 'type'): + if rule not in schema[field] and rule in self.schema[field]: + schema[field][rule] = self.schema[field][rule] + if 'allow_unknown' not in schema[field]: + schema[field]['allow_unknown'] = self.allow_unknown + + validator = self._get_child_validator( + schema_crumb=(field, operator, i), + schema=schema, allow_unknown=True) + if validator(self.document, update=self.update, normalize=False): + valid_counter += 1 + else: + self._drop_nodes_from_errorpaths(validator._errors, [], [3]) + _errors.extend(validator._errors) + + return valid_counter, _errors + + def _validate_anyof(self, definitions, field, value): + """ {'type': 'list', 'logical': 'anyof'} """ + valids, _errors = \ + self.__validate_logical('anyof', definitions, field, value) + if valids < 1: + self._error(field, errors.ANYOF, _errors, + valids, len(definitions)) + + def _validate_allof(self, definitions, field, value): + """ {'type': 'list', 'logical': 'allof'} """ + valids, _errors = \ + self.__validate_logical('allof', definitions, field, value) + if valids < len(definitions): + self._error(field, errors.ALLOF, _errors, + valids, len(definitions)) + + def _validate_noneof(self, definitions, field, value): + """ {'type': 'list', 'logical': 'noneof'} """ + valids, _errors = \ + self.__validate_logical('noneof', definitions, field, value) + if valids > 0: + self._error(field, errors.NONEOF, _errors, + valids, len(definitions)) + + def _validate_oneof(self, definitions, field, value): + """ {'type': 'list', 'logical': 'oneof'} """ + valids, _errors = \ + self.__validate_logical('oneof', definitions, field, value) + if valids != 1: + self._error(field, errors.ONEOF, _errors, + valids, len(definitions)) + + def _validate_max(self, max_value, field, value): + """ {'nullable': False } """ + try: + if value > max_value: + self._error(field, errors.MAX_VALUE) + except TypeError: + pass + + def _validate_min(self, min_value, field, value): + """ {'nullable': False } """ + try: + if value < min_value: + self._error(field, errors.MIN_VALUE) + except TypeError: + pass + + def _validate_maxlength(self, max_length, field, value): + """ {'type': 'integer'} """ + if isinstance(value, Iterable) and len(value) > max_length: + self._error(field, errors.MAX_LENGTH, len(value)) + + def _validate_minlength(self, min_length, field, value): + """ {'type': 'integer'} """ + if isinstance(value, Iterable) and len(value) < min_length: + self._error(field, errors.MIN_LENGTH, len(value)) + + def _validate_nullable(self, nullable, field, value): + """ {'type': 'boolean'} """ + if value is None: + if not nullable: + self._error(field, errors.NOT_NULLABLE) + self._drop_remaining_rules( + 'empty', 'forbidden', 'items', 'keyschema', 'min', 'max', + 'minlength', 'maxlength', 'regex', 'schema', 'type', + 'valueschema') + + def _validate_keyschema(self, schema, field, value): + """ {'type': ['dict', 'string'], 'validator': 'bulk_schema', + 'forbidden': ['rename', 'rename_handler']} """ + if isinstance(value, Mapping): + validator = self._get_child_validator( + document_crumb=field, + schema_crumb=(field, 'keyschema'), + schema=dict(((k, schema) for k in value.keys()))) + if not validator(dict(((k, k) for k in value.keys())), + normalize=False): + self._drop_nodes_from_errorpaths(validator._errors, + [], [2, 4]) + self._error(field, errors.KEYSCHEMA, validator._errors) + + def _validate_readonly(self, readonly, field, value): + """ {'type': 'boolean'} """ + if readonly: + if not self._is_normalized: + self._error(field, errors.READONLY_FIELD) + # If the document was normalized (and therefore already been + # checked for readonly fields), we still have to return True + # if an error was filed. + has_error = errors.READONLY_FIELD in \ + self.document_error_tree.fetch_errors_from( + self.document_path + (field,)) + if self._is_normalized and has_error: + self._drop_remaining_rules() + + def _validate_regex(self, pattern, field, value): + """ {'type': 'string'} """ + if not isinstance(value, _str_type): + return + if not pattern.endswith('$'): + pattern += '$' + re_obj = re.compile(pattern) + if not re_obj.match(value): + self._error(field, errors.REGEX_MISMATCH) + + _validate_required = dummy_for_rule_validation(""" {'type': 'boolean'} """) + + def __validate_required_fields(self, document): + """ Validates that required fields are not missing. + + :param document: The document being validated. + """ + try: + required = set(field for field, definition in self.schema.items() + if self._resolve_rules_set(definition). + get('required') is True) + except AttributeError: + if self.is_child and self.schema_path[-1] == 'schema': + raise _SchemaRuleTypeError + else: + raise + required -= self._unrequired_by_excludes + missing = required - set(field for field in document + if document.get(field) is not None or + not self.ignore_none_values) + + for field in missing: + self._error(field, errors.REQUIRED_FIELD) + + # At least on field from self._unrequired_by_excludes should be + # present in document + if self._unrequired_by_excludes: + fields = set(field for field in document + if document.get(field) is not None) + if self._unrequired_by_excludes.isdisjoint(fields): + for field in self._unrequired_by_excludes - fields: + self._error(field, errors.REQUIRED_FIELD) + + def _validate_schema(self, schema, field, value): + """ {'type': ['dict', 'string'], + 'anyof': [{'validator': 'schema'}, + {'validator': 'bulk_schema'}]} """ + if schema is None: + return + + if isinstance(value, Sequence) and not isinstance(value, _str_type): + self.__validate_schema_sequence(field, schema, value) + elif isinstance(value, Mapping): + self.__validate_schema_mapping(field, schema, value) + + def __validate_schema_mapping(self, field, schema, value): + schema = self._resolve_schema(schema) + allow_unknown = self.schema[field].get('allow_unknown', + self.allow_unknown) + validator = self._get_child_validator(document_crumb=field, + schema_crumb=(field, 'schema'), + schema=schema, + allow_unknown=allow_unknown) + try: + if not validator(value, update=self.update, normalize=False): + self._error(field, errors.MAPPING_SCHEMA, validator._errors) + except _SchemaRuleTypeError: + self._error(field, errors.BAD_TYPE_FOR_SCHEMA) + raise + + def __validate_schema_sequence(self, field, schema, value): + schema = dict(((i, schema) for i in range(len(value)))) + validator = self._get_child_validator( + document_crumb=field, schema_crumb=(field, 'schema'), + schema=schema, allow_unknown=self.allow_unknown) + validator(dict(((i, v) for i, v in enumerate(value))), + update=self.update, normalize=False) + + if validator._errors: + self._drop_nodes_from_errorpaths(validator._errors, [], [2]) + self._error(field, errors.SEQUENCE_SCHEMA, validator._errors) + + def _validate_type(self, data_type, field, value): + """ {'type': ['string', 'list'], + 'validator': 'type'} """ + if not data_type: + return + + types = (data_type,) if isinstance(data_type, _str_type) else data_type + + for _type in types: + # TODO remove this block on next major release + # this implementation still supports custom type validation methods + type_definition = self.types_mapping.get(_type) + if type_definition is not None: + matched = isinstance(value, type_definition.included_types) \ + and not isinstance(value, type_definition.excluded_types) + else: + type_handler = self.__get_rule_handler('validate_type', _type) + matched = type_handler(value) + if matched: + return + + # TODO uncomment this block on next major release + # when _validate_type_* methods were deprecated: + # type_definition = self.types_mapping[_type] + # if isinstance(value, type_definition.included_types) \ + # and not isinstance(value, type_definition.excluded_types): # noqa 501 + # return + + self._error(field, errors.BAD_TYPE) + self._drop_remaining_rules() + + def _validate_validator(self, validator, field, value): + """ {'oneof': [ + {'type': 'callable'}, + {'type': 'list', + 'schema': {'oneof': [{'type': 'callable'}, + {'type': 'string'}]}}, + {'type': 'string'} + ]} """ + if isinstance(validator, _str_type): + validator = self.__get_rule_handler('validator', validator) + validator(field, value) + elif isinstance(validator, Iterable): + for v in validator: + self._validate_validator(v, field, value) + else: + validator(field, value, self._error) + + def _validate_valueschema(self, schema, field, value): + """ {'type': ['dict', 'string'], 'validator': 'bulk_schema', + 'forbidden': ['rename', 'rename_handler']} """ + schema_crumb = (field, 'valueschema') + if isinstance(value, Mapping): + validator = self._get_child_validator( + document_crumb=field, schema_crumb=schema_crumb, + schema=dict((k, schema) for k in value)) + validator(value, update=self.update, normalize=False) + if validator._errors: + self._drop_nodes_from_errorpaths(validator._errors, [], [2]) + self._error(field, errors.VALUESCHEMA, validator._errors) + + +RULE_SCHEMA_SEPARATOR = \ + "The rule's arguments are validated against this schema:" + + +class InspectedValidator(type): + """ Metaclass for all validators """ + def __new__(cls, *args): + if '__doc__' not in args[2]: + args[2].update({'__doc__': args[1][0].__doc__}) + return super(InspectedValidator, cls).__new__(cls, *args) + + def __init__(cls, *args): + def attributes_with_prefix(prefix): + return tuple(x.split('_', 2)[-1] for x in dir(cls) + if x.startswith('_' + prefix)) + + super(InspectedValidator, cls).__init__(*args) + + cls._types_from_methods, cls.validation_rules = (), {} + for attribute in attributes_with_prefix('validate'): + # TODO remove inspection of type test methods in next major release + if attribute.startswith('type_'): + cls._types_from_methods += (attribute[len('type_'):],) + else: + cls.validation_rules[attribute] = \ + cls.__get_rule_schema('_validate_' + attribute) + + # TODO remove on next major release + if cls._types_from_methods: + warn("Methods for type testing are deprecated, use TypeDefinition " + "and the 'types_mapping'-property of a Validator-instance " + "instead.", DeprecationWarning) + + cls.validators = tuple(x for x in attributes_with_prefix('validator')) + x = cls.validation_rules['validator']['oneof'] + x[1]['schema']['oneof'][1]['allowed'] = x[2]['allowed'] = cls.validators + + for rule in (x for x in cls.mandatory_validations if x != 'nullable'): + cls.validation_rules[rule]['required'] = True + + cls.coercers, cls.default_setters, cls.normalization_rules = (), (), {} + for attribute in attributes_with_prefix('normalize'): + if attribute.startswith('coerce_'): + cls.coercers += (attribute[len('coerce_'):],) + elif attribute.startswith('default_setter_'): + cls.default_setters += (attribute[len('default_setter_'):],) + else: + cls.normalization_rules[attribute] = \ + cls.__get_rule_schema('_normalize_' + attribute) + + for rule in ('coerce', 'rename_handler'): + x = cls.normalization_rules[rule]['oneof'] + x[1]['schema']['oneof'][1]['allowed'] = \ + x[2]['allowed'] = cls.coercers + cls.normalization_rules['default_setter']['oneof'][1]['allowed'] = \ + cls.default_setters + + cls.rules = {} + cls.rules.update(cls.validation_rules) + cls.rules.update(cls.normalization_rules) + + def __get_rule_schema(cls, method_name): + docstring = getattr(cls, method_name).__doc__ + if docstring is None: + result = {} + else: + if RULE_SCHEMA_SEPARATOR in docstring: + docstring = docstring.split(RULE_SCHEMA_SEPARATOR)[1] + try: + result = literal_eval(docstring.strip()) + except Exception: + result = {} + + if not result: + warn("No validation schema is defined for the arguments of rule " + "'%s'" % method_name.split('_', 2)[-1]) + + return result + + +Validator = InspectedValidator('Validator', (BareValidator,), {}) diff --git a/pipenv/vendor/requirementslib/__init__.py b/pipenv/vendor/requirementslib/__init__.py index ddb17b6e53..1f3a2fcb78 100644 --- a/pipenv/vendor/requirementslib/__init__.py +++ b/pipenv/vendor/requirementslib/__init__.py @@ -1,5 +1,5 @@ # -*- coding=utf-8 -*- -__version__ = '1.1.9.dev0' +__version__ = '1.1.9' from .exceptions import RequirementError diff --git a/pipenv/vendor/requirementslib/models/requirements.py b/pipenv/vendor/requirementslib/models/requirements.py index cc8e683113..248ca77786 100644 --- a/pipenv/vendor/requirementslib/models/requirements.py +++ b/pipenv/vendor/requirementslib/models/requirements.py @@ -96,7 +96,10 @@ def from_pipfile(cls, name, pipfile): @property def line_part(self): - return "{0}".format(canonicalize_name(self.name)) + # FIXME: This should actually be canonicalized but for now we have to + # simply lowercase it and replace underscores, since full canonicalization + # also replaces dots and that doesn't actually work when querying the index + return "{0}".format(self.name.lower().replace("_", "-")) @property def pipfile_part(self): diff --git a/pipenv/vendor/vendor.txt b/pipenv/vendor/vendor.txt index cac0801dba..8d13cdba2b 100644 --- a/pipenv/vendor/vendor.txt +++ b/pipenv/vendor/vendor.txt @@ -27,7 +27,7 @@ requests==2.19.1 idna==2.7 urllib3==1.23 certifi==2018.8.24 -requirementslib==1.1.7 +requirementslib==1.1.9 attrs==18.2.0 distlib==0.2.8 packaging==18.0 @@ -46,4 +46,5 @@ pip-shims==0.3.1 ptyprocess==0.6.0 enum34==1.1.6 yaspin==0.14.0 +cerberus==1.2 git+https://github.com/sarugaku/passa.git@master#egg=passa From 14d643939fad515f266cbd3364cbdfe7c72c37db Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 10 Oct 2018 17:21:25 -0400 Subject: [PATCH 2/4] Fix pipfile entry formation Signed-off-by: Dan Ryan --- pipenv/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pipenv/utils.py b/pipenv/utils.py index 88622b686d..4c65f3635f 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -489,9 +489,14 @@ def resolve_deps( # if verbose: # print('Error generating hash for {}'.format(name)) req.hashes = sorted(set(collected_hashes)) - name, entry = req.pipfile_entry + name, _entry = req.pipfile_entry + entry = {} + if isinstance(_entry, six.string_types): + entry["version"] = _entry + else: + entry["version"] = version + entry.update(_entry) entry["name"] = name - entry["version"] = version # if index: # d.update({"index": index}) if markers_lookup.get(result.name): From 7df1e857844ce55f8c34b62e299b80f64114a29a Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 10 Oct 2018 17:36:14 -0400 Subject: [PATCH 3/4] Fix versions Signed-off-by: Dan Ryan --- pipenv/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipenv/utils.py b/pipenv/utils.py index 4c65f3635f..f3599301da 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -492,10 +492,10 @@ def resolve_deps( name, _entry = req.pipfile_entry entry = {} if isinstance(_entry, six.string_types): - entry["version"] = _entry + entry["version"] = _entry.lstrip("=") else: - entry["version"] = version entry.update(_entry) + entry["version"] = version entry["name"] = name # if index: # d.update({"index": index}) From f308e4eeb999af4f6109db1b99677cf548c5814d Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Wed, 10 Oct 2018 17:56:57 -0400 Subject: [PATCH 4/4] Add license for Cerberus Signed-off-by: Dan Ryan --- pipenv/vendor/cerberus/LICENSE | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 pipenv/vendor/cerberus/LICENSE diff --git a/pipenv/vendor/cerberus/LICENSE b/pipenv/vendor/cerberus/LICENSE new file mode 100644 index 0000000000..2e0bcdd8d8 --- /dev/null +++ b/pipenv/vendor/cerberus/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2012-2016 Nicola Iarocci. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE.