Repository: MongoEngine/mongoengine

Issue:
bug in ListField + extend method?
- MongoEngine 0.8.7 on Mac + Python 2.7

tags = ListField(StringField())

```
# I am having problem with this
more_tags = ['a', 'b']
self.tags += more_tags
self.save()

# This works
more_tags = ['a', 'b']
for tag in more_tags:
    self.tags.append(tag)
self.save()
```

Any ideas?

bug in ListField + extend method?
- MongoEngine 0.8.7 on Mac + Python 2.7

tags = ListField(StringField())

```
# I am having problem with this
more_tags = ['a', 'b']
self.tags += more_tags
self.save()

# This works
more_tags = ['a', 'b']
for tag in more_tags:
    self.tags.append(tag)
self.save()
```

Any ideas?



Discussion:
It appears that DictField has similar issues with the method setdefault.

I believe the attribute "please update this field" is not setted using these methods.

I just checked BaseList class which is inherited by class list, a basic
class in python. (mongoengine/base/datastructures.py)
and I found out there is no def **add** method.
I think **add** method must be implemented as I wrote below to use +
operand.

```
def __add__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self). __add__(*args, **kwargs)
```

---

class BaseList(list):
    """A special list so we can watch any changes
    """

```
_dereferenced = False
_instance = None
_name = None

def __init__(self, list_items, instance, name):
    Document = _import_class('Document')
    EmbeddedDocument = _import_class('EmbeddedDocument')

    if isinstance(instance, (Document, EmbeddedDocument)):
        self._instance = weakref.proxy(instance)
    self._name = name
    return super(BaseList, self).__init__(list_items)

def __getitem__(self, *args, **kwargs):
    value = super(BaseList, self).__getitem__(*args, **kwargs)

    EmbeddedDocument = _import_class('EmbeddedDocument')
    if isinstance(value, EmbeddedDocument) and value._instance is None:
        value._instance = self._instance
    return value

def __setitem__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__setitem__(*args, **kwargs)

def __delitem__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__delitem__(*args, **kwargs)

def __setslice__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__setslice__(*args, **kwargs)

def __delslice__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__delslice__(*args, **kwargs)

def __getstate__(self):
    self.instance = None
    self._dereferenced = False
    return self

def __setstate__(self, state):
    self = state
    return self

def append(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).append(*args, **kwargs)

def extend(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).extend(*args, **kwargs)

def insert(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).insert(*args, **kwargs)

def pop(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).pop(*args, **kwargs)

def remove(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).remove(*args, **kwargs)

def reverse(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).reverse(*args, **kwargs)

def sort(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).sort(*args, **kwargs)

def _mark_as_changed(self):
    if hasattr(self._instance, '_mark_as_changed'):
        self._instance._mark_as_changed(self._name)
```

---

It appears that DictField has similar issues with the method setdefault.

I believe the attribute "please update this field" is not setted using these methods.

I just checked BaseList class which is inherited by class list, a basic
class in python. (mongoengine/base/datastructures.py)
and I found out there is no def **add** method.
I think **add** method must be implemented as I wrote below to use +
operand.

```
def __add__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self). __add__(*args, **kwargs)
```

---

class BaseList(list):
    """A special list so we can watch any changes
    """

```
_dereferenced = False
_instance = None
_name = None

def __init__(self, list_items, instance, name):
    Document = _import_class('Document')
    EmbeddedDocument = _import_class('EmbeddedDocument')

    if isinstance(instance, (Document, EmbeddedDocument)):
        self._instance = weakref.proxy(instance)
    self._name = name
    return super(BaseList, self).__init__(list_items)

def __getitem__(self, *args, **kwargs):
    value = super(BaseList, self).__getitem__(*args, **kwargs)

    EmbeddedDocument = _import_class('EmbeddedDocument')
    if isinstance(value, EmbeddedDocument) and value._instance is None:
        value._instance = self._instance
    return value

def __setitem__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__setitem__(*args, **kwargs)

def __delitem__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__delitem__(*args, **kwargs)

def __setslice__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__setslice__(*args, **kwargs)

def __delslice__(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).__delslice__(*args, **kwargs)

def __getstate__(self):
    self.instance = None
    self._dereferenced = False
    return self

def __setstate__(self, state):
    self = state
    return self

def append(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).append(*args, **kwargs)

def extend(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).extend(*args, **kwargs)

def insert(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).insert(*args, **kwargs)

def pop(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).pop(*args, **kwargs)

def remove(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).remove(*args, **kwargs)

def reverse(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).reverse(*args, **kwargs)

def sort(self, *args, **kwargs):
    self._mark_as_changed()
    return super(BaseList, self).sort(*args, **kwargs)

def _mark_as_changed(self):
    if hasattr(self._instance, '_mark_as_changed'):
        self._instance._mark_as_changed(self._name)
```

---


Resolving patch:
diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py
--- a/mongoengine/base/datastructures.py
+++ b/mongoengine/base/datastructures.py
@@ -156,6 +156,14 @@ def __setstate__(self, state):
         self = state
         return self
 
+    def __iadd__(self, other):
+        self._mark_as_changed()
+        return super(BaseList, self).__iadd__(other)
+
+    def __imul__(self, other):
+        self._mark_as_changed()
+        return super(BaseList, self).__imul__(other)
+
     def append(self, *args, **kwargs):
         self._mark_as_changed()
         return super(BaseList, self).append(*args, **kwargs)


Test patch:
diff --git a/tests/fields/fields.py b/tests/fields/fields.py
--- a/tests/fields/fields.py
+++ b/tests/fields/fields.py
@@ -946,6 +946,18 @@ class BlogPost(Document):
             BlogPost.objects.filter(info__0__test__exact='5').count(), 0)
         self.assertEqual(
             BlogPost.objects.filter(info__100__test__exact='test').count(), 0)
+
+        post = BlogPost()
+        post.info = ['1', '2']
+        post.save()
+        post = BlogPost.objects(info=['1', '2']).get()
+        post.info += ['3', '4']
+        post.save()
+        self.assertEqual(BlogPost.objects(info=['1', '2', '3', '4']).count(), 1)
+        post = BlogPost.objects(info=['1', '2', '3', '4']).get()
+        post.info *= 2
+        post.save()
+        self.assertEqual(BlogPost.objects(info=['1', '2', '3', '4', '1', '2', '3', '4']).count(), 1)
         BlogPost.drop_collection()
 
     def test_list_field_passed_in_value(self):