Skip to content

Commit 972ac73

Browse files
author
Omer Katz
authored
Merge pull request #1497 from userlocalhost/feature/order_guarantee
added a feature to save object data in order
2 parents 63206c3 + d8b238d commit 972ac73

File tree

4 files changed

+153
-3
lines changed

4 files changed

+153
-3
lines changed

mongoengine/dereference.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import OrderedDict
12
from bson import DBRef, SON
23
import six
34

@@ -201,6 +202,10 @@ def _attach_objects(self, items, depth=0, instance=None, name=None):
201202
as_tuple = isinstance(items, tuple)
202203
iterator = enumerate(items)
203204
data = []
205+
elif isinstance(items, OrderedDict):
206+
is_list = False
207+
iterator = items.iteritems()
208+
data = OrderedDict()
204209
else:
205210
is_list = False
206211
iterator = items.iteritems()

mongoengine/fields.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import time
66
import uuid
77
import warnings
8+
from collections import Mapping
89
from operator import itemgetter
910

1011
from bson import Binary, DBRef, ObjectId, SON
@@ -619,6 +620,14 @@ class DynamicField(BaseField):
619620
620621
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
621622

623+
def __init__(self, container_class=dict, *args, **kwargs):
624+
self._container_cls = container_class
625+
if not issubclass(self._container_cls, Mapping):
626+
self.error('The class that is specified in `container_class` parameter '
627+
'must be a subclass of `dict`.')
628+
629+
super(DynamicField, self).__init__(*args, **kwargs)
630+
622631
def to_mongo(self, value, use_db_field=True, fields=None):
623632
"""Convert a Python type to a MongoDB compatible type.
624633
"""
@@ -644,7 +653,7 @@ def to_mongo(self, value, use_db_field=True, fields=None):
644653
is_list = True
645654
value = {k: v for k, v in enumerate(value)}
646655

647-
data = {}
656+
data = self._container_cls()
648657
for k, v in value.iteritems():
649658
data[k] = self.to_mongo(v, use_db_field, fields)
650659

tests/fields/fields.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import math
66
import itertools
77
import re
8+
import pymongo
89

910
from nose.plugins.skip import SkipTest
11+
from collections import OrderedDict
1012
import six
1113

1214
try:
@@ -25,9 +27,12 @@
2527
from mongoengine import *
2628
from mongoengine.connection import get_db
2729
from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList,
28-
_document_registry)
30+
_document_registry, TopLevelDocumentMetaclass)
2931

30-
from tests.utils import MongoDBTestCase
32+
from tests.utils import MongoDBTestCase, MONGO_TEST_DB
33+
from mongoengine.python_support import IS_PYMONGO_3
34+
if IS_PYMONGO_3:
35+
from bson import CodecOptions
3136

3237
__all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase")
3338

@@ -4110,6 +4115,67 @@ class CustomData(Document):
41104115
self.assertTrue(hasattr(CustomData.c_field, 'custom_data'))
41114116
self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a'])
41124117

4118+
def test_dynamicfield_with_container_class(self):
4119+
"""
4120+
Tests that object can be stored in order by DynamicField class
4121+
with container_class parameter.
4122+
"""
4123+
raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)]
4124+
4125+
class Doc(Document):
4126+
ordered_data = DynamicField(container_class=OrderedDict)
4127+
unordered_data = DynamicField()
4128+
4129+
Doc.drop_collection()
4130+
4131+
doc = Doc(ordered_data=OrderedDict(raw_data), unordered_data=dict(raw_data)).save()
4132+
4133+
# checks that the data is in order
4134+
self.assertEqual(type(doc.ordered_data), OrderedDict)
4135+
self.assertEqual(type(doc.unordered_data), dict)
4136+
self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a')
4137+
4138+
# checks that the data is stored to the database in order
4139+
pymongo_db = pymongo.MongoClient()[MONGO_TEST_DB]
4140+
if IS_PYMONGO_3:
4141+
codec_option = CodecOptions(document_class=OrderedDict)
4142+
db_doc = pymongo_db.doc.with_options(codec_options=codec_option).find_one()
4143+
else:
4144+
db_doc = pymongo_db.doc.find_one(as_class=OrderedDict)
4145+
4146+
self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a')
4147+
4148+
def test_dynamicfield_with_wrong_container_class(self):
4149+
with self.assertRaises(ValidationError):
4150+
class DocWithInvalidField:
4151+
data = DynamicField(container_class=list)
4152+
4153+
def test_dynamicfield_with_wrong_container_class_and_reload_docuemnt(self):
4154+
# This is because 'codec_options' is supported on pymongo3 or later
4155+
if IS_PYMONGO_3:
4156+
class OrderedDocument(Document):
4157+
my_metaclass = TopLevelDocumentMetaclass
4158+
__metaclass__ = TopLevelDocumentMetaclass
4159+
4160+
@classmethod
4161+
def _get_collection(cls):
4162+
collection = super(OrderedDocument, cls)._get_collection()
4163+
opts = CodecOptions(document_class=OrderedDict)
4164+
4165+
return collection.with_options(codec_options=opts)
4166+
4167+
raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)]
4168+
4169+
class Doc(OrderedDocument):
4170+
data = DynamicField(container_class=OrderedDict)
4171+
4172+
Doc.drop_collection()
4173+
4174+
doc = Doc(data=OrderedDict(raw_data)).save()
4175+
doc.reload()
4176+
4177+
self.assertEqual(type(doc.data), OrderedDict)
4178+
self.assertEqual(','.join(doc.data.keys()), 'd,c,b,a')
41134179

41144180
class CachedReferenceFieldTest(MongoDBTestCase):
41154181

tests/test_dereference.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
import unittest
33

44
from bson import DBRef, ObjectId
5+
from collections import OrderedDict
56

67
from mongoengine import *
78
from mongoengine.connection import get_db
89
from mongoengine.context_managers import query_counter
10+
from mongoengine.python_support import IS_PYMONGO_3
11+
from mongoengine.base import TopLevelDocumentMetaclass
12+
if IS_PYMONGO_3:
13+
from bson import CodecOptions
914

1015

1116
class FieldTest(unittest.TestCase):
@@ -1287,5 +1292,70 @@ class Playlist(Document):
12871292

12881293
self.assertEqual(q, 2)
12891294

1295+
def test_dynamic_field_dereference(self):
1296+
class Merchandise(Document):
1297+
name = StringField()
1298+
price = IntField()
1299+
1300+
class Store(Document):
1301+
merchandises = DynamicField()
1302+
1303+
Merchandise.drop_collection()
1304+
Store.drop_collection()
1305+
1306+
merchandises = {
1307+
'#1': Merchandise(name='foo', price=100).save(),
1308+
'#2': Merchandise(name='bar', price=120).save(),
1309+
'#3': Merchandise(name='baz', price=110).save(),
1310+
}
1311+
Store(merchandises=merchandises).save()
1312+
1313+
store = Store.objects().first()
1314+
for obj in store.merchandises.values():
1315+
self.assertFalse(isinstance(obj, Merchandise))
1316+
1317+
store.select_related()
1318+
for obj in store.merchandises.values():
1319+
self.assertTrue(isinstance(obj, Merchandise))
1320+
1321+
def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self):
1322+
# This is because 'codec_options' is supported on pymongo3 or later
1323+
if IS_PYMONGO_3:
1324+
class OrderedDocument(Document):
1325+
my_metaclass = TopLevelDocumentMetaclass
1326+
__metaclass__ = TopLevelDocumentMetaclass
1327+
1328+
@classmethod
1329+
def _get_collection(cls):
1330+
collection = super(OrderedDocument, cls)._get_collection()
1331+
opts = CodecOptions(document_class=OrderedDict)
1332+
1333+
return collection.with_options(codec_options=opts)
1334+
1335+
class Merchandise(Document):
1336+
name = StringField()
1337+
price = IntField()
1338+
1339+
class Store(OrderedDocument):
1340+
merchandises = DynamicField(container_class=OrderedDict)
1341+
1342+
Merchandise.drop_collection()
1343+
Store.drop_collection()
1344+
1345+
merchandises = OrderedDict()
1346+
merchandises['#1'] = Merchandise(name='foo', price=100).save()
1347+
merchandises['#2'] = Merchandise(name='bar', price=120).save()
1348+
merchandises['#3'] = Merchandise(name='baz', price=110).save()
1349+
1350+
Store(merchandises=merchandises).save()
1351+
1352+
store = Store.objects().first()
1353+
1354+
store.select_related()
1355+
1356+
# confirms that the load data order is same with the one at storing
1357+
self.assertTrue(type(store.merchandises), OrderedDict)
1358+
self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3')
1359+
12901360
if __name__ == '__main__':
12911361
unittest.main()

0 commit comments

Comments
 (0)