diff --git a/CHANGES.rst b/CHANGES.rst index d28009d6..781979b9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,7 @@ New - The ``contains`` rule (`#358`_) - All fields that are defined as ``readonly`` are removed from a document when a validator has the ``purge_readonly`` flag set to ``True`` (`#240`_) +- The ``validator`` rule is renamed to ``check_with`` (`#405`_) - **Python 2.6 and 3.3 are no longer supported** Fixed @@ -27,7 +28,7 @@ Improved - Change ``allowed`` rule to use containers instead of lists (`#384`_) - Remove ``Registry`` from top level namespace (`#354`_) - Remove ``utils.is_class`` -- Check the ``empty`` rule against values of type ``Sized``. +- Check the ``empty`` rule against values of type ``Sized`` Docs ~~~~ @@ -39,16 +40,17 @@ Docs - Add feature freeze note to CONTRIBUTING and note on Python support in README - Add the intent of a ``dataclasses`` module to ROADMAP.md -- Update README link. Make it point to the new PYPI website +- Update README link. Make it point to the new PyPI website - Update README with elaborations on versioning and testing - Fix misspellings and missing pronouns - Remove redundant hint from ``*of-rules``. -- Add usage reccommendation regarding the ``*ok-rules`` +- Add usage recommendation regarding the ``*of-rules`` - Add a few clarifications to the GitHub issue template -- Update README link. Make it point to the new PYPI website +- Update README link. Make it point to the new PyPI website .. _`#420`: https://github.com/pyeve/cerberus/issues/420 .. _`#406`: https://github.com/pyeve/cerberus/issues/406 +.. _`#405`: https://github.com/pyeve/cerberus/issues/405 .. _`#404`: https://github.com/pyeve/cerberus/issues/404 .. _`#402`: https://github.com/pyeve/cerberus/issues/402 .. _`#389`: https://github.com/pyeve/cerberus/issues/389 diff --git a/cerberus/schema.py b/cerberus/schema.py index 76341814..aeffa8af 100644 --- a/cerberus/schema.py +++ b/cerberus/schema.py @@ -2,6 +2,7 @@ from collections import Callable, Hashable, Iterable, Mapping, MutableMapping, Sequence from copy import copy +from warnings import warn from cerberus import errors from cerberus.platform import _str_type @@ -116,6 +117,10 @@ def expand(cls, schema): schema = cls._expand_subschemas(schema) except Exception: pass + + # TODO remove this with the next major release + schema = cls._rename_deprecated_rulenames(schema) + return schema @classmethod @@ -190,6 +195,23 @@ def update(self, schema): else: self.schema = _new_schema + # TODO remove with next major release + @staticmethod + def _rename_deprecated_rulenames(schema): + for old, new in (('validator', 'check_with'),): + for field, rules in schema.items(): + if old in rules: + warn( + "The rule '{old}' was renamed to '{new}'. The old name will " + "not be available in the next major release of " + "Cerberus".format(old=old, new=new), + DeprecationWarning, + ) + schema[field][new] = schema[field][old] + schema[field].pop(old) + + return schema + def regenerate_validation_schema(self): self.validation_schema = SchemaValidationSchema(self.validator) @@ -247,7 +269,7 @@ def __init__(self, validator): class SchemaValidatorMixin(object): - """ This validator is extended to validate schemas passed to a Cerberus + """ This validator mixin provides mechanics to validate schemas passed to a Cerberus validator. """ @property @@ -278,33 +300,7 @@ 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): + def _check_with_bulk_schema(self, field, value): # resolve schema registry reference if isinstance(value, _str_type): if value in self.known_rules_set_refs: @@ -336,7 +332,7 @@ def _validator_bulk_schema(self, field, value): else: self.target_validator._valid_schemas.add(_hash) - def _validator_dependencies(self, field, value): + def _check_with_dependencies(self, field, value): if isinstance(value, _str_type): pass elif isinstance(value, Mapping): @@ -352,24 +348,24 @@ def _validator_dependencies(self, field, value): path = self.document_path + (field,) self._error(path, 'All dependencies must be a hashable type.') - def _validator_handler(self, field, value): + def _check_with_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 + not in self.target_validator.checkers + 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) + self._check_with_handler(field, handler) - def _validator_items(self, field, value): + def _check_with_items(self, field, value): for i, schema in enumerate(value): - self._validator_bulk_schema((field, i), schema) + self._check_with_bulk_schema((field, i), schema) - def _validator_schema(self, field, value): + def _check_with_schema(self, field, value): try: value = self._handle_schema_reference_for_validator(field, value) except _Abort: @@ -388,6 +384,25 @@ def _validator_schema(self, field, value): else: self.target_validator._valid_schemas.add(_hash) + def _check_with_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) + + 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 _handle_schema_reference_for_validator(self, field, value): if not isinstance(value, _str_type): return value @@ -402,24 +417,31 @@ def _handle_schema_reference_for_validator(self, field, 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 _validate_logical(self, rule, field, value): + """ {'allowed': ('allof', 'anyof', 'noneof', 'oneof')} """ + if not isinstance(value, Sequence): + self._error(field, errors.BAD_TYPE) + return - 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) + 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) #### diff --git a/cerberus/tests/test_customization.py b/cerberus/tests/test_customization.py index cbbe5555..8bc3f464 100644 --- a/cerberus/tests/test_customization.py +++ b/cerberus/tests/test_customization.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from pytest import mark + import cerberus from cerberus.tests import assert_fail, assert_success from cerberus.tests.conftest import sample_schema @@ -41,13 +43,33 @@ def _validate_bar(self, value): assert 'bar' in CustomValidator.validation_rules -def test_issue_265(): +# TODO remove 'validator' as rule parameter with the next major release +@mark.parametrize('rule', ('check_with', 'validator')) +def test_check_with_method(rule): + # https://github.com/pyeve/cerberus/issues/265 + class MyValidator(cerberus.Validator): + def _check_with_oddity(self, field, value): + if not value & 1: + self._error(field, "Must be an odd number") + + v = MyValidator(schema={'amount': {rule: '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',)), + ) + + +# TODO remove test with the next major release +@mark.parametrize('rule', ('check_with', 'validator')) +def test_validator_method(rule): 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'}}) + v = MyValidator(schema={'amount': {rule: 'oddity'}}) assert_success(document={'amount': 1}, validator=v) assert_fail( document={'amount': 2}, diff --git a/cerberus/validator.py b/cerberus/validator.py index e5cc6547..2ad90b43 100644 --- a/cerberus/validator.py +++ b/cerberus/validator.py @@ -1041,7 +1041,7 @@ def validate_rule(rule): _validate_allow_unknown = dummy_for_rule_validation( """ {'oneof': [{'type': 'boolean'}, {'type': ['dict', 'string'], - 'validator': 'bulk_schema'}]} """ + 'check_with': 'bulk_schema'}]} """ ) def _validate_allowed(self, allowed_values, field, value): @@ -1054,6 +1054,32 @@ def _validate_allowed(self, allowed_values, field, value): if value not in allowed_values: self._error(field, errors.UNALLOWED_VALUE, value) + def _validate_check_with(self, checks, field, value): + """ {'oneof': [ + {'type': 'callable'}, + {'type': 'list', + 'schema': {'oneof': [{'type': 'callable'}, + {'type': 'string'}]}}, + {'type': 'string'} + ]} """ + if isinstance(checks, _str_type): + try: + value_checker = self.__get_rule_handler('check_with', checks) + # TODO remove on next major release + except RuntimeError: + value_checker = self.__get_rule_handler('validator', checks) + warn( + "The 'validator' rule was renamed to 'check_with'. Please update " + "your schema and method names accordingly.", + DeprecationWarning, + ) + value_checker(field, value) + elif isinstance(checks, Iterable): + for v in checks: + self._validate_check_with(v, field, value) + else: + checks(field, value, self._error) + def _validate_contains(self, expected_values, field, value): """ {'empty': False } """ if not isinstance(value, Iterable): @@ -1072,7 +1098,7 @@ def _validate_contains(self, expected_values, field, value): def _validate_dependencies(self, dependencies, field, value): """ {'type': ('dict', 'hashable', 'list'), - 'validator': 'dependencies'} """ + 'check_with': 'dependencies'} """ if isinstance(dependencies, _str_type): dependencies = (dependencies,) @@ -1122,7 +1148,7 @@ def _validate_empty(self, empty, field, value): 'minlength', 'maxlength', 'regex', - 'validator', + 'check_with', ) if not empty: self._error(field, errors.EMPTY_NOT_ALLOWED) @@ -1164,7 +1190,7 @@ def _validate_forbidden(self, forbidden_values, field, value): self._error(field, errors.FORBIDDEN_VALUE, value) def _validate_items(self, items, field, values): - """ {'type': 'list', 'validator': 'items'} """ + """ {'type': 'list', 'check_with': 'items'} """ if len(items) != len(values): self._error(field, errors.ITEMS_LENGTH, len(items), len(values)) else: @@ -1279,7 +1305,7 @@ def _validate_nullable(self, nullable, field, value): ) def _validate_keyschema(self, schema, field, value): - """ {'type': ['dict', 'string'], 'validator': 'bulk_schema', + """ {'type': ['dict', 'string'], 'check_with': 'bulk_schema', 'forbidden': ['rename', 'rename_handler']} """ if isinstance(value, Mapping): validator = self._get_child_validator( @@ -1356,8 +1382,8 @@ def __validate_required_fields(self, document): def _validate_schema(self, schema, field, value): """ {'type': ['dict', 'string'], - 'anyof': [{'validator': 'schema'}, - {'validator': 'bulk_schema'}]} """ + 'anyof': [{'check_with': 'schema'}, + {'check_with': 'bulk_schema'}]} """ if schema is None: return @@ -1402,7 +1428,7 @@ def __validate_schema_sequence(self, field, schema, value): def _validate_type(self, data_type, field, value): """ {'type': ['string', 'list'], - 'validator': 'type'} """ + 'check_with': 'type'} """ if not data_type: return @@ -1432,25 +1458,8 @@ def _validate_type(self, data_type, field, value): 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', + """ {'type': ['dict', 'string'], 'check_with': 'bulk_schema', 'forbidden': ['rename', 'rename_handler']} """ schema_crumb = (field, 'valueschema') if isinstance(value, Mapping): @@ -1479,7 +1488,9 @@ def __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) + x[len(prefix) + 2 :] + for x in dir(cls) + if x.startswith('_' + prefix + '_') ) super(InspectedValidator, cls).__init__(*args) @@ -1503,9 +1514,12 @@ def attributes_with_prefix(prefix): 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 + # TODO remove second summand on next major release + cls.checkers = tuple(x for x in attributes_with_prefix('check_with')) + tuple( + x for x in attributes_with_prefix('validator') + ) + x = cls.validation_rules['check_with']['oneof'] + x[1]['schema']['oneof'][1]['allowed'] = x[2]['allowed'] = cls.checkers for rule in (x for x in cls.mandatory_validations if x != 'nullable'): cls.validation_rules[rule]['required'] = True diff --git a/docs/customize.rst b/docs/customize.rst index 179f89c2..741b3693 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -116,17 +116,17 @@ They can also be defined for subclasses of :class:`~cerberus.Validator`: .. _validator-rule-methods: -Methods that can be referenced by the validator rule ----------------------------------------------------- +Methods that can be referenced by the check_with rule +----------------------------------------------------- If a validation test doesn't depend on a specified constraint from a schema or needs to be more complex than a rule should be, it's possible to rather define -it as validators than as a rule. There are two ways to use the -:ref:`validator rule `. +it as *value checker* than as a rule. There are two ways to use the +:ref:`check_with rule `. One is by extending :class:`~cerberus.Validator` with a method prefixed with -``_validator_``. This allows to access the whole context of the validator +``_check_with_``. This allows to access the whole context of the validator instance including arbitrary configuration values and state. To reference such -method using the ``validator`` rule, simply pass the unprefixed method name as +method using the ``check_with`` rule, simply pass the unprefixed method name as a string constraint. For example, one can define an ``oddity`` validator method as follows: @@ -134,7 +134,7 @@ For example, one can define an ``oddity`` validator method as follows: .. testcode:: class MyValidator(Validator): - def _validator_oddity(self, field, value): + def _check_with_oddity(self, field, value): if not value & 1: self._error(field, "Must be an odd number") @@ -142,7 +142,7 @@ Usage would look something like: .. testcode:: - schema = {'amount': {'type': 'integer', 'validator': 'oddity'}} + schema = {'amount': {'type': 'integer', 'check_with': 'oddity'}} The second option to use the rule is to define a standalone function and pass it as the constraint. This brings with it the benefit of not having to extend diff --git a/docs/validation-rules.rst b/docs/validation-rules.rst index 70e7540b..084c213c 100644 --- a/docs/validation-rules.rst +++ b/docs/validation-rules.rst @@ -62,6 +62,56 @@ Validates if *any* of the provided constraints validates the field. See `\*of-ru .. versionadded:: 0.9 +.. _check-with-rule: + +check_with +---------- +Validates the value of a field by calling either a function or method. + +A function must be implemented like the following prototype:: + + def functionnname(field, value, error): + if value is invalid: + error(field, 'error message') + +The ``error`` argument points to the calling validator's ``_error`` method. See +:doc:`customize` on how to submit errors. + +Here's an example that tests whether an integer is odd or not: + +.. testcode:: + + def oddity(field, value, error): + if not value & 1: + error(field, "Must be an odd number") + +Then, you can validate a value like this: + +.. doctest:: + + >>> schema = {'amount': {'check_with': oddity}} + >>> v = Validator(schema) + >>> v.validate({'amount': 10}) + False + >>> v.errors + {'amount': ['Must be an odd number']} + + >>> v.validate({'amount': 9}) + True + +If the rule's constraint is a string, the :class:`~cerberus.Validator` instance +must have a method with that name prefixed by ``_check_with_``. See +:ref:`check-with-rule-methods` for an equivalent to the function-based example +above. + +The constraint can also be a sequence of these that will be called consecutively. :: + + schema = {'field': {'check_with': (oddity, 'prime number')}} + +.. versionchanged:: 1.3 + The rule was renamed from ``validator`` to ``check_with`` + + contains -------- This rule validates that the a container object contains all of the defined items. @@ -793,52 +843,6 @@ A list of types can be used to allow different values: .. [#] This is actually an alias of :class:`py2:str` in Python 2. -.. _validator-rule: - -validator ---------- -Validates the value by calling either a function or method. - -A function must be implemented like this the following prototype: :: - - def functionnname(field, value, error): - if value is invalid: - error(field, 'error message') - -The ``error`` argument points to the calling validator's ``_error`` method. See -:doc:`customize` on how to submit errors. - -Here's an example that tests whether an integer is odd or not: - -.. testcode:: - - def oddity(field, value, error): - if not value & 1: - error(field, "Must be an odd number") - -Then, you can validate a value like this: - -.. doctest:: - - >>> schema = {'amount': {'validator': oddity}} - >>> v = Validator(schema) - >>> v.validate({'amount': 10}) - False - >>> v.errors - {'amount': ['Must be an odd number']} - - >>> v.validate({'amount': 9}) - True - -If the rule's constraint is a string, the :class:`~cerberus.Validator` instance -must have a method with that name prefixed by ``_validator_``. See -:ref:`validator-rule-methods` for an equivalent to the function-based example -above. - -The constraint can also be a sequence of these that will be called consecutively. :: - - schema = {'field': {'validator': [oddity, 'prime number']}} - .. _valueschema-rule: valueschema