Skip to content

Commit

Permalink
Refactors type validation to a descriptive paradigm
Browse files Browse the repository at this point in the history
Methods to test types are hereby declared as deprecated and their support is
supposed to be removed with the next breaking release.

Also - accidentally - contains improvements of docs related to errors.
  • Loading branch information
funkyfuture authored and nicolaiarocci committed Sep 25, 2017
1 parent 549cb26 commit ce1ef4f
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 97 deletions.
4 changes: 3 additions & 1 deletion cerberus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@

from __future__ import absolute_import

from cerberus.validator import Validator, DocumentError
from cerberus.validator import DocumentError, Validator
from cerberus.schema import (rules_set_registry, schema_registry, Registry,
SchemaError)
from cerberus.utils import TypeDefinition


__version__ = "1.1"
Expand All @@ -21,6 +22,7 @@
DocumentError.__name__,
Registry.__name__,
SchemaError.__name__,
TypeDefinition.__name__,
Validator.__name__,
'schema_registry',
'rules_set_registry'
Expand Down
14 changes: 6 additions & 8 deletions cerberus/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@
from cerberus.utils import compare_paths_lt, quote_string


ErrorDefinition = namedtuple('cerberus_error', 'code, rule')
ErrorDefinition = namedtuple('ErrorDefinition', 'code, rule')
"""
Error definition class
Each distinguishable error is defined as a two-value-tuple that holds
a *unique* error id as integer and the rule as string that can cause it.
The attributes are accessible as properties ``id`` and ``rule``.
The names do not contain a common prefix as they are supposed to be referenced
within the module namespace, e.g. errors.CUSTOM
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``.
"""


Expand Down
50 changes: 36 additions & 14 deletions cerberus/schema.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import absolute_import

from collections import Callable, Hashable, Iterable, Mapping, MutableMapping
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
from cerberus.utils import (get_Validator_class, validator_factory,
mapping_hash, TypeDefinition)


class SchemaError(Exception):
Expand All @@ -22,6 +24,13 @@ def __new__(cls, *args, **kwargs):
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={}):
Expand Down Expand Up @@ -273,18 +282,6 @@ def _validate_logical(self, rule, field, value):
else:
self.target_validator._valid_schemas.add(_hash)

def _validate_type_callable(self, value):
if isinstance(value, Callable):
return True

def _validate_type_hashable(self, value):
if isinstance(value, Hashable):
return True

def _validate_type_hashables(self, value):
if self._validate_type_list(value):
return all(self._validate_type_hashable(x) for x in value)

def _validator_bulk_schema(self, field, value):
if isinstance(value, _str_type):
if value in self.known_rules_set_refs:
Expand Down Expand Up @@ -313,6 +310,22 @@ def _validator_bulk_schema(self, field, value):
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
Expand Down Expand Up @@ -355,6 +368,15 @@ def _validator_schema(self, field, value):
else:
self.target_validator._valid_schemas.add(_hash)

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)

####

Expand Down
19 changes: 19 additions & 0 deletions cerberus/tests/test_assorted.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-

from decimal import Decimal

from pytest import mark

from cerberus import TypeDefinition, Validator
from cerberus.tests import assert_fail, assert_success


Expand All @@ -27,3 +30,19 @@ def test_that_test_fails(test, document):
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)
2 changes: 1 addition & 1 deletion cerberus/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def test_validated_schema_cache():
v = Validator({'foozifix': {'coerce': int}})
assert len(v._valid_schemas) == cache_size

max_cache_size = 130
max_cache_size = 131
assert cache_size <= max_cache_size, \
"There's an unexpected high amount (%s) of cached valid " \
"definition schemas. Unless you added further tests, " \
Expand Down
1 change: 1 addition & 0 deletions cerberus/tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ def _validate_min_number(self, min_number, field, value):
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
Expand Down
26 changes: 25 additions & 1 deletion cerberus/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
from __future__ import absolute_import

from collections import Mapping, Sequence
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])):
Expand Down Expand Up @@ -66,6 +79,17 @@ def quote_string(value):
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, mixin=None, class_dict={}):
""" Dynamically create a :class:`~cerberus.Validator` subclass.
Docstrings of mixin-classes will be added to the resulting
Expand Down
Loading

0 comments on commit ce1ef4f

Please sign in to comment.