Repository: Scille/umongo
Pull request #335 (2021-01-04 23:02:44)

Add _dict_io_validate


Patch:
diff --git a/umongo/frameworks/motor_asyncio.py b/umongo/frameworks/motor_asyncio.py
index ccb61a5..30c8270 100644
--- a/umongo/frameworks/motor_asyncio.py
+++ b/umongo/frameworks/motor_asyncio.py
@@ -1,3 +1,4 @@
+import collections
 from contextvars import ContextVar
 from contextlib import asynccontextmanager
 
@@ -13,7 +14,7 @@ from ..instance import Instance
 from ..document import DocumentImplementation
 from ..data_objects import Reference
 from ..exceptions import NotCreatedError, UpdateError, DeleteError, NoneReferenceError
-from ..fields import ReferenceField, ListField, EmbeddedField
+from ..fields import ReferenceField, ListField, DictField, EmbeddedField
 from ..query_mapper import map_query
 
 from .tools import cook_find_filter, remove_cls_field_from_embedded_docs
@@ -361,6 +362,26 @@ async def _list_io_validate(field, value):
         raise ma.ValidationError(errors)
 
 
+async def _dict_io_validate(field, value):
+    if not value or not field.value_field:
+        return
+    validators = field.value_field.io_validate
+    if not validators:
+        return
+    tasks = []
+    for key, val in value.items():
+        tasks.append(_run_validators(validators, field.value_field, val))
+    results = await asyncio.gather(*tasks, return_exceptions=True)
+    errors = collections.defaultdict(dict)
+    for key, res in zip(value.keys(), results):
+        if isinstance(res, ma.ValidationError):
+            errors[key]["value"] = res.messages
+        elif res:
+            raise res
+    if errors:
+        raise ma.ValidationError(errors)
+
+
 async def _embedded_document_io_validate(field, value):
     if not value:
         return
@@ -405,6 +426,8 @@ class MotorAsyncIOBuilder(BaseBuilder):
             ]
         if isinstance(field, ListField):
             field.io_validate_recursive = _list_io_validate
+        if isinstance(field, DictField):
+            field.io_validate_recursive = _dict_io_validate
         if isinstance(field, ReferenceField):
             field.io_validate.append(_reference_io_validate)
             field.reference_cls = MotorAsyncIOReference
diff --git a/umongo/frameworks/pymongo.py b/umongo/frameworks/pymongo.py
index aace5fe..8815950 100644
--- a/umongo/frameworks/pymongo.py
+++ b/umongo/frameworks/pymongo.py
@@ -1,3 +1,4 @@
+import collections
 from contextvars import ContextVar
 from contextlib import contextmanager
 
@@ -11,7 +12,7 @@ from ..instance import Instance
 from ..document import DocumentImplementation
 from ..data_objects import Reference
 from ..exceptions import NotCreatedError, UpdateError, DeleteError, NoneReferenceError
-from ..fields import ReferenceField, ListField, EmbeddedField
+from ..fields import ReferenceField, ListField, DictField, EmbeddedField
 from ..query_mapper import map_query
 
 from .tools import cook_find_filter, remove_cls_field_from_embedded_docs
@@ -293,6 +294,22 @@ def _list_io_validate(field, value):
         raise ma.ValidationError(errors)
 
 
+def _dict_io_validate(field, value):
+    if not value or not field.value_field:
+        return
+    errors = collections.defaultdict(dict)
+    validators = field.value_field.io_validate
+    if not validators:
+        return
+    for key, val in value.items():
+        try:
+            _run_validators(validators, field.value_field, val)
+        except ma.ValidationError as exc:
+            errors[key]["value"] = exc.messages
+    if errors:
+        raise ma.ValidationError(errors)
+
+
 def _embedded_document_io_validate(field, value):
     if not value:
         return
@@ -333,6 +350,8 @@ class PyMongoBuilder(BaseBuilder):
                 field.io_validate = [validators]
         if isinstance(field, ListField):
             field.io_validate_recursive = _list_io_validate
+        if isinstance(field, DictField):
+            field.io_validate_recursive = _dict_io_validate
         if isinstance(field, ReferenceField):
             field.io_validate.append(_reference_io_validate)
             field.reference_cls = PyMongoReference
diff --git a/umongo/frameworks/txmongo.py b/umongo/frameworks/txmongo.py
index 408d173..6905ed6 100644
--- a/umongo/frameworks/txmongo.py
+++ b/umongo/frameworks/txmongo.py
@@ -10,7 +10,7 @@ from ..instance import Instance
 from ..document import DocumentImplementation
 from ..data_objects import Reference
 from ..exceptions import NotCreatedError, UpdateError, DeleteError, NoneReferenceError
-from ..fields import ReferenceField, ListField, EmbeddedField
+from ..fields import ReferenceField, ListField, DictField, EmbeddedField
 from ..query_mapper import map_query
 
 from .tools import cook_find_filter, remove_cls_field_from_embedded_docs
@@ -216,14 +216,17 @@ class TxMongoDocument(DocumentImplementation):
             yield cls.collection.create_index(index, **kwargs)
 
 
-def _errback_factory(errors, field=None):
+def _errback_factory(errors, field=None, subkey=None):
 
     def errback(err):
         if isinstance(err.value, ma.ValidationError):
+            error = err.value.messages
+            if subkey is not None:
+                error = {subkey: error}
             if field is not None:
-                errors[field] = err.value.messages
+                errors[field] = error
             else:
-                errors.extend(err.value.messages)
+                errors.extend(error)
         else:
             raise err.value
 
@@ -299,6 +302,24 @@ def _list_io_validate(field, value):
         raise ma.ValidationError(errors)
 
 
+@inlineCallbacks
+def _dict_io_validate(field, value):
+    if not value or not field.value_field:
+        return
+    validators = field.value_field.io_validate
+    if not validators:
+        return
+    errors = {}
+    defers = []
+    for key, exc in value.items():
+        defer = _run_validators(validators, field.value_field, exc)
+        defer.addErrback(_errback_factory(errors, key, subkey="value"))
+        defers.append(defer)
+    yield DeferredList(defers)
+    if errors:
+        raise ma.ValidationError(errors)
+
+
 def _embedded_document_io_validate(field, value):
     if not value:
         return
@@ -341,6 +362,8 @@ class TxMongoBuilder(BaseBuilder):
             field.io_validate = validators
         if isinstance(field, ListField):
             field.io_validate_recursive = _list_io_validate
+        if isinstance(field, DictField):
+            field.io_validate_recursive = _dict_io_validate
         if isinstance(field, ReferenceField):
             field.io_validate.append(_reference_io_validate)
             field.reference_cls = TxMongoReference


Test patch:
diff --git a/tests/frameworks/test_motor_asyncio.py b/tests/frameworks/test_motor_asyncio.py
index 9a06fda..e5b158b 100644
--- a/tests/frameworks/test_motor_asyncio.py
+++ b/tests/frameworks/test_motor_asyncio.py
@@ -394,6 +394,10 @@ class TestMotorAsyncIO(BaseDBTest):
             class IOStudent(Student):
                 io_field = fields.StrField(io_validate=io_validate)
                 list_io_field = fields.ListField(fields.IntField(io_validate=io_validate))
+                dict_io_field = fields.DictField(
+                    fields.StrField(),
+                    fields.IntField(io_validate=io_validate),
+                )
                 reference_io_field = fields.ReferenceField(
                     classroom_model.Course, io_validate=io_validate)
                 embedded_io_field = fields.EmbeddedField(EmbeddedDoc, io_validate=io_validate)
@@ -403,6 +407,7 @@ class TestMotorAsyncIO(BaseDBTest):
                 name='Marty',
                 io_field='io?',
                 list_io_field=[1, 2],
+                dict_io_field={"1": 1, "2": 2},
                 reference_io_field=bad_reference,
                 embedded_io_field={'io_field': 42}
             )
@@ -411,6 +416,7 @@ class TestMotorAsyncIO(BaseDBTest):
             assert exc.value.messages == {
                 'io_field': ['Ho boys !'],
                 'list_io_field': {0: ['Ho boys !'], 1: ['Ho boys !']},
+                'dict_io_field': {"1": {"value": ['Ho boys !']}, "2": {"value": ['Ho boys !']}},
                 'reference_io_field': ['Ho boys !', 'Reference not found for document Course.'],
                 'embedded_io_field': {'io_field': ['Ho boys !']}
             }
@@ -495,6 +501,37 @@ class TestMotorAsyncIO(BaseDBTest):
 
         loop.run_until_complete(do_test())
 
+    def test_io_validate_dict(self, loop, instance, classroom_model):
+
+        async def do_test():
+
+            Student = classroom_model.Student
+            called = []
+            keys = ["1", "2", "3", "4"]
+            values = [1, 2, 3, 4]
+
+            def io_validate(field, value):
+                called.append(value)
+
+            @instance.register
+            class IOStudent(Student):
+                io_field = fields.DictField(
+                    fields.StrField(),
+                    fields.IntField(io_validate=io_validate),
+                    allow_none=True
+                )
+
+            student = IOStudent(name='Marty', io_field=dict(zip(keys, values)))
+            await student.io_validate()
+            assert called == values
+
+            student.io_field = None
+            await student.io_validate()
+            del student.io_field
+            await student.io_validate()
+
+        loop.run_until_complete(do_test())
+
     def test_io_validate_embedded(self, loop, instance, classroom_model):
         Student = classroom_model.Student
 
diff --git a/tests/frameworks/test_pymongo.py b/tests/frameworks/test_pymongo.py
index 100279d..9a27761 100644
--- a/tests/frameworks/test_pymongo.py
+++ b/tests/frameworks/test_pymongo.py
@@ -287,6 +287,10 @@ class TestPymongo(BaseDBTest):
         class IOStudent(Student):
             io_field = fields.StrField(io_validate=io_validate)
             list_io_field = fields.ListField(fields.IntField(io_validate=io_validate))
+            dict_io_field = fields.DictField(
+                fields.StrField(),
+                fields.IntField(io_validate=io_validate),
+            )
             reference_io_field = fields.ReferenceField(
                 classroom_model.Course, io_validate=io_validate)
             embedded_io_field = fields.EmbeddedField(EmbeddedDoc, io_validate=io_validate)
@@ -296,6 +300,7 @@ class TestPymongo(BaseDBTest):
             name='Marty',
             io_field='io?',
             list_io_field=[1, 2],
+            dict_io_field={"1": 1, "2": 2},
             reference_io_field=bad_reference,
             embedded_io_field={'io_field': 42}
         )
@@ -304,6 +309,7 @@ class TestPymongo(BaseDBTest):
         assert exc.value.messages == {
             'io_field': ['Ho boys !'],
             'list_io_field': {0: ['Ho boys !'], 1: ['Ho boys !']},
+            'dict_io_field': {"1": {"value": ['Ho boys !']}, "2": {"value": ['Ho boys !']}},
             'reference_io_field': ['Ho boys !', 'Reference not found for document Course.'],
             'embedded_io_field': {'io_field': ['Ho boys !']}
         }
@@ -347,6 +353,32 @@ class TestPymongo(BaseDBTest):
         del student.io_field
         student.io_validate()
 
+    def test_io_validate_dict(self, instance, classroom_model):
+        Student = classroom_model.Student
+        called = []
+        keys = ["1", "2", "3", "4"]
+        values = [1, 2, 3, 4]
+
+        def io_validate(field, value):
+            called.append(value)
+
+        @instance.register
+        class IOStudent(Student):
+            io_field = fields.DictField(
+                fields.StrField(),
+                fields.IntField(io_validate=io_validate),
+                allow_none=True
+            )
+
+        student = IOStudent(name='Marty', io_field=dict(zip(keys, values)))
+        student.io_validate()
+        assert called == values
+
+        student.io_field = None
+        student.io_validate()
+        del student.io_field
+        student.io_validate()
+
     def test_io_validate_embedded(self, instance, classroom_model):
         Student = classroom_model.Student
 
diff --git a/tests/frameworks/test_txmongo.py b/tests/frameworks/test_txmongo.py
index a829d44..c342aa4 100644
--- a/tests/frameworks/test_txmongo.py
+++ b/tests/frameworks/test_txmongo.py
@@ -344,6 +344,10 @@ class TestTxMongo(BaseDBTest):
         class IOStudent(Student):
             io_field = fields.StrField(io_validate=io_validate)
             list_io_field = fields.ListField(fields.IntField(io_validate=io_validate))
+            dict_io_field = fields.DictField(
+                fields.StrField(),
+                fields.IntField(io_validate=io_validate),
+            )
             reference_io_field = fields.ReferenceField(
                 classroom_model.Course, io_validate=io_validate)
             embedded_io_field = fields.EmbeddedField(EmbeddedDoc, io_validate=io_validate)
@@ -353,6 +357,7 @@ class TestTxMongo(BaseDBTest):
             name='Marty',
             io_field='io?',
             list_io_field=[1, 2],
+            dict_io_field={"1": 1, "2": 2},
             reference_io_field=bad_reference,
             embedded_io_field={'io_field': 42}
         )
@@ -361,6 +366,7 @@ class TestTxMongo(BaseDBTest):
         assert exc.value.messages == {
             'io_field': ['Ho boys !'],
             'list_io_field': {0: ['Ho boys !'], 1: ['Ho boys !']},
+            'dict_io_field': {"1": {"value": ['Ho boys !']}, "2": {"value": ['Ho boys !']}},
             'reference_io_field': ['Ho boys !', 'Reference not found for document Course.'],
             'embedded_io_field': {'io_field': ['Ho boys !']}
         }
@@ -427,6 +433,33 @@ class TestTxMongo(BaseDBTest):
         yield student.io_validate()
         assert called == values
 
+    @pytest_inlineCallbacks
+    def test_io_validate_dict(self, instance, classroom_model):
+        Student = classroom_model.Student
+        called = []
+        keys = ["1", "2", "3", "4"]
+        values = [1, 2, 3, 4]
+
+        def io_validate(field, value):
+            called.append(value)
+
+        @instance.register
+        class IOStudent(Student):
+            io_field = fields.DictField(
+                fields.StrField(),
+                fields.IntField(io_validate=io_validate),
+                allow_none=True
+            )
+
+        student = IOStudent(name='Marty', io_field=dict(zip(keys, values)))
+        yield student.io_validate()
+        assert called == values
+
+        student.io_field = None
+        yield student.io_validate()
+        del student.io_field
+        yield student.io_validate()
+
     @pytest_inlineCallbacks
     def test_io_validate_embedded(self, instance, classroom_model):
         Student = classroom_model.Student