diff --git a/cerberus/cerberus.py b/cerberus/cerberus.py index c5912f5b..9b3c45e5 100644 --- a/cerberus/cerberus.py +++ b/cerberus/cerberus.py @@ -453,6 +453,7 @@ def __normalize_mapping(self, mapping, schema): self.__normalize_rename_fields(mapping, schema) if self.purge_unknown: self._normalize_purge_unknown(mapping, schema) + self._normalize_default(mapping, schema) self._normalize_coerce(mapping, schema) self.__normalize_containers(mapping, schema) return mapping @@ -564,6 +565,14 @@ def _normalize_rename_handler(self, mapping, schema, field): mapping[new_name] = mapping[field] del mapping[field] + def _normalize_default(self, mapping, schema): + for field in tuple(schema): + nullable = schema[field].get('nullable', False) + if 'default' in schema[field] and \ + (field not in mapping or + mapping[field] is None and not nullable): + mapping[field] = schema[field]['default'] + # # Validating def validate(self, document, schema=None, update=False, normalize=True): diff --git a/cerberus/tests/__init__.py b/cerberus/tests/__init__.py index 7df20fe5..9772a17b 100644 --- a/cerberus/tests/__init__.py +++ b/cerberus/tests/__init__.py @@ -235,3 +235,10 @@ def assertChildErrors(self, *args, **kwargs): def assertBadType(self, field, data_type, value): self.assertFail({field: value}) self.assertError(field, (field, 'type'), errors.BAD_TYPE, data_type) + + def assertNormalized(self, document, expected, schema=None, validator=None): + if validator is None: + validator = self.validator + + self.assertSuccess(document, schema, validator) + self.assertDictEqual(validator.document, expected) diff --git a/cerberus/tests/tests.py b/cerberus/tests/tests.py index 9179acda..2a3d0e18 100644 --- a/cerberus/tests/tests.py +++ b/cerberus/tests/tests.py @@ -1319,19 +1319,17 @@ def test_excludes_hashable(self): class TestNormalization(TestBase): def test_coerce(self): - schema = { - 'amount': {'coerce': int} - } - v = Validator(schema) - v.validate({'amount': '1'}) - self.assertEqual(v.document['amount'], 1) + schema = {'amount': {'coerce': int}} + document = {'amount': '1'} + expected = {'amount': 1} + self.assertNormalized(document, expected, schema) def test_coerce_in_subschema(self): schema = {'thing': {'type': 'dict', 'schema': {'amount': {'coerce': int}}}} - v = Validator(schema) - self.assertEqual(v.validated({'thing': {'amount': '2'}}) - ['thing']['amount'], 2) # noqa + document = {'thing': {'amount': '2'}} + expected = {'thing': {'amount': 2}} + self.assertNormalized(document, expected, schema) def test_coerce_not_destructive(self): schema = { @@ -1361,34 +1359,47 @@ def test_coerce_catches_TypeError(self): def test_coerce_unknown(self): schema = {'foo': {'schema': {}, 'allow_unknown': {'coerce': int}}} - v = Validator(schema) document = {'foo': {'bar': '0'}} - self.assertDictEqual(v.normalized(document), {'foo': {'bar': 0}}) + expected = {'foo': {'bar': 0}} + self.assertNormalized(document, expected, schema) def test_normalized(self): schema = {'amount': {'coerce': int}} - v = Validator(schema) - self.assertEqual(v.normalized({'amount': '2'})['amount'], 2) + document = {'amount': '2'} + expected = {'amount': 2} + self.assertNormalized(document, expected, schema) def test_rename(self): + schema = {'foo': {'rename': 'bar'}} document = {'foo': 0} - v = Validator({'foo': {'rename': 'bar'}}) - self.assertDictEqual(v.normalized(document), {'bar': 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'} + self.validator(document, schema, False) + self.assertDictEqual(self.validator.document, expected) def test_rename_handler(self): + validator = Validator(allow_unknown={'rename_handler': int}) + schema = {} document = {'0': 'foo'} - v = Validator({}, allow_unknown={'rename_handler': int}) - self.assertDictEqual(v.normalized(document), {0: 'foo'}) + expected = {0: 'foo'} + self.assertNormalized(document, expected, schema, validator) def test_purge_unknown(self): - v = Validator({'foo': {'type': 'string'}}, purge_unknown=True) - self.assertDictEqual(v.normalized({'bar': 'foo'}), {}) - v.purge_unknown = False - self.assertDictEqual(v.normalized({'bar': 'foo'}), {'bar': 'foo'}) - v.schema = {'foo': {'type': 'dict', - 'schema': {'foo': {'type': 'string'}}, - 'purge_unknown': True}} - self.assertDictEqual(v.normalized({'foo': {'bar': ''}}), {'foo': {}}) + validator = Validator(purge_unknown=True) + schema = {'foo': {'type': 'string'}} + document = {'bar': 'foo'} + expected = {} + self.assertNormalized(document, expected, schema, validator) + + def test_purge_unknown_in_subschema(self): + schema = {'foo': {'type': 'dict', + 'schema': {'foo': {'type': 'string'}}, + 'purge_unknown': True}} + document = {'foo': {'bar': ''}} + expected = {'foo': {}} + self.assertNormalized(document, expected, schema) def test_issue_147_complex(self): schema = {'revision': {'coerce': int}} @@ -1417,27 +1428,70 @@ def test_coerce_in_valueschema(self): schema = {'thing': {'type': 'dict', 'valueschema': {'coerce': int, 'type': 'integer'}}} - self.assertSuccess({'thing': {'amount': '2'}}, schema) - self.assertDictEqual(self.validator.document, {'thing': {'amount': 2}}) + document = {'thing': {'amount': '2'}} + expected = {'thing': {'amount': 2}} + self.assertNormalized(document, expected, schema) def test_coerce_in_propertyschema(self): # https://github.com/nicolaiarocci/cerberus/issues/155 schema = {'thing': {'type': 'dict', 'propertyschema': {'coerce': int, 'type': 'integer'}}} - self.assertSuccess({'thing': {'5': 'foo'}}, schema) - self.assertDictEqual(self.validator.document, {'thing': {5: 'foo'}}) + document = {'thing': {'5': 'foo'}} + expected = {'thing': {5: 'foo'}} + self.assertNormalized(document, expected, schema) def test_coercion_of_sequence_items(self): # https://github.com/nicolaiarocci/cerberus/issues/161 schema = {'a_list': {'type': 'list', 'schema': {'type': 'float', 'coerce': float}}} - self.assertSuccess({'a_list': [3, 4, 5]}, schema) - result = self.validator.document - self.assertListEqual(result['a_list'], [3.0, 4.0, 5.0]) - for x in result['a_list']: + document = {'a_list': [3, 4, 5]} + expected = {'a_list': [3.0, 4.0, 5.0]} + self.assertNormalized(document, expected, schema) + for x in self.validator.document['a_list']: self.assertIsInstance(x, float) + def test_default_missing(self): + schema = {'foo': {'type': 'string'}, + 'bar': {'type': 'string', + 'default': 'bar_value'}} + document = {'foo': 'foo_value'} + expected = {'foo': 'foo_value', 'bar': 'bar_value'} + self.assertNormalized(document, expected, schema) + + def test_default_existent(self): + schema = {'foo': {'type': 'string'}, + 'bar': {'type': 'string', + 'default': 'bar_value'}} + document = {'foo': 'foo_value', 'bar': 'non_default'} + self.assertNormalized(document, document.copy(), schema) + + def test_default_none_nullable(self): + schema = {'foo': {'type': 'string'}, + 'bar': {'type': 'string', + 'nullable': True, + 'default': 'bar_value'}} + document = {'foo': 'foo_value', 'bar': None} + self.assertNormalized(document, document.copy(), schema) + + def test_default_none_nonnullable(self): + schema = {'foo': {'type': 'string'}, + 'bar': {'type': 'string', + 'nullable': False, + 'default': 'bar_value'}} + document = {'foo': 'foo_value', 'bar': 'bar_value'} + self.assertNormalized(document, document.copy(), schema) + + def test_default_missing_in_subschema(self): + schema = {'thing': {'type': 'dict', + 'schema': {'foo': {'type': 'string'}, + 'bar': {'type': 'string', + 'default': 'bar_value'}}}} + document = {'thing': {'foo': 'foo_value'}} + expected = {'thing': {'foo': 'foo_value', + 'bar': 'bar_value'}} + self.assertNormalized(document, expected, schema) + class DefinitionSchema(TestBase): def test_validated_schema_cache(self): diff --git a/docs/normalization-rules.rst b/docs/normalization-rules.rst index 039a7f0a..9644f408 100644 --- a/docs/normalization-rules.rst +++ b/docs/normalization-rules.rst @@ -39,6 +39,24 @@ subdocuments like ``allow_unknown`` (see :ref:`allowing-the-unknown`). The defau .. versionadded:: 0.10 +Default Values +-------------- +You can set default values for missing fields in the document by using the ``default`` rule. + +.. doctest:: + + >>> v.schema = {'amount': {'type': 'integer'}, 'kind': {'type': 'string', 'default': 'purchase'}} + >>> v.normalized({'amount': 1}) + {'amount': 1, 'kind': 'purchase'} + + >>> v.normalized({'amount': 1, 'kind': 'other'}) + {'amount': 1, 'kind': 'other'} + + >>> v.normalized({'amount': 1, 'kind': None}) + {'amount': 1, 'kind': None} + +.. versionadded:: 0.10 + .. _type-coercion: Value Coercion