Skip to content

Commit

Permalink
add require_all (close #417)
Browse files Browse the repository at this point in the history
  • Loading branch information
pohmelie committed Aug 13, 2018
1 parent 7d61ac3 commit 1e914e6
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 14 deletions.
97 changes: 97 additions & 0 deletions cerberus/tests/test_validation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-

import itertools
import re
import sys
from datetime import datetime, date
Expand Down Expand Up @@ -1838,3 +1839,99 @@ def test_contains(constraint):
errors.MISSING_MEMBERS
].info[0]
assert any(x in missing_actors for x in ('Eric Idle', 'Terry Gilliam'))


def test_require_all_simple():
schema = {'foo': {'type': 'string'}}
validator = Validator(require_all=True)
assert_fail(
{},
schema,
validator,
error=('foo', '__require_all__', errors.REQUIRED_FIELD, True),
)
assert_success({'foo': 'bar'}, schema, validator)
validator.require_all = False
assert_success({}, schema, validator)
assert_success({'foo': 'bar'}, schema, validator)


def test_require_all_override_by_required():
schema = {'foo': {'type': 'string', 'required': False}}
validator = Validator(require_all=True)
assert_success({}, schema, validator)
assert_success({'foo': 'bar'}, schema, validator)
validator.require_all = False
assert_success({}, schema, validator)
assert_success({'foo': 'bar'}, schema, validator)

schema = {'foo': {'type': 'string', 'required': True}}
validator.require_all = True
assert_fail(
{},
schema,
validator,
error=('foo', ('foo', 'required'), errors.REQUIRED_FIELD, True),
)
assert_success({'foo': 'bar'}, schema, validator)
validator.require_all = False
assert_fail(
{},
schema,
validator,
error=('foo', ('foo', 'required'), errors.REQUIRED_FIELD, True),
)
assert_success({'foo': 'bar'}, schema, validator)


@mark.parametrize(
"validator_require_all, sub_doc_require_all",
list(itertools.product([True, False], repeat=2)),
)
def test_require_all_override_by_subdoc_require_all(
validator_require_all, sub_doc_require_all
):
sub_schema = {"bar": {"type": "string"}}
schema = {
"foo": {
"type": "dict",
"require_all": sub_doc_require_all,
"schema": sub_schema,
}
}
validator = Validator(require_all=validator_require_all)

assert_success({"foo": {"bar": "baz"}}, schema, validator)
if validator_require_all:
assert_fail({}, schema, validator)
else:
assert_success({}, schema, validator)
if sub_doc_require_all:
assert_fail({"foo": {}}, schema, validator)
else:
assert_success({"foo": {}}, schema, validator)


def test_require_all_and_exclude():
schema = {
'foo': {'type': 'string', 'excludes': 'bar'},
'bar': {'type': 'string', 'excludes': 'foo'},
}
validator = Validator(require_all=True)
assert_fail(
{},
schema,
validator,
errors=[
('foo', '__require_all__', errors.REQUIRED_FIELD, True),
('bar', '__require_all__', errors.REQUIRED_FIELD, True),
],
)
assert_success({'foo': 'value'}, schema, validator)
assert_success({'bar': 'value'}, schema, validator)
assert_fail({'foo': 'value', 'bar': 'value'}, schema, validator)
validator.require_all = False
assert_success({}, schema, validator)
assert_success({'foo': 'value'}, schema, validator)
assert_success({'bar': 'value'}, schema, validator)
assert_fail({'foo': 'value', 'bar': 'value'}, schema, validator)
44 changes: 39 additions & 5 deletions cerberus/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ class BareValidator(object):
:param allow_unknown: See :attr:`~cerberus.Validator.allow_unknown`.
Defaults to ``False``.
:type allow_unknown: :class:`bool` or any :term:`mapping`
:param require_all: See :attr:`~cerberus.Validator.require_all`.
Defaults to ``False``.
:type require_all: :class:`bool`
:param purge_unknown: See :attr:`~cerberus.Validator.purge_unknown`.
Defaults to to ``False``.
:type purge_unknown: :class:`bool`
Expand Down Expand Up @@ -132,7 +135,8 @@ 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, purge_readonly=False,
allow_unknown=False, require_all=False,
purge_unknown=False, purge_readonly=False,
error_handler=errors.BasicErrorHandler)
"""

Expand Down Expand Up @@ -169,6 +173,7 @@ def __init__(self, *args, **kwargs):
self.__store_config(args, kwargs)
self.schema = kwargs.get('schema', None)
self.allow_unknown = kwargs.get('allow_unknown', False)
self.require_all = kwargs.get('require_all', False)
self._remaining_rules = []
""" Keeps track of the rules that are next in line to be evaluated
during the validation of a field.
Expand Down Expand Up @@ -198,6 +203,7 @@ def __store_config(self, args, kwargs):
'schema',
'ignore_none_values',
'allow_unknown',
'require_all',
'purge_unknown',
'purge_readonly',
)
Expand Down Expand Up @@ -280,6 +286,10 @@ def _error(self, *args):
field_definitions = self._resolve_rules_set(self.schema[field])
if rule == 'nullable':
constraint = field_definitions.get(rule, False)
elif rule == 'required':
constraint = field_definitions.get(rule, self.require_all)
if rule not in field_definitions:
schema_path = "__require_all__"
else:
constraint = field_definitions[rule]

Expand Down Expand Up @@ -314,6 +324,7 @@ def _get_child_validator(self, document_crumb=None, schema_crumb=None, **kwargs)
child_config['is_child'] = True
child_config['error_handler'] = toy_error_handler
child_config['root_allow_unknown'] = self.allow_unknown
child_config['root_require_all'] = self.require_all
child_config['root_document'] = self.document
child_config['root_schema'] = self.schema

Expand Down Expand Up @@ -428,6 +439,17 @@ def allow_unknown(self, value):
DefinitionSchema(self, {'allow_unknown': value})
self._config['allow_unknown'] = value

@property
def require_all(self):
""" If ``True`` known fields that are defined in the schema will
be required.
Type: :class:`bool` """
return self._config.get('require_all', False)

@require_all.setter
def require_all(self, value):
self._config['require_all'] = value

@property
def errors(self):
""" The errors of the last processing formatted by the handler that is
Expand Down Expand Up @@ -488,6 +510,12 @@ def root_allow_unknown(self):
first level ancestor of a child validator. """
return self._config.get('root_allow_unknown', self.allow_unknown)

@property
def root_require_all(self):
""" The :attr:`~cerberus.Validator.require_all` attribute of
the first level ancestor of a child validator. """
return self._config.get('root_require_all', self.require_all)

@property
def root_document(self):
""" The :attr:`~cerberus.Validator.document` attribute of the
Expand Down Expand Up @@ -770,6 +798,7 @@ def __normalize_mapping_per_schema(self, field, mapping, schema):
schema=rules.get('schema', {}),
allow_unknown=rules.get('allow_unknown', self.allow_unknown), # noqa: E501
purge_unknown=rules.get('purge_unknown', self.purge_unknown),
require_all=rules.get('require_all', self.require_all),
) # noqa: E501
value_type = type(mapping[field])
result_value = validator.normalized(mapping[field], always_return_document=True)
Expand Down Expand Up @@ -1019,7 +1048,7 @@ def validate_rule(rule):
for x in definitions
if x not in rules_queue
and x not in self.normalization_rules
and x not in ('allow_unknown', 'meta', 'required')
and x not in ('allow_unknown', 'require_all', 'meta', 'required')
)
self._remaining_rules = rules_queue

Expand Down Expand Up @@ -1161,12 +1190,12 @@ def _validate_excludes(self, excluded_fields, field, value):

# Mark the currently evaluated field as not required for now if it actually is.
# One of the so marked will be needed to pass when required fields are checked.
if self.schema[field].get('required', False):
if self.schema[field].get('required', self.require_all):
self._unrequired_by_excludes.add(field)

for excluded_field in excluded_fields:
if excluded_field in self.schema and self.schema[field].get(
'required', False
'required', self.require_all
):

self._unrequired_by_excludes.add(excluded_field)
Expand Down Expand Up @@ -1349,6 +1378,8 @@ def _validate_regex(self, pattern, field, value):

_validate_required = dummy_for_rule_validation(""" {'type': 'boolean'} """)

_validate_require_all = dummy_for_rule_validation(""" {'type': 'boolean'} """)

def __validate_required_fields(self, document):
""" Validates that required fields are not missing.
Expand All @@ -1358,7 +1389,8 @@ def __validate_required_fields(self, document):
required = set(
field
for field, definition in self.schema.items()
if self._resolve_rules_set(definition).get('required') is True
if self._resolve_rules_set(definition).get('required', self.require_all)
is True
)
except AttributeError:
if self.is_child and self.schema_path[-1] == 'schema':
Expand Down Expand Up @@ -1398,11 +1430,13 @@ def _validate_schema(self, schema, field, 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)
require_all = self.schema[field].get('require_all', self.require_all)
validator = self._get_child_validator(
document_crumb=field,
schema_crumb=(field, 'schema'),
schema=schema,
allow_unknown=allow_unknown,
require_all=require_all,
)
try:
if not validator(value, update=self.update, normalize=False):
Expand Down
8 changes: 4 additions & 4 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ Validator Class
_errors, errors, _get_child_validator, ignore_none_values,
is_child, _lookup_field, mandatory_validations, normalized,
priority_validations, purge_unknown, recent_error,
_remaining_rules, root_allow_unknown, root_document, root_schema,
rules_set_registry, schema, schema_error_tree, schema_path,
schema_registry, types, types_mapping, _valid_schemas, validate,
validated
require_all, _remaining_rules, root_allow_unknown, root_document,
root_require_all, root_schema, rules_set_registry, schema,
schema_error_tree, schema_path, schema_registry, types,
types_mapping, _valid_schemas, validate, validated


Rules Set & Schema Registry
Expand Down
11 changes: 7 additions & 4 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -334,16 +334,19 @@ Study the source code for example usages.
Added ``document_crumb`` and ``schema_crumb`` as optional keyword-
arguments.

`Validator.root_document`, `.root_schema` & `root_allow_unknown`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`Validator.root_document`, `.root_schema`, `.root_allow_unknown` & `.root_require_all`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A child-validator - as used when validating a ``schema`` - can access the first
generation validator's document and schema that are being processed as well as
the constraints for unknown fields via its ``root_document`` and ``root_schema``
``root_allow_unknown``-properties.
the constraints for unknown fields via its ``root_document``, ``root_schema``,
``root_allow_unknown`` and ``root_require_all`` properties.

.. versionadded:: 1.0

.. versionchanged:: 1.3
Added ``root_require_all``

`Validator.document_path` & `Validator.schema_path`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion docs/schemas.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ which is expected to be a string not longer than 10 characters. Something like
very long string'}`` or ``{'name': 99}`` would not.

By default all keys in a document are optional unless the :ref:`required`-rule
is set for a key.
is set ``True`` for individual fields or the validator's :attr:~cerberus.Validator.require_all
is set to ``True`` in order to expect all schema-defined fields to be present in the document.


Registries
Expand Down
40 changes: 40 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,46 @@ mapping that is checked against the :ref:`schema <schema_dict-rule>` rule:
``allow_unknown`` can also be set to a validation schema.


.. _requiring-all:

Requiring all
-------------
See also :ref:`this paragraph <require_all>`, :ref:`this paragraph <required>`.

By default any keys defined in the schema are not required.
However, you can require all document keys pairs by setting
``require_all`` to ``True`` at validator initialization (``v = Validator(…, require_all=True)``)
or change it latter via attribute access (``v.require_all = True``).
``require_all`` can also be set as rule to configure a validator for a subdocument
that is checked against the :ref:`schema <schema_dict-rule>` rule:

.. doctest::

>>> v = Validator()
>>> v.require_all
False

>>> schema = {
... 'name': {'type': 'string'},
... 'a_dict': {
... 'type': 'dict',
... 'require_all': True,
... 'schema': {
... 'address': {'type': 'string'}
... }
... }
... }

>>> v.validate({'name': 'foo', 'a_dict': {}}, schema)
False
>>> v.errors
{'a_dict': [{'address': ['required field']}]}

>>> v.validate({'a_dict': {'address': 'foobar'}}, schema)
True

.. versionadded:: 1.3

Fetching Processed Documents
----------------------------

Expand Down
15 changes: 15 additions & 0 deletions docs/validation-rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -660,10 +660,21 @@ library's :mod:`re`-module.

.. versionadded:: 0.7

.. _require_all:

require_all
-----------
This can be used in conjunction with the `schema <schema_dict-rule>`_ rule
when validating a mapping in order to set the
:attr:`~cerberus.Validator.require_all` property of the validator for the
subdocument.
For a full elaboration refer to :ref:`this paragraph <requiring-all>`.

.. _required:

required
--------

If ``True`` the field is mandatory. Validation will fail when it is missing,
unless :meth:`~cerberus.Validator.validate` is called with ``update=True``:

Expand All @@ -679,6 +690,10 @@ unless :meth:`~cerberus.Validator.validate` is called with ``update=True``:
>>> v.validate(document, update=True)
True

.. note::

To define all fields of a document as required see :ref:`this paragraph <requiring-all>`.

.. note::

String fields with empty values will still be validated, even when
Expand Down

0 comments on commit 1e914e6

Please sign in to comment.