Repository: Scille/umongo
Pull request #174 (2019-03-07 17:18:45)

Validate default values
I changed my mind about https://github.com/Scille/umongo/issues/78#issuecomment-470522732.

This change factorizes default/missing deserialization from `DictField`/`ListField` into `BaseField`.

Then, the `default` values are validated, which fixes the problem described in https://github.com/Scille/umongo/issues/36#issuecomment-247329135.

It is not only about validation. Consider a `DateTimeField` with a default value of `utcnow`. We want it to get the same constraint as if set manually. I'm thinking of the rounding added in https://github.com/Scille/umongo/pull/172.

- [x] I shall add a test to check `default` param validation.


Patch:
diff --git a/umongo/abstract.py b/umongo/abstract.py
index 0778836..eea632b 100644
--- a/umongo/abstract.py
+++ b/umongo/abstract.py
@@ -140,6 +140,18 @@ class BaseField(ma_fields.Field):
 
         super().__init__(*args, **kwargs)
 
+        # Deserialize default/missing values
+        # This ensures they are validated and get the proper types and constraints
+        for attr in ('default', 'missing'):
+            default = getattr(self, attr)
+            if default is not missing:
+                if callable(default):
+                    def call_default():
+                        return self.deserialize(default())
+                    setattr(self, attr, call_default)
+                else:
+                    setattr(self, attr, self.deserialize(default))
+
         # Overwrite error_messages to handle i18n translation
         self.error_messages = I18nErrorDict(self.error_messages)
         # `io_validate` will be run after `io_validate_resursive`
diff --git a/umongo/fields.py b/umongo/fields.py
index 7b109c4..1532a68 100644
--- a/umongo/fields.py
+++ b/umongo/fields.py
@@ -53,18 +53,6 @@ __all__ = (
 
 class DictField(BaseField, ma_fields.Dict):
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        for attr in ('default', 'missing'):
-            default = getattr(self, attr)
-            if default is not missing:
-                if callable(default):
-                    def call_default():
-                        return Dict(default())
-                    setattr(self, attr, call_default)
-                else:
-                    setattr(self, attr, Dict(default))
-
     def _deserialize(self, value, attr, data):
         value = super()._deserialize(value, attr, data)
         return Dict(value)
@@ -87,18 +75,6 @@ class DictField(BaseField, ma_fields.Dict):
 
 class ListField(BaseField, ma_fields.List):
 
-    def __init__(self, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-        for attr in ('default', 'missing'):
-            default = getattr(self, attr)
-            if default is not missing:
-                if callable(default):
-                    def call_default():
-                        return List(self.container, default())
-                    setattr(self, attr, call_default)
-                else:
-                    setattr(self, attr, List(self.container, default))
-
     def _deserialize(self, value, attr, data):
         return List(self.container, super()._deserialize(value, attr, data))
 


Test patch:
diff --git a/tests/test_document.py b/tests/test_document.py
index 1e898f3..005a81f 100644
--- a/tests/test_document.py
+++ b/tests/test_document.py
@@ -4,6 +4,7 @@ import pytest
 from datetime import datetime
 from bson import ObjectId, DBRef
 
+from marshmallow import validate
 from umongo import (Document, EmbeddedDocument, Schema, fields, exceptions,
                     post_dump, pre_load, validates_schema)
 
@@ -417,6 +418,16 @@ class TestDocument(BaseTest):
         assert jane.id != john.id
         assert jane.name == 'John Doe'
 
+    def test_validate_default(self):
+        """Check default values are validated"""
+
+        with pytest.raises(exceptions.ValidationError):
+            class User(Document):
+                name = fields.StringField(
+                    default='Eric',
+                    validate=validate.OneOf(('Stan', 'Kyle', 'Kenny'))
+                )
+
 
 class TestConfig(BaseTest):