ok
Direktori : /opt/alt/python35/lib/python3.5/site-packages/cerberus/ |
Current File : //opt/alt/python35/lib/python3.5/site-packages/cerberus/validator.py |
""" 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 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 Validator(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 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') """ Rules that will be processed in that order before any other and abort validation of a document's field if return ``True``. Type: :class:`tuple` """ _valid_schemas = set() """ A :class:`set` of hashed 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) def __init_error_handler(self, 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(' ', '_')) return getattr(self, methodname, None) 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: context = context.get(part) if context is None: return None, None 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 # 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 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) # # 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: 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) 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], 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], error) def __normalize_coerce(self, processor, field, value, 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, 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: 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]['schema'], allow_unknown=schema[field].get('allow_unknown', self.allow_unknown), # noqa purge_unknown=schema[field].get('purge_unknown', self.purge_unknown)) # noqa mapping[field] = validator.normalized(mapping[field], always_return_document=True) 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) result = validator.normalized(document, always_return_document=True) for i in result: mapping[field][i] = result[i] 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, 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) self.is_normalized = True 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) # Remember to keep the validations method below this line # sorted alphabetically 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) if validator: return validator(definitions.get(rule, None), field, value) definitions = self._resolve_rules_set(definitions) value = self.document[field] """ _validate_-methods must return True to abort validation. """ prior_rules = tuple((x for x in self.priority_validations if x in definitions or x in self.mandatory_validations)) for rule in prior_rules: if validate_rule(rule): return rules = set(definitions) rules |= set(self.mandatory_validations) rules -= set(prior_rules + ('allow_unknown', 'required')) rules -= set(self.normalization_rules) for rule in rules: try: validate_rule(rule) except _SchemaRuleTypeError: break _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', 'hashables']} """ 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 and not empty: self._error(field, errors.EMPTY_NOT_ALLOWED) def _validate_excludes(self, excludes, field, value): """ {'type': ['hashable', 'hashables']} """ 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 validator = self._get_child_validator(document_crumb=field, schema_crumb=(field, 'items'), # noqa 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 nullable: return True else: self._error(field, errors.NOT_NULLABLE) return True 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) return True # 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: return True 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(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']} """ types = [data_type] if isinstance(data_type, _str_type) else data_type if any(self.__get_rule_handler('validate_type', x)(value) for x in types): return else: self._error(field, errors.BAD_TYPE) return True def _validate_type_boolean(self, value): if isinstance(value, bool): return True def _validate_type_date(self, value): if isinstance(value, date): return True def _validate_type_datetime(self, value): if isinstance(value, datetime): return True def _validate_type_dict(self, value): if isinstance(value, Mapping): return True def _validate_type_float(self, value): if isinstance(value, (float, _int_types)): return True def _validate_type_integer(self, value): if isinstance(value, _int_types): return True def _validate_type_binary(self, value): if isinstance(value, (bytes, bytearray)): return True def _validate_type_list(self, value): if isinstance(value, Sequence) and not isinstance( value, _str_type): return True def _validate_type_number(self, value): if isinstance(value, (_int_types, float)) \ and not isinstance(value, bool): return True def _validate_type_set(self, value): if isinstance(value, set): return True def _validate_type_string(self, value): if isinstance(value, _str_type): return True 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, cls.validation_rules = (), {} for attribute in attributes_with_prefix('validate'): if attribute.startswith('type_'): cls.types += (attribute[len('type_'):],) else: cls.validation_rules[attribute] = \ cls.__get_rule_schema('_validate_' + attribute) cls.validation_rules['type']['allowed'] = cls.types 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', (Validator,), {})