Repository: Scille/umongo (python)

Issue:
DictField / ListField: how to pass dict / list as default?
Since #154, default values must be passed in deserialized form for consistency with marshmallow 3.

One can't write this anymore:

```py
list_field = ListField(IntField, default=list)
```

One must now write

```py
from umongo.data_objects import List
list_field = ListField(IntField, default=List)
```

Not ideal.

Shall we still take default values in serialized form in umongo even if it is not consistent with marshmallow 3? Is there another way to cast the default value in these specific cases?

Edit: in fact, the workaround above doesn't work, so it is worse than not ideal, it is a blocker.

Pull request description:
Fix DictField/ListField default
Fixes #230.

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 4100c7e..62ac03e 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,25 +2,6 @@
 History
 =======
 
-3.0.0b1 (unreleased)
---------------------
-
-Features:
-
-* Support marshmallow 3 (see #154).
-* All field parameters beginning with ``"marshmallow_"`` are passed to the
-  marshmallow schema, rather than only a given list of known parameters.
-  (see #228)
-
-Other changes:
-
-* *Backwards-incompatible*: Drop support for marshmallow 2. See marshmallow
-  upgrading guide for a comprehensive list of changes. (see #154)
-* *Backwards-incompatible*: ``StrictDateTimeField`` is removed as marshmallow
-  now provides ``NaiveDateTimeField`` and ``AwareDateTimeField``. (see #154)
-* *Backwards-incompatible*: ``default`` shall now be provided in deserialized
-  form. (see #154)
-
 2.2.0 (2019-12-18)
 ------------------
 
diff --git a/umongo/fields.py b/umongo/fields.py
index 950a4cd..79d8c04 100644
--- a/umongo/fields.py
+++ b/umongo/fields.py
@@ -53,6 +53,19 @@ __all__ = (
 
 class DictField(BaseField, ma_fields.Dict):
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        def cast_value_or_callable(value):
+            if value is missing:
+                return missing
+            if callable(value):
+                return lambda: Dict(value())
+            return Dict(value)
+
+        self.default = cast_value_or_callable(self.default)
+        self.missing = cast_value_or_callable(self.missing)
+
     def _deserialize(self, value, attr, data, **kwargs):
         value = super()._deserialize(value, attr, data, **kwargs)
         return Dict(value)
@@ -75,6 +88,19 @@ class DictField(BaseField, ma_fields.Dict):
 
 class ListField(BaseField, ma_fields.List):
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        def cast_value_or_callable(inner, value):
+            if value is missing:
+                return missing
+            if callable(value):
+                return lambda: List(inner, value())
+            return List(inner, value)
+
+        self.default = cast_value_or_callable(self.inner, self.default)
+        self.missing = cast_value_or_callable(self.inner, self.missing)
+
     def _deserialize(self, value, attr, data, **kwargs):
         ret = List(self.inner, super()._deserialize(value, attr, data, **kwargs))
         return ret


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 9577717..21c5ff9 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -275,6 +275,33 @@ class TestFields(BaseTest):
         d4.from_mongo({'in_mongo_dict': None})
         assert d4.get('dict') is None
 
+    def test_dict_default(self):
+
+        class MySchema(Schema):
+            # Passing a mutable as default is a bad idea in real life
+            d_dict = fields.DictField(default={'1': 1, '2': 2})
+            c_dict = fields.DictField(default=lambda: {'1': 1, '2': 2})
+
+        MyDataProxy = data_proxy_factory('My', MySchema())
+        d = MyDataProxy()
+        assert d.to_mongo() == {
+            'd_dict': {'1': 1, '2': 2},
+            'c_dict': {'1': 1, '2': 2},
+        }
+        assert isinstance(d.get('d_dict'), Dict)
+        assert isinstance(d.get('c_dict'), Dict)
+        d.get('d_dict')['3'] = 3
+        d.get('c_dict')['3'] = 3
+
+        d.delete('d_dict')
+        d.delete('c_dict')
+        assert d.to_mongo() == {
+            'd_dict': {'1': 1, '2': 2},
+            'c_dict': {'1': 1, '2': 2},
+        }
+        assert isinstance(d.get('d_dict'), Dict)
+        assert isinstance(d.get('c_dict'), Dict)
+
     def test_list(self):
 
         class MySchema(Schema):
@@ -360,6 +387,36 @@ class TestFields(BaseTest):
             d3._data.get('in_mongo_list')
         ) == '<object umongo.data_objects.List([])>'
 
+    def test_list_default(self):
+
+        class MySchema(Schema):
+            d_list = fields.ListField(fields.IntField(), default=(1, 2, 3))
+            c_list = fields.ListField(fields.IntField(), default=lambda: (1, 2, 3))
+
+        MyDataProxy = data_proxy_factory('My', MySchema())
+        d = MyDataProxy()
+        assert d.to_mongo() == {
+            'd_list': [1, 2, 3],
+            'c_list': [1, 2, 3],
+        }
+        assert isinstance(d.get('d_list'), List)
+        assert isinstance(d.get('c_list'), List)
+        d.get('d_list').append(4)
+        d.get('c_list').append(4)
+        assert d.to_mongo(update=True) == {
+            '$set': {'c_list': [1, 2, 3, 4], 'd_list': [1, 2, 3, 4]}}
+
+        d.delete('d_list')
+        d.delete('c_list')
+        assert d.to_mongo() == {
+            'd_list': [1, 2, 3],
+            'c_list': [1, 2, 3],
+        }
+        assert isinstance(d.get('d_list'), List)
+        assert isinstance(d.get('c_list'), List)
+        assert d.to_mongo(update=True) == {
+            '$set': {'c_list': [1, 2, 3], 'd_list': [1, 2, 3]}}
+
     def test_complex_list(self):
 
         @self.instance.register