From a7edd8602cd38f6ad39fd274e3cbb8a737d7d1b9 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 21 Sep 2011 01:42:52 -0700 Subject: [PATCH] Added support for expando style dynamic documents. Added two new classes: DynamicDocument and DynamicEmbeddedDocument for handling expando style setting of attributes. [closes #112] --- docs/apireference.rst | 6 + docs/changelog.rst | 5 + docs/guide/defining-documents.rst | 28 ++ docs/upgrade.rst | 5 + mongoengine/base.py | 148 +++++++++-- mongoengine/document.py | 47 +++- mongoengine/queryset.py | 10 +- tests/dynamic_document.py | 413 ++++++++++++++++++++++++++++++ 8 files changed, 641 insertions(+), 21 deletions(-) create mode 100644 tests/dynamic_document.py diff --git a/docs/apireference.rst b/docs/apireference.rst index 57472dc8..932152fe 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -21,6 +21,12 @@ Documents .. autoclass:: mongoengine.EmbeddedDocument :members: +.. autoclass:: mongoengine.DynamicDocument + :members: + +.. autoclass:: mongoengine.DynamicEmbeddedDocument + :members: + .. autoclass:: mongoengine.document.MapReduceDocument :members: diff --git a/docs/changelog.rst b/docs/changelog.rst index b7904bdc..904eb200 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,11 @@ Changelog ========= +Changes in dev +============== + +- Added DynamicDocument and EmbeddedDynamicDocument classes for expando schemas + Changes in v0.5 =============== diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index fd005e40..6367d95a 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -24,6 +24,34 @@ objects** as class attributes to the document class:: title = StringField(max_length=200, required=True) date_modified = DateTimeField(default=datetime.datetime.now) +Dynamic document schemas +======================== +One of the benefits of MongoDb is dynamic schemas for a collection, whilst data +should be planned and organised (after all explicit is better than implicit!) +there are scenarios where having dynamic / expando style documents is desirable. + +:class:`~mongoengine.DynamicDocument` documents work in the same way as +:class:`~mongoengine.Document` but any data / attributes set to them will also +be saved :: + + from mongoengine import * + + class Page(DynamicDocument): + title = StringField(max_length=200, required=True) + + # Create a new page and add tags + >>> page = Page(title='Using MongoEngine') + >>> page.tags = ['mongodb', 'mongoengine'] + >>> page.save() + + >>> Page.objects(tags='mongoengine').count() + >>> 1 + +..note:: + + There is one caveat on Dynamic Documents: fields cannot start with `_` + + Fields ====== By default, fields are not required. To make a field mandatory, set the diff --git a/docs/upgrade.rst b/docs/upgrade.rst index c684c1ad..671b9301 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -2,6 +2,11 @@ Upgrading ========= +0.5 to 0.6 +========== + +TBC + 0.4 to 0.5 =========== diff --git a/mongoengine/base.py b/mongoengine/base.py index 02ad8bbf..40953f17 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -301,6 +301,40 @@ class ComplexBaseField(BaseField): owner_document = property(_get_owner_document, _set_owner_document) +class BaseDynamicField(BaseField): + """Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" + + def to_mongo(self, value): + """Convert a Python type to a MongoDBcompatible type. + """ + + if isinstance(value, basestring): + return value + + if hasattr(value, 'to_mongo'): + return value.to_mongo() + + if not isinstance(value, (dict, list, tuple)): + return value + + is_list = False + if not hasattr(value, 'items'): + is_list = True + value = dict([(k, v) for k, v in enumerate(value)]) + + data = {} + for k, v in value.items(): + data[k] = self.to_mongo(v) + + if is_list: # Convert back to a list + value = [v for k, v in sorted(data.items(), key=operator.itemgetter(0))] + else: + value = data + return value + + def lookup_member(self, member_name): + return member_name + class ObjectIdField(BaseField): """An field wrapper around MongoDB's ObjectIds. """ @@ -585,30 +619,98 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): class BaseDocument(object): + _dynamic = False + def __init__(self, **values): signals.pre_init.send(self.__class__, document=self, values=values) self._data = {} self._initialised = False + # Assign default values to instance for attr_name, field in self._fields.items(): value = getattr(self, attr_name, None) setattr(self, attr_name, value) - # Assign initial values to instance - for attr_name in values.keys(): - try: - value = values.pop(attr_name) - setattr(self, attr_name, value) - except AttributeError: - pass + # Set passed values after initialisation + if self._dynamic: + self._dynamic_fields = {} + dynamic_data = {} + for key, value in values.items(): + if key in self._fields or key == '_id': + setattr(self, key, value) + elif self._dynamic: + dynamic_data[key] = value + else: + for key, value in values.items(): + setattr(self, key, value) - # Set any get_fieldname_display methods + # Set any get_fieldname_display methodsF self.__set_field_display() # Flag initialised self._initialised = True + + if self._dynamic: + for key, value in dynamic_data.items(): + setattr(self, key, value) signals.post_init.send(self.__class__, document=self) + def __setattr__(self, name, value): + # Handle dynamic data only if an intialised dynamic document + if self._dynamic and getattr(self, '_initialised', False): + + field = None + if not hasattr(self, name) and not name.startswith('_'): + field = BaseDynamicField(db_field=name) + field.name = name + self._dynamic_fields[name] = field + + if not name.startswith('_'): + value = self.__expand_dynamic_values(name, value) + + # Handle marking data as changed + if name in self._dynamic_fields: + self._data[name] = value + if hasattr(self, '_changed_fields'): + self._mark_as_changed(name) + + super(BaseDocument, self).__setattr__(name, value) + + def __expand_dynamic_values(self, name, value): + """expand any dynamic values to their correct types / values""" + if not isinstance(value, (dict, list, tuple)): + return value + + is_list = False + if not hasattr(value, 'items'): + is_list = True + value = dict([(k, v) for k, v in enumerate(value)]) + + if not is_list and '_cls' in value: + cls = get_document(value['_cls']) + value = cls(**value) + value._dynamic = True + value._changed_fields = [] + return value + + data = {} + for k, v in value.items(): + key = name if is_list else k + data[k] = self.__expand_dynamic_values(key, v) + + if is_list: # Convert back to a list + value = [v for k, v in sorted(data.items(), key=operator.itemgetter(0))] + else: + value = data + + # Convert lists / values so we can watch for any changes on them + if isinstance(value, (list, tuple)) and not isinstance(value, BaseList): + value = BaseList(value, instance=self, name=name) + elif isinstance(value, dict) and not isinstance(value, BaseDict): + value = BaseDict(value, instance=self, name=name) + + return value + def validate(self): """Ensure that all fields' values are valid and that required fields are present. @@ -653,6 +755,12 @@ class BaseDocument(object): data['_types'] = self._superclasses.keys() + [self._class_name] if '_id' in data and data['_id'] is None: del data['_id'] + + if not self._dynamic: + return data + + for name, field in self._dynamic_fields.items(): + data[name] = field.to_mongo(self._data.get(name, None)) return data @classmethod @@ -727,14 +835,19 @@ class BaseDocument(object): def _get_changed_fields(self, key=''): """Returns a list of all fields that have explicitly been changed. """ - from mongoengine import EmbeddedDocument + from mongoengine import EmbeddedDocument, DynamicEmbeddedDocument _changed_fields = [] _changed_fields += getattr(self, '_changed_fields', []) - for field_name in self._fields: + + field_list = self._fields.copy() + if self._dynamic: + field_list.update(self._dynamic_fields) + + for field_name in field_list: db_field_name = self._db_field_map.get(field_name, field_name) key = '%s.' % db_field_name field = getattr(self, field_name, None) - if isinstance(field, EmbeddedDocument) and db_field_name not in _changed_fields: # Grab all embedded fields that have been changed + if isinstance(field, (EmbeddedDocument, DynamicEmbeddedDocument)) and db_field_name not in _changed_fields: # Grab all embedded fields that have been changed _changed_fields += ["%s%s" % (key, k) for k in field._get_changed_fields(key) if k] elif isinstance(field, (list, tuple, dict)) and db_field_name not in _changed_fields: # Loop list / dict fields as they contain documents # Determine the iterator to use @@ -747,7 +860,6 @@ class BaseDocument(object): continue list_key = "%s%s." % (key, index) _changed_fields += ["%s%s" % (list_key, k) for k in value._get_changed_fields(list_key) if k] - return _changed_fields def _delta(self): @@ -785,8 +897,11 @@ class BaseDocument(object): # If we've set a value that ain't the default value dont unset it. default = None - - if path in self._fields: + if self._dynamic and parts[0] in self._dynamic_fields: + del(set_data[path]) + unset_data[path] = 1 + continue + elif path in self._fields: default = self._fields[path].default else: # Perform a full lookup for lists / embedded lookups d = self @@ -805,7 +920,10 @@ class BaseDocument(object): field_name = d._reverse_db_field_map.get(db_field_name, db_field_name) - default = d._fields[field_name].default + if field_name in d._fields: + default = d._fields.get(field_name).default + else: + default = None if default is not None: if callable(default): diff --git a/mongoengine/document.py b/mongoengine/document.py index 3ccc4ddc..81c288ef 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,13 +1,14 @@ +import operator from mongoengine import signals from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, - ValidationError, BaseDict, BaseList) + ValidationError, BaseDict, BaseList, BaseDynamicField) from queryset import OperationError from connection import _get_db import pymongo -__all__ = ['Document', 'EmbeddedDocument', 'ValidationError', - 'OperationError', 'InvalidCollectionError'] +__all__ = ['Document', 'EmbeddedDocument', 'DynamicDocument', 'DynamicEmbeddedDocument', + 'ValidationError', 'OperationError', 'InvalidCollectionError'] class InvalidCollectionError(Exception): @@ -198,6 +199,7 @@ class Document(BaseDocument): reset_changed_fields(field, inspected_docs) reset_changed_fields(self) + self._changed_fields = [] signals.post_save.send(self.__class__, document=self, created=creation_mode) def update(self, **kwargs): @@ -247,8 +249,12 @@ class Document(BaseDocument): """ id_field = self._meta['id_field'] obj = self.__class__.objects(**{id_field: self[id_field]}).first() + for field in self._fields: setattr(self, field, self._reload(field, obj[field])) + if self._dynamic: + for name in self._dynamic_fields.keys(): + setattr(self, name, self._reload(name, obj._data[name])) self._changed_fields = [] def _reload(self, key, value): @@ -261,7 +267,7 @@ class Document(BaseDocument): elif isinstance(value, BaseList): value = [self._reload(key, v) for v in value] value = BaseList(value, instance=self, name=key) - elif isinstance(value, EmbeddedDocument): + elif isinstance(value, (EmbeddedDocument, DynamicEmbeddedDocument)): value._changed_fields = [] return value @@ -289,6 +295,39 @@ class Document(BaseDocument): db.drop_collection(cls._get_collection_name()) +class DynamicDocument(Document): + """A Dynamic Document class allowing flexible, expandable and uncontrolled + schemas. As a :class:`~mongoengine.Document` subclass, acts in the same + way as an ordinary document but has expando style properties. Any data + passed or set against the :class:`~mongoengine.DynamicDocument` that is + not a field is automatically converted into a + :class:`~mongoengine.BaseDynamicField` and data can be attributed to that + field. + + ..note:: + + There is one caveat on Dynamic Documents: fields cannot start with `_` + """ + __metaclass__ = TopLevelDocumentMetaclass + _dynamic = True + + +class DynamicEmbeddedDocument(EmbeddedDocument): + """A Dynamic Embedded Document class allowing flexible, expandable and + uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more + information about dynamic documents. + """ + + __metaclass__ = DocumentMetaclass + _dynamic = True + + def __delattr__(self, *args, **kwargs): + """Deletes the attribute by setting to None and allowing _delta to unset + it""" + field_name = args[0] + setattr(self, field_name, None) + + class MapReduceDocument(object): """A document returned from a map/reduce query. diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index a46581ef..2b44dc4b 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -590,7 +590,14 @@ class QuerySet(object): if field_name == 'pk': # Deal with "primary key" alias field_name = document._meta['id_field'] - field = document._fields[field_name] + if field_name in document._fields: + field = document._fields[field_name] + elif document._dynamic: + from base import BaseDynamicField + field = BaseDynamicField(db_field=field_name) + else: + raise InvalidQueryError('Cannot resolve field "%s"' + % field_name) else: # Look up subfield on the previous field new_field = field.lookup_member(field_name) @@ -603,7 +610,6 @@ class QuerySet(object): % field_name) field = new_field # update field to the new field type fields.append(field) - return fields @classmethod diff --git a/tests/dynamic_document.py b/tests/dynamic_document.py new file mode 100644 index 00000000..d76b196c --- /dev/null +++ b/tests/dynamic_document.py @@ -0,0 +1,413 @@ +import unittest + +from mongoengine import * +from mongoengine.connection import _get_db + +class DynamicDocTest(unittest.TestCase): + + def setUp(self): + connect(db='mongoenginetest') + self.db = _get_db() + + class Person(DynamicDocument): + name = StringField() + + Person.drop_collection() + + self.Person = Person + + def test_simple_dynamic_document(self): + """Ensures simple dynamic documents are saved correctly""" + + p = self.Person() + p.name = "James" + p.age = 34 + + self.assertEquals(p.to_mongo(), + {"_types": ["Person"], "_cls": "Person", + "name": "James", "age": 34} + ) + + p.save() + + self.assertEquals(self.Person.objects.first().age, 34) + + # Confirm no changes to self.Person + self.assertFalse(hasattr(self.Person, 'age')) + + def test_change_scope_of_variable(self): + """Test changing the scope of a dynamic field has no adverse effects""" + p = self.Person() + p.name = "Dean" + p.misc = 22 + p.save() + + p = self.Person.objects.get() + p.misc = {'hello': 'world'} + p.save() + + p = self.Person.objects.get() + self.assertEquals(p.misc, {'hello': 'world'}) + + def test_dynamic_document_queries(self): + """Ensure we can query dynamic fields""" + p = self.Person() + p.name = "Dean" + p.age = 22 + p.save() + + self.assertEquals(1, self.Person.objects(age=22).count()) + p = self.Person.objects(age=22) + p = p.get() + self.assertEquals(22, p.age) + + def test_complex_data_lookups(self): + """Ensure you can query dynamic document dynamic fields""" + p = self.Person() + p.misc = {'hello': 'world'} + p.save() + + self.assertEquals(1, self.Person.objects(misc__hello='world').count()) + + def test_inheritance(self): + """Ensure that dynamic document plays nice with inheritance""" + class Employee(self.Person): + salary = IntField() + + Employee.drop_collection() + + self.assertTrue('name' in Employee._fields) + self.assertTrue('salary' in Employee._fields) + self.assertEqual(Employee._get_collection_name(), + self.Person._get_collection_name()) + + joe_bloggs = Employee() + joe_bloggs.name = "Joe Bloggs" + joe_bloggs.salary = 10 + joe_bloggs.age = 20 + joe_bloggs.save() + + self.assertEquals(1, self.Person.objects(age=20).count()) + self.assertEquals(1, Employee.objects(age=20).count()) + + joe_bloggs = self.Person.objects.first() + self.assertTrue(isinstance(joe_bloggs, Employee)) + + def test_embedded_dynamic_document(self): + """Test dynamic embedded documents""" + class Embedded(DynamicEmbeddedDocument): + pass + + class Doc(DynamicDocument): + pass + + Doc.drop_collection() + doc = Doc() + + embedded_1 = Embedded() + embedded_1.string_field = 'hello' + embedded_1.int_field = 1 + embedded_1.dict_field = {'hello': 'world'} + embedded_1.list_field = ['1', 2, {'hello': 'world'}] + doc.embedded_field = embedded_1 + + self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc", + "embedded_field": { + "_types": ['Embedded'], "_cls": "Embedded", + "string_field": "hello", + "int_field": 1, + "dict_field": {"hello": "world"}, + "list_field": ['1', 2, {'hello': 'world'}] + } + }) + doc.save() + + doc = Doc.objects.first() + self.assertEquals(doc.embedded_field.__class__, Embedded) + self.assertEquals(doc.embedded_field.string_field, "hello") + self.assertEquals(doc.embedded_field.int_field, 1) + self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'}) + self.assertEquals(doc.embedded_field.list_field, ['1', 2, {'hello': 'world'}]) + + def test_complex_embedded_documents(self): + """Test complex dynamic embedded documents setups""" + class Embedded(DynamicEmbeddedDocument): + pass + + class Doc(DynamicDocument): + pass + + Doc.drop_collection() + doc = Doc() + + embedded_1 = Embedded() + embedded_1.string_field = 'hello' + embedded_1.int_field = 1 + embedded_1.dict_field = {'hello': 'world'} + + embedded_2 = Embedded() + embedded_2.string_field = 'hello' + embedded_2.int_field = 1 + embedded_2.dict_field = {'hello': 'world'} + embedded_2.list_field = ['1', 2, {'hello': 'world'}] + + embedded_1.list_field = ['1', 2, embedded_2] + doc.embedded_field = embedded_1 + + self.assertEquals(doc.to_mongo(), {"_types": ['Doc'], "_cls": "Doc", + "embedded_field": { + "_types": ['Embedded'], "_cls": "Embedded", + "string_field": "hello", + "int_field": 1, + "dict_field": {"hello": "world"}, + "list_field": ['1', 2, + {"_types": ['Embedded'], "_cls": "Embedded", + "string_field": "hello", + "int_field": 1, + "dict_field": {"hello": "world"}, + "list_field": ['1', 2, {'hello': 'world'}]} + ] + } + }) + doc.save() + doc = Doc.objects.first() + self.assertEquals(doc.embedded_field.__class__, Embedded) + self.assertEquals(doc.embedded_field.string_field, "hello") + self.assertEquals(doc.embedded_field.int_field, 1) + self.assertEquals(doc.embedded_field.dict_field, {'hello': 'world'}) + self.assertEquals(doc.embedded_field.list_field[0], '1') + self.assertEquals(doc.embedded_field.list_field[1], 2) + + embedded_field = doc.embedded_field.list_field[2] + + self.assertEquals(embedded_field.__class__, Embedded) + self.assertEquals(embedded_field.string_field, "hello") + self.assertEquals(embedded_field.int_field, 1) + self.assertEquals(embedded_field.dict_field, {'hello': 'world'}) + self.assertEquals(embedded_field.list_field, ['1', 2, {'hello': 'world'}]) + + def test_delta_for_dynamic_documents(self): + p = self.Person() + p.name = "Dean" + p.age = 22 + p.save() + + p.age = 24 + self.assertEquals(p.age, 24) + self.assertEquals(p._get_changed_fields(), ['age']) + self.assertEquals(p._delta(), ({'age': 24}, {})) + + p = self.Person.objects(age=22).get() + p.age = 24 + self.assertEquals(p.age, 24) + self.assertEquals(p._get_changed_fields(), ['age']) + self.assertEquals(p._delta(), ({'age': 24}, {})) + + p.save() + self.assertEquals(1, self.Person.objects(age=24).count()) + + def test_delta(self): + + class Doc(DynamicDocument): + pass + + Doc.drop_collection() + doc = Doc() + doc.save() + + doc = Doc.objects.first() + self.assertEquals(doc._get_changed_fields(), []) + self.assertEquals(doc._delta(), ({}, {})) + + doc.string_field = 'hello' + self.assertEquals(doc._get_changed_fields(), ['string_field']) + self.assertEquals(doc._delta(), ({'string_field': 'hello'}, {})) + + doc._changed_fields = [] + doc.int_field = 1 + self.assertEquals(doc._get_changed_fields(), ['int_field']) + self.assertEquals(doc._delta(), ({'int_field': 1}, {})) + + doc._changed_fields = [] + dict_value = {'hello': 'world', 'ping': 'pong'} + doc.dict_field = dict_value + self.assertEquals(doc._get_changed_fields(), ['dict_field']) + self.assertEquals(doc._delta(), ({'dict_field': dict_value}, {})) + + doc._changed_fields = [] + list_value = ['1', 2, {'hello': 'world'}] + doc.list_field = list_value + self.assertEquals(doc._get_changed_fields(), ['list_field']) + self.assertEquals(doc._delta(), ({'list_field': list_value}, {})) + + # Test unsetting + doc._changed_fields = [] + doc.dict_field = {} + self.assertEquals(doc._get_changed_fields(), ['dict_field']) + self.assertEquals(doc._delta(), ({}, {'dict_field': 1})) + + doc._changed_fields = [] + doc.list_field = [] + self.assertEquals(doc._get_changed_fields(), ['list_field']) + self.assertEquals(doc._delta(), ({}, {'list_field': 1})) + + def test_delta_recursive(self): + """Testing deltaing works with dynamic documents""" + class Embedded(DynamicEmbeddedDocument): + pass + + class Doc(DynamicDocument): + pass + + Doc.drop_collection() + doc = Doc() + doc.save() + + doc = Doc.objects.first() + self.assertEquals(doc._get_changed_fields(), []) + self.assertEquals(doc._delta(), ({}, {})) + + embedded_1 = Embedded() + embedded_1.string_field = 'hello' + embedded_1.int_field = 1 + embedded_1.dict_field = {'hello': 'world'} + embedded_1.list_field = ['1', 2, {'hello': 'world'}] + doc.embedded_field = embedded_1 + + self.assertEquals(doc._get_changed_fields(), ['embedded_field']) + + embedded_delta = { + '_types': ['Embedded'], + '_cls': 'Embedded', + 'string_field': 'hello', + 'int_field': 1, + 'dict_field': {'hello': 'world'}, + 'list_field': ['1', 2, {'hello': 'world'}] + } + self.assertEquals(doc.embedded_field._delta(), (embedded_delta, {})) + self.assertEquals(doc._delta(), ({'embedded_field': embedded_delta}, {})) + + doc.save() + doc.reload() + + doc.embedded_field.dict_field = {} + self.assertEquals(doc._get_changed_fields(), ['embedded_field.dict_field']) + self.assertEquals(doc.embedded_field._delta(), ({}, {'dict_field': 1})) + + self.assertEquals(doc._delta(), ({}, {'embedded_field.dict_field': 1})) + doc.save() + doc.reload() + + doc.embedded_field.list_field = [] + self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field']) + self.assertEquals(doc.embedded_field._delta(), ({}, {'list_field': 1})) + self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field': 1})) + doc.save() + doc.reload() + + embedded_2 = Embedded() + embedded_2.string_field = 'hello' + embedded_2.int_field = 1 + embedded_2.dict_field = {'hello': 'world'} + embedded_2.list_field = ['1', 2, {'hello': 'world'}] + + doc.embedded_field.list_field = ['1', 2, embedded_2] + self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field']) + self.assertEquals(doc.embedded_field._delta(), ({ + 'list_field': ['1', 2, { + '_cls': 'Embedded', + '_types': ['Embedded'], + 'string_field': 'hello', + 'dict_field': {'hello': 'world'}, + 'int_field': 1, + 'list_field': ['1', 2, {'hello': 'world'}], + }] + }, {})) + + self.assertEquals(doc._delta(), ({ + 'embedded_field.list_field': ['1', 2, { + '_cls': 'Embedded', + '_types': ['Embedded'], + 'string_field': 'hello', + 'dict_field': {'hello': 'world'}, + 'int_field': 1, + 'list_field': ['1', 2, {'hello': 'world'}], + }] + }, {})) + doc.save() + doc.reload() + + self.assertEquals(doc.embedded_field.list_field[2]._changed_fields, []) + self.assertEquals(doc.embedded_field.list_field[0], '1') + self.assertEquals(doc.embedded_field.list_field[1], 2) + for k in doc.embedded_field.list_field[2]._fields: + self.assertEquals(doc.embedded_field.list_field[2][k], embedded_2[k]) + + doc.embedded_field.list_field[2].string_field = 'world' + self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field.2.string_field']) + self.assertEquals(doc.embedded_field._delta(), ({'list_field.2.string_field': 'world'}, {})) + self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.string_field': 'world'}, {})) + doc.save() + doc.reload() + self.assertEquals(doc.embedded_field.list_field[2].string_field, 'world') + + # Test multiple assignments + doc.embedded_field.list_field[2].string_field = 'hello world' + doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2] + self.assertEquals(doc._get_changed_fields(), ['embedded_field.list_field']) + self.assertEquals(doc.embedded_field._delta(), ({ + 'list_field': ['1', 2, { + '_types': ['Embedded'], + '_cls': 'Embedded', + 'string_field': 'hello world', + 'int_field': 1, + 'list_field': ['1', 2, {'hello': 'world'}], + 'dict_field': {'hello': 'world'}}]}, {})) + self.assertEquals(doc._delta(), ({ + 'embedded_field.list_field': ['1', 2, { + '_types': ['Embedded'], + '_cls': 'Embedded', + 'string_field': 'hello world', + 'int_field': 1, + 'list_field': ['1', 2, {'hello': 'world'}], + 'dict_field': {'hello': 'world'}} + ]}, {})) + doc.save() + doc.reload() + self.assertEquals(doc.embedded_field.list_field[2].string_field, 'hello world') + + # Test list native methods + doc.embedded_field.list_field[2].list_field.pop(0) + self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}]}, {})) + doc.save() + doc.reload() + + doc.embedded_field.list_field[2].list_field.append(1) + self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [2, {'hello': 'world'}, 1]}, {})) + doc.save() + doc.reload() + self.assertEquals(doc.embedded_field.list_field[2].list_field, [2, {'hello': 'world'}, 1]) + + doc.embedded_field.list_field[2].list_field.sort() + doc.save() + doc.reload() + self.assertEquals(doc.embedded_field.list_field[2].list_field, [1, 2, {'hello': 'world'}]) + + del(doc.embedded_field.list_field[2].list_field[2]['hello']) + self.assertEquals(doc._delta(), ({'embedded_field.list_field.2.list_field': [1, 2, {}]}, {})) + doc.save() + doc.reload() + + del(doc.embedded_field.list_field[2].list_field) + self.assertEquals(doc._delta(), ({}, {'embedded_field.list_field.2.list_field': 1})) + + doc.save() + doc.reload() + + doc.dict_field = {'embedded': embedded_1} + doc.save() + doc.reload() + + doc.dict_field['embedded'].string_field = 'Hello World' + self.assertEquals(doc._get_changed_fields(), ['dict_field.embedded.string_field']) + self.assertEquals(doc._delta(), ({'dict_field.embedded.string_field': 'Hello World'}, {}))