Repository: MongoEngine/mongoengine

Issue:
Proper ValidationError when trying to save a single doc as a list of EmbeddedDocuments
```
In [8]: class EmbeddedDoc(EmbeddedDocument):
   ...:     name = StringField()
   ...:

In [9]: class Doc(Document):
   ...:     docs = ListField(EmbeddedDocumentField(EmbeddedDoc))
   ...:

In [10]: Doc.objects.create(docs=[EmbeddedDoc(name='whatever')])  # works fine
Out[10]: <Doc: Doc object>

In [11]: Doc.objects.create(docs=EmbeddedDoc(name='whatever'))  # uh oh, assigning a single doc to a list?!
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-11-226358097c9d> in <module>()
----> 1 Doc.objects.create(docs=EmbeddedDoc(name='whatever'))  # uh oh, assigning a single doc to a list?!

/Users/wojcikstefan/Repos/temp/mongoengine/mongoengine/queryset/base.pyc in create(self, **kwargs)
    286         .. versionadded:: 0.4
    287         """
--> 288         return self._document(**kwargs).save()
    289
    290     def first(self):

/Users/wojcikstefan/Repos/temp/mongoengine/mongoengine/base/document.pyc in __init__(self, *args, **values)
    113                         field = self._fields.get(key)
    114                         if field and not isinstance(field, FileField):
--> 115                             value = field.to_python(value)
    116                     setattr(self, key, value)
    117                 else:

/Users/wojcikstefan/Repos/temp/mongoengine/mongoengine/base/fields.pyc in to_python(self, value)
    315             self.field._auto_dereference = self._auto_dereference
    316             value_dict = {key: self.field.to_python(item)
--> 317                           for key, item in value.items()}
    318         else:
    319             Document = _import_class('Document')

/Users/wojcikstefan/Repos/temp/mongoengine/mongoengine/base/fields.pyc in <dictcomp>((key, item))
    315             self.field._auto_dereference = self._auto_dereference
    316             value_dict = {key: self.field.to_python(item)
--> 317                           for key, item in value.items()}
    318         else:
    319             Document = _import_class('Document')

/Users/wojcikstefan/Repos/temp/mongoengine/mongoengine/fields.pyc in to_python(self, value)
    544     def to_python(self, value):
    545         if not isinstance(value, self.document_type):
--> 546             return self.document_type._from_son(value, _auto_dereference=self._auto_dereference)
    547         return value
    548

/Users/wojcikstefan/Repos/temp/mongoengine/mongoengine/base/document.pyc in _from_son(cls, son, _auto_dereference, only_fields, created)
    679
    680         if son and not isinstance(son, dict):
--> 681             raise ValueError("The source SON object needs to be of type 'dict'")
    682
    683         # Get the class name from the document, falling back to the given

ValueError: The source SON object needs to be of type 'dict'
```

`ValueError: The source SON object needs to be of type 'dict'` isn't very helpful when debugging this subtle issue.

With simple types it works as expected, raising a `ValidationError` with a clear message:
```
In [2]: class Doc(Document):
   ...:     lst = ListField(StringField())
   ...:

In [5]: Doc.objects.create(lst='1')
(...)
ValidationError: ValidationError (Doc:None) (Only lists and tuples may be used in a list field: ['lst'])
```

Thanks to #1460 for exposing it.


Discussion:

Resolving patch:
diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py
--- a/mongoengine/base/fields.py
+++ b/mongoengine/base/fields.py
@@ -313,11 +313,16 @@ def to_python(self, value):
         if hasattr(value, 'to_python'):
             return value.to_python()
 
+        BaseDocument = _import_class('BaseDocument')
+        if isinstance(value, BaseDocument):
+            # Something is wrong, return the value as it is
+            return value
+
         is_list = False
         if not hasattr(value, 'items'):
             try:
                 is_list = True
-                value = {k: v for k, v in enumerate(value)}
+                value = {idx: v for idx, v in enumerate(value)}
             except TypeError:  # Not iterable return the value
                 return value
 


Test patch:
diff --git a/tests/fields/fields.py b/tests/fields/fields.py
--- a/tests/fields/fields.py
+++ b/tests/fields/fields.py
@@ -3868,7 +3868,7 @@ class ToEmbedChild(ToEmbedParent):
         assert isinstance(doc.field, ToEmbedChild)
         assert doc.field == to_embed_child
 
-    def test_invalid_dict_value(self):
+    def test_dict_field_invalid_dict_value(self):
         class DictFieldTest(Document):
             dictionary = DictField(required=True)
 
@@ -3882,6 +3882,22 @@ class DictFieldTest(Document):
         test.dictionary  # Just access to test getter
         self.assertRaises(ValidationError, test.validate)
 
+    def test_dict_field_raises_validation_error_if_wrongly_assign_embedded_doc(self):
+        class DictFieldTest(Document):
+            dictionary = DictField(required=True)
+
+        DictFieldTest.drop_collection()
+
+        class Embedded(EmbeddedDocument):
+            name = StringField()
+
+        embed = Embedded(name='garbage')
+        doc = DictFieldTest(dictionary=embed)
+        with self.assertRaises(ValidationError) as ctx_err:
+            doc.validate()
+        self.assertIn("'dictionary'", str(ctx_err.exception))
+        self.assertIn('Only dictionaries may be used in a DictField', str(ctx_err.exception))
+
     def test_cls_field(self):
         class Animal(Document):
             meta = {'allow_inheritance': True}
@@ -3980,6 +3996,28 @@ class BlogPost(Document):
             self.Comments(author='user3', message='message1')
         ]).save()
 
+    def test_fails_upon_validate_if_provide_a_doc_instead_of_a_list_of_doc(self):
+        # Relates to Issue #1464
+        comment = self.Comments(author='John')
+
+        class Title(Document):
+            content = StringField()
+
+        # Test with an embeddedDocument instead of a list(embeddedDocument)
+        # It's an edge case but it used to fail with a vague error, making it difficult to troubleshoot it
+        post = self.BlogPost(comments=comment)
+        with self.assertRaises(ValidationError) as ctx_err:
+            post.validate()
+        self.assertIn("'comments'", str(ctx_err.exception))
+        self.assertIn('Only lists and tuples may be used in a list field', str(ctx_err.exception))
+
+        # Test with a Document
+        post = self.BlogPost(comments=Title(content='garbage'))
+        with self.assertRaises(ValidationError) as e:
+            post.validate()
+        self.assertIn("'comments'", str(ctx_err.exception))
+        self.assertIn('Only lists and tuples may be used in a list field', str(ctx_err.exception))
+
     def test_no_keyword_filter(self):
         """
         Tests the filter method of a List of Embedded Documents