Repository: MongoEngine/mongoengine

Issue:
_changed_fields does not detect changes to first item of ListField properly
Just figured out the following. This is harmless but it means that mongoengine will save the whole list field every time the first item is updated

```
class Doc(Document):
    listfield = ListField(StringField())

doc = Doc(listfield=['a', 'b']).save()
doc.listfield[1] = 'new1'
print(doc._changed_fields)    # ['listfield.1']

doc.reload()
doc.listfield[0] = 'new0'
print(doc._changed_fields)    # ['listfield'] <-- should be listfield.0
```



Discussion:

Resolving patch:
diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py
--- a/mongoengine/base/datastructures.py
+++ b/mongoengine/base/datastructures.py
@@ -179,7 +179,7 @@ def __setitem__(self, key, value):
 
     def _mark_as_changed(self, key=None):
         if hasattr(self._instance, "_mark_as_changed"):
-            if key:
+            if key is not None:
                 self._instance._mark_as_changed(
                     "{}.{}".format(self._name, key % len(self))
                 )


Test patch:
diff --git a/tests/document/test_delta.py b/tests/document/test_delta.py
--- a/tests/document/test_delta.py
+++ b/tests/document/test_delta.py
@@ -29,7 +29,8 @@ def test_delta(self):
         self.delta(Document)
         self.delta(DynamicDocument)
 
-    def delta(self, DocClass):
+    @staticmethod
+    def delta(DocClass):
         class Doc(DocClass):
             string_field = StringField()
             int_field = IntField()
@@ -428,13 +429,20 @@ class Doc(DocClass):
         assert doc.dict_field == {"hello": "world"}
         assert doc.list_field == ["1", 2, {"hello": "world"}]
 
-    def test_delta_recursive_db_field(self):
+    def test_delta_recursive_db_field_on_doc_and_embeddeddoc(self):
         self.delta_recursive_db_field(Document, EmbeddedDocument)
+
+    def test_delta_recursive_db_field_on_doc_and_dynamicembeddeddoc(self):
         self.delta_recursive_db_field(Document, DynamicEmbeddedDocument)
+
+    def test_delta_recursive_db_field_on_dynamicdoc_and_embeddeddoc(self):
         self.delta_recursive_db_field(DynamicDocument, EmbeddedDocument)
+
+    def test_delta_recursive_db_field_on_dynamicdoc_and_dynamicembeddeddoc(self):
         self.delta_recursive_db_field(DynamicDocument, DynamicEmbeddedDocument)
 
-    def delta_recursive_db_field(self, DocClass, EmbeddedClass):
+    @staticmethod
+    def delta_recursive_db_field(DocClass, EmbeddedClass):
         class Embedded(EmbeddedClass):
             string_field = StringField(db_field="db_string_field")
             int_field = IntField(db_field="db_int_field")
@@ -487,6 +495,7 @@ class Doc(DocClass):
         doc = doc.reload(10)
         assert doc.embedded_field.dict_field == {}
 
+        assert doc._get_changed_fields() == []
         doc.embedded_field.list_field = []
         assert doc._get_changed_fields() == ["db_embedded_field.db_list_field"]
         assert doc.embedded_field._delta() == ({}, {"db_list_field": 1})
@@ -634,6 +643,7 @@ class Doc(DocClass):
         doc.save()
         doc = doc.reload(10)
 
+        assert doc._delta() == ({}, {},)
         del doc.embedded_field.list_field[2].list_field
         assert doc._delta() == (
             {},
@@ -732,12 +742,12 @@ def test_delta_with_dbref_true(self):
         assert organization._get_changed_fields() == []
 
         updates, removals = organization._delta()
-        assert {} == removals
-        assert {} == updates
+        assert removals == {}
+        assert updates == {}
 
         organization.employees.append(person)
         updates, removals = organization._delta()
-        assert {} == removals
+        assert removals == {}
         assert "employees" in updates
 
     def test_delta_with_dbref_false(self):
@@ -749,12 +759,12 @@ def test_delta_with_dbref_false(self):
         assert organization._get_changed_fields() == []
 
         updates, removals = organization._delta()
-        assert {} == removals
-        assert {} == updates
+        assert removals == {}
+        assert updates == {}
 
         organization.employees.append(person)
         updates, removals = organization._delta()
-        assert {} == removals
+        assert removals == {}
         assert "employees" in updates
 
     def test_nested_nested_fields_mark_as_changed(self):
@@ -775,11 +785,11 @@ class MyDoc(Document):
         subdoc = mydoc.subs["a"]["b"]
         subdoc.name = "bar"
 
-        assert ["name"] == subdoc._get_changed_fields()
-        assert ["subs.a.b.name"] == mydoc._get_changed_fields()
+        assert subdoc._get_changed_fields() == ["name"]
+        assert mydoc._get_changed_fields() == ["subs.a.b.name"]
 
         mydoc._clear_changed_fields()
-        assert [] == mydoc._get_changed_fields()
+        assert mydoc._get_changed_fields() == []
 
     def test_lower_level_mark_as_changed(self):
         class EmbeddedDoc(EmbeddedDocument):
@@ -794,17 +804,17 @@ class MyDoc(Document):
 
         mydoc = MyDoc.objects.first()
         mydoc.subs["a"] = EmbeddedDoc()
-        assert ["subs.a"] == mydoc._get_changed_fields()
+        assert mydoc._get_changed_fields() == ["subs.a"]
 
         subdoc = mydoc.subs["a"]
         subdoc.name = "bar"
 
-        assert ["name"] == subdoc._get_changed_fields()
-        assert ["subs.a"] == mydoc._get_changed_fields()
+        assert subdoc._get_changed_fields() == ["name"]
+        assert mydoc._get_changed_fields() == ["subs.a"]
         mydoc.save()
 
         mydoc._clear_changed_fields()
-        assert [] == mydoc._get_changed_fields()
+        assert mydoc._get_changed_fields() == []
 
     def test_upper_level_mark_as_changed(self):
         class EmbeddedDoc(EmbeddedDocument):
@@ -821,15 +831,15 @@ class MyDoc(Document):
         subdoc = mydoc.subs["a"]
         subdoc.name = "bar"
 
-        assert ["name"] == subdoc._get_changed_fields()
-        assert ["subs.a.name"] == mydoc._get_changed_fields()
+        assert subdoc._get_changed_fields() == ["name"]
+        assert mydoc._get_changed_fields() == ["subs.a.name"]
 
         mydoc.subs["a"] = EmbeddedDoc()
-        assert ["subs.a"] == mydoc._get_changed_fields()
+        assert mydoc._get_changed_fields() == ["subs.a"]
         mydoc.save()
 
         mydoc._clear_changed_fields()
-        assert [] == mydoc._get_changed_fields()
+        assert mydoc._get_changed_fields() == []
 
     def test_referenced_object_changed_attributes(self):
         """Ensures that when you save a new reference to a field, the referenced object isn't altered"""
diff --git a/tests/document/test_instance.py b/tests/document/test_instance.py
--- a/tests/document/test_instance.py
+++ b/tests/document/test_instance.py
@@ -188,7 +188,7 @@ def __str__(self):
 
     def test_queryset_resurrects_dropped_collection(self):
         self.Person.drop_collection()
-        assert [] == list(self.Person.objects())
+        assert list(self.Person.objects()) == []
 
         # Ensure works correctly with inhertited classes
         class Actor(self.Person):
@@ -196,7 +196,7 @@ class Actor(self.Person):
 
         Actor.objects()
         self.Person.drop_collection()
-        assert [] == list(Actor.objects())
+        assert list(Actor.objects()) == []
 
     def test_polymorphic_references(self):
         """Ensure that the correct subclasses are returned from a query
@@ -578,7 +578,8 @@ class Doc(Document):
         doc.embedded_field.list_field.append(1)
         doc.embedded_field.dict_field["woot"] = "woot"
 
-        assert doc._get_changed_fields() == [
+        changed = doc._get_changed_fields()
+        assert changed == [
             "list_field",
             "dict_field.woot",
             "embedded_field.list_field",
diff --git a/tests/fields/test_dict_field.py b/tests/fields/test_dict_field.py
--- a/tests/fields/test_dict_field.py
+++ b/tests/fields/test_dict_field.py
@@ -113,7 +113,7 @@ class BlogPost(Document):
         post.info.setdefault("authors", [])
         post.save()
         post.reload()
-        assert [] == post.info["authors"]
+        assert post.info["authors"] == []
 
     def test_dictfield_dump_document(self):
         """Ensure a DictField can handle another document's dump."""
diff --git a/tests/fields/test_fields.py b/tests/fields/test_fields.py
--- a/tests/fields/test_fields.py
+++ b/tests/fields/test_fields.py
@@ -1084,7 +1084,7 @@ class Simple(Document):
 
         e = Simple().save()
         e.mapping = []
-        assert [] == e._changed_fields
+        assert e._changed_fields == []
 
         class Simple(Document):
             mapping = DictField()
@@ -1093,7 +1093,7 @@ class Simple(Document):
 
         e = Simple().save()
         e.mapping = {}
-        assert [] == e._changed_fields
+        assert e._changed_fields == []
 
     def test_slice_marks_field_as_changed(self):
         class Simple(Document):
diff --git a/tests/fields/test_geo_fields.py b/tests/fields/test_geo_fields.py
--- a/tests/fields/test_geo_fields.py
+++ b/tests/fields/test_geo_fields.py
@@ -381,7 +381,7 @@ class Log(Document):
 
             meta = {"indexes": [[("location", "2dsphere"), ("datetime", 1)]]}
 
-        assert [] == Log._geo_indices()
+        assert Log._geo_indices() == []
 
         Log.drop_collection()
         Log.ensure_indexes()
@@ -401,7 +401,7 @@ class Log(Document):
                 "indexes": [{"fields": [("location", "2dsphere"), ("datetime", 1)]}]
             }
 
-        assert [] == Log._geo_indices()
+        assert Log._geo_indices() == []
 
         Log.drop_collection()
         Log.ensure_indexes()
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -9,10 +9,14 @@
 class DocumentStub(object):
     def __init__(self):
         self._changed_fields = []
+        self._unset_fields = []
 
     def _mark_as_changed(self, key):
         self._changed_fields.append(key)
 
+    def _mark_as_unset(self, key):
+        self._unset_fields.append(key)
+
 
 class TestBaseDict:
     @staticmethod
@@ -314,7 +318,7 @@ def test___setitem___with_slice(self):
     def test___setitem___item_0_calls_mark_as_changed(self):
         base_list = self._get_baselist([True])
         base_list[0] = False
-        assert base_list._instance._changed_fields == ["my_name"]
+        assert base_list._instance._changed_fields == ["my_name.0"]
         assert base_list == [False]
 
     def test___setitem___item_1_calls_mark_as_changed(self):