_dynamic field updates - fixed pickling and creation order
Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) Fixed pickling dynamic documents `_dynamic_fields` (#387)
This commit is contained in:
parent
f26f1a526c
commit
af86aee970
@ -4,6 +4,8 @@ Changelog
|
|||||||
|
|
||||||
Changes in 0.8.3
|
Changes in 0.8.3
|
||||||
================
|
================
|
||||||
|
- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396)
|
||||||
|
- Fixed pickling dynamic documents `_dynamic_fields` (#387)
|
||||||
- Fixed ListField setslice and delslice dirty tracking (#390)
|
- Fixed ListField setslice and delslice dirty tracking (#390)
|
||||||
- Added Django 1.5 PY3 support (#392)
|
- Added Django 1.5 PY3 support (#392)
|
||||||
- Added match ($elemMatch) support for EmbeddedDocuments (#379)
|
- Added match ($elemMatch) support for EmbeddedDocuments (#379)
|
||||||
|
@ -54,7 +54,7 @@ be saved ::
|
|||||||
|
|
||||||
There is one caveat on Dynamic Documents: fields cannot start with `_`
|
There is one caveat on Dynamic Documents: fields cannot start with `_`
|
||||||
|
|
||||||
Dynamic fields are stored in alphabetical order *after* any declared fields.
|
Dynamic fields are stored in creation order *after* any declared fields.
|
||||||
|
|
||||||
Fields
|
Fields
|
||||||
======
|
======
|
||||||
|
@ -2,6 +2,16 @@
|
|||||||
Upgrading
|
Upgrading
|
||||||
#########
|
#########
|
||||||
|
|
||||||
|
|
||||||
|
0.8.2 to 0.8.2
|
||||||
|
**************
|
||||||
|
|
||||||
|
Minor change that may impact users:
|
||||||
|
|
||||||
|
DynamicDocument fields are now stored in creation order after any declared
|
||||||
|
fields. Previously they were stored alphabetically.
|
||||||
|
|
||||||
|
|
||||||
0.7 to 0.8
|
0.7 to 0.8
|
||||||
**********
|
**********
|
||||||
|
|
||||||
|
@ -42,6 +42,9 @@ class BaseDocument(object):
|
|||||||
# Combine positional arguments with named arguments.
|
# Combine positional arguments with named arguments.
|
||||||
# We only want named arguments.
|
# We only want named arguments.
|
||||||
field = iter(self._fields_ordered)
|
field = iter(self._fields_ordered)
|
||||||
|
# If its an automatic id field then skip to the first defined field
|
||||||
|
if self._auto_id_field:
|
||||||
|
next(field)
|
||||||
for value in args:
|
for value in args:
|
||||||
name = next(field)
|
name = next(field)
|
||||||
if name in values:
|
if name in values:
|
||||||
@ -51,6 +54,7 @@ class BaseDocument(object):
|
|||||||
signals.pre_init.send(self.__class__, document=self, values=values)
|
signals.pre_init.send(self.__class__, document=self, values=values)
|
||||||
|
|
||||||
self._data = {}
|
self._data = {}
|
||||||
|
self._dynamic_fields = SON()
|
||||||
|
|
||||||
# Assign default values to instance
|
# Assign default values to instance
|
||||||
for key, field in self._fields.iteritems():
|
for key, field in self._fields.iteritems():
|
||||||
@ -61,7 +65,6 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
# Set passed values after initialisation
|
# Set passed values after initialisation
|
||||||
if self._dynamic:
|
if self._dynamic:
|
||||||
self._dynamic_fields = {}
|
|
||||||
dynamic_data = {}
|
dynamic_data = {}
|
||||||
for key, value in values.iteritems():
|
for key, value in values.iteritems():
|
||||||
if key in self._fields or key == '_id':
|
if key in self._fields or key == '_id':
|
||||||
@ -116,6 +119,7 @@ class BaseDocument(object):
|
|||||||
field = DynamicField(db_field=name)
|
field = DynamicField(db_field=name)
|
||||||
field.name = name
|
field.name = name
|
||||||
self._dynamic_fields[name] = field
|
self._dynamic_fields[name] = field
|
||||||
|
self._fields_ordered += (name,)
|
||||||
|
|
||||||
if not name.startswith('_'):
|
if not name.startswith('_'):
|
||||||
value = self.__expand_dynamic_values(name, value)
|
value = self.__expand_dynamic_values(name, value)
|
||||||
@ -142,7 +146,8 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
def __getstate__(self):
|
def __getstate__(self):
|
||||||
data = {}
|
data = {}
|
||||||
for k in ('_changed_fields', '_initialised', '_created'):
|
for k in ('_changed_fields', '_initialised', '_created',
|
||||||
|
'_dynamic_fields', '_fields_ordered'):
|
||||||
if hasattr(self, k):
|
if hasattr(self, k):
|
||||||
data[k] = getattr(self, k)
|
data[k] = getattr(self, k)
|
||||||
data['_data'] = self.to_mongo()
|
data['_data'] = self.to_mongo()
|
||||||
@ -151,21 +156,21 @@ class BaseDocument(object):
|
|||||||
def __setstate__(self, data):
|
def __setstate__(self, data):
|
||||||
if isinstance(data["_data"], SON):
|
if isinstance(data["_data"], SON):
|
||||||
data["_data"] = self.__class__._from_son(data["_data"])._data
|
data["_data"] = self.__class__._from_son(data["_data"])._data
|
||||||
for k in ('_changed_fields', '_initialised', '_created', '_data'):
|
for k in ('_changed_fields', '_initialised', '_created', '_data',
|
||||||
|
'_fields_ordered', '_dynamic_fields'):
|
||||||
if k in data:
|
if k in data:
|
||||||
setattr(self, k, data[k])
|
setattr(self, k, data[k])
|
||||||
|
for k in data.get('_dynamic_fields').keys():
|
||||||
|
setattr(self, k, data["_data"].get(k))
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
if 'id' in self._fields and 'id' not in self._fields_ordered:
|
|
||||||
return iter(('id', ) + self._fields_ordered)
|
|
||||||
|
|
||||||
return iter(self._fields_ordered)
|
return iter(self._fields_ordered)
|
||||||
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
"""Dictionary-style field access, return a field's value if present.
|
"""Dictionary-style field access, return a field's value if present.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if name in self._fields:
|
if name in self._fields_ordered:
|
||||||
return getattr(self, name)
|
return getattr(self, name)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
@ -241,6 +246,8 @@ class BaseDocument(object):
|
|||||||
for field_name in self:
|
for field_name in self:
|
||||||
value = self._data.get(field_name, None)
|
value = self._data.get(field_name, None)
|
||||||
field = self._fields.get(field_name)
|
field = self._fields.get(field_name)
|
||||||
|
if field is None and self._dynamic:
|
||||||
|
field = self._dynamic_fields.get(field_name)
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
value = field.to_mongo(value)
|
value = field.to_mongo(value)
|
||||||
@ -265,15 +272,6 @@ class BaseDocument(object):
|
|||||||
not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)):
|
not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)):
|
||||||
data.pop('_cls')
|
data.pop('_cls')
|
||||||
|
|
||||||
if not self._dynamic:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Sort dynamic fields by key
|
|
||||||
dynamic_fields = sorted(self._dynamic_fields.iteritems(),
|
|
||||||
key=operator.itemgetter(0))
|
|
||||||
for name, field in dynamic_fields:
|
|
||||||
data[name] = field.to_mongo(self._data.get(name, None))
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def validate(self, clean=True):
|
def validate(self, clean=True):
|
||||||
@ -289,11 +287,8 @@ class BaseDocument(object):
|
|||||||
errors[NON_FIELD_ERRORS] = error
|
errors[NON_FIELD_ERRORS] = error
|
||||||
|
|
||||||
# Get a list of tuples of field names and their current values
|
# Get a list of tuples of field names and their current values
|
||||||
fields = [(field, self._data.get(name))
|
fields = [(self._fields.get(name, self._dynamic_fields.get(name)),
|
||||||
for name, field in self._fields.items()]
|
self._data.get(name)) for name in self._fields_ordered]
|
||||||
if self._dynamic:
|
|
||||||
fields += [(field, self._data.get(name))
|
|
||||||
for name, field in self._dynamic_fields.items()]
|
|
||||||
|
|
||||||
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
|
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
|
||||||
GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField")
|
GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField")
|
||||||
@ -406,11 +401,7 @@ class BaseDocument(object):
|
|||||||
return _changed_fields
|
return _changed_fields
|
||||||
inspected.add(self.id)
|
inspected.add(self.id)
|
||||||
|
|
||||||
field_list = self._fields.copy()
|
for field_name in self._fields_ordered:
|
||||||
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)
|
db_field_name = self._db_field_map.get(field_name, field_name)
|
||||||
key = '%s.' % db_field_name
|
key = '%s.' % db_field_name
|
||||||
@ -450,7 +441,6 @@ class BaseDocument(object):
|
|||||||
doc = self.to_mongo()
|
doc = self.to_mongo()
|
||||||
|
|
||||||
set_fields = self._get_changed_fields()
|
set_fields = self._get_changed_fields()
|
||||||
set_data = {}
|
|
||||||
unset_data = {}
|
unset_data = {}
|
||||||
parts = []
|
parts = []
|
||||||
if hasattr(self, '_changed_fields'):
|
if hasattr(self, '_changed_fields'):
|
||||||
|
@ -91,11 +91,12 @@ class DocumentMetaclass(type):
|
|||||||
attrs['_fields'] = doc_fields
|
attrs['_fields'] = doc_fields
|
||||||
attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k))
|
attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k))
|
||||||
for k, v in doc_fields.iteritems()])
|
for k, v in doc_fields.iteritems()])
|
||||||
|
attrs['_reverse_db_field_map'] = dict(
|
||||||
|
(v, k) for k, v in attrs['_db_field_map'].iteritems())
|
||||||
|
|
||||||
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
|
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
|
||||||
(v.creation_counter, v.name)
|
(v.creation_counter, v.name)
|
||||||
for v in doc_fields.itervalues()))
|
for v in doc_fields.itervalues()))
|
||||||
attrs['_reverse_db_field_map'] = dict(
|
|
||||||
(v, k) for k, v in attrs['_db_field_map'].iteritems())
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Set document hierarchy
|
# Set document hierarchy
|
||||||
@ -358,12 +359,18 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
new_class.id = field
|
new_class.id = field
|
||||||
|
|
||||||
# Set primary key if not defined by the document
|
# Set primary key if not defined by the document
|
||||||
|
new_class._auto_id_field = False
|
||||||
if not new_class._meta.get('id_field'):
|
if not new_class._meta.get('id_field'):
|
||||||
|
new_class._auto_id_field = True
|
||||||
new_class._meta['id_field'] = 'id'
|
new_class._meta['id_field'] = 'id'
|
||||||
new_class._fields['id'] = ObjectIdField(db_field='_id')
|
new_class._fields['id'] = ObjectIdField(db_field='_id')
|
||||||
new_class._fields['id'].name = 'id'
|
new_class._fields['id'].name = 'id'
|
||||||
new_class.id = new_class._fields['id']
|
new_class.id = new_class._fields['id']
|
||||||
|
|
||||||
|
# Prepend id field to _fields_ordered
|
||||||
|
if 'id' in new_class._fields and 'id' not in new_class._fields_ordered:
|
||||||
|
new_class._fields_ordered = ('id', ) + new_class._fields_ordered
|
||||||
|
|
||||||
# Merge in exceptions with parent hierarchy
|
# Merge in exceptions with parent hierarchy
|
||||||
exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned)
|
exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned)
|
||||||
module = attrs.get('__module__')
|
module = attrs.get('__module__')
|
||||||
|
@ -460,11 +460,8 @@ class Document(BaseDocument):
|
|||||||
else:
|
else:
|
||||||
msg = "Reloaded document has been deleted"
|
msg = "Reloaded document has been deleted"
|
||||||
raise OperationError(msg)
|
raise OperationError(msg)
|
||||||
for field in self._fields:
|
for field in self._fields_ordered:
|
||||||
setattr(self, field, self._reload(field, obj[field]))
|
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 = obj._changed_fields
|
self._changed_fields = obj._changed_fields
|
||||||
self._created = False
|
self._created = False
|
||||||
return obj
|
return obj
|
||||||
|
@ -3,6 +3,7 @@ import sys
|
|||||||
sys.path[0:0] = [""]
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from bson import SON
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import get_db
|
from mongoengine.connection import get_db
|
||||||
|
|
||||||
@ -613,13 +614,13 @@ class DeltaTest(unittest.TestCase):
|
|||||||
Person.drop_collection()
|
Person.drop_collection()
|
||||||
|
|
||||||
p = Person(name="James", age=34)
|
p = Person(name="James", age=34)
|
||||||
self.assertEqual(p._delta(), ({'age': 34, 'name': 'James',
|
self.assertEqual(p._delta(), (
|
||||||
'_cls': 'Person'}, {}))
|
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
|
||||||
|
|
||||||
p.doc = 123
|
p.doc = 123
|
||||||
del(p.doc)
|
del(p.doc)
|
||||||
self.assertEqual(p._delta(), ({'age': 34, 'name': 'James',
|
self.assertEqual(p._delta(), (
|
||||||
'_cls': 'Person'}, {'doc': 1}))
|
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
|
||||||
|
|
||||||
p = Person()
|
p = Person()
|
||||||
p.name = "Dean"
|
p.name = "Dean"
|
||||||
@ -631,14 +632,14 @@ class DeltaTest(unittest.TestCase):
|
|||||||
self.assertEqual(p._get_changed_fields(), ['age'])
|
self.assertEqual(p._get_changed_fields(), ['age'])
|
||||||
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
||||||
|
|
||||||
p = self.Person.objects(age=22).get()
|
p = Person.objects(age=22).get()
|
||||||
p.age = 24
|
p.age = 24
|
||||||
self.assertEqual(p.age, 24)
|
self.assertEqual(p.age, 24)
|
||||||
self.assertEqual(p._get_changed_fields(), ['age'])
|
self.assertEqual(p._get_changed_fields(), ['age'])
|
||||||
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
self.assertEqual(p._delta(), ({'age': 24}, {}))
|
||||||
|
|
||||||
p.save()
|
p.save()
|
||||||
self.assertEqual(1, self.Person.objects(age=24).count())
|
self.assertEqual(1, Person.objects(age=24).count())
|
||||||
|
|
||||||
def test_dynamic_delta(self):
|
def test_dynamic_delta(self):
|
||||||
|
|
||||||
|
@ -10,7 +10,8 @@ import uuid
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from bson import DBRef
|
from bson import DBRef
|
||||||
from tests.fixtures import PickleEmbedded, PickleTest, PickleSignalsTest
|
from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
|
||||||
|
PickleDyanmicEmbedded, PickleDynamicTest)
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.errors import (NotRegistered, InvalidDocumentError,
|
from mongoengine.errors import (NotRegistered, InvalidDocumentError,
|
||||||
@ -1827,6 +1828,29 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(pickle_doc.string, "Two")
|
self.assertEqual(pickle_doc.string, "Two")
|
||||||
self.assertEqual(pickle_doc.lists, ["1", "2", "3"])
|
self.assertEqual(pickle_doc.lists, ["1", "2", "3"])
|
||||||
|
|
||||||
|
def test_dynamic_document_pickle(self):
|
||||||
|
|
||||||
|
pickle_doc = PickleDynamicTest(name="test", number=1, string="One", lists=['1', '2'])
|
||||||
|
pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar")
|
||||||
|
pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved
|
||||||
|
|
||||||
|
pickle_doc.save()
|
||||||
|
|
||||||
|
pickled_doc = pickle.dumps(pickle_doc)
|
||||||
|
resurrected = pickle.loads(pickled_doc)
|
||||||
|
|
||||||
|
self.assertEqual(resurrected, pickle_doc)
|
||||||
|
self.assertEqual(resurrected._fields_ordered,
|
||||||
|
pickle_doc._fields_ordered)
|
||||||
|
self.assertEqual(resurrected._dynamic_fields.keys(),
|
||||||
|
pickle_doc._dynamic_fields.keys())
|
||||||
|
|
||||||
|
self.assertEqual(resurrected.embedded, pickle_doc.embedded)
|
||||||
|
self.assertEqual(resurrected.embedded._fields_ordered,
|
||||||
|
pickle_doc.embedded._fields_ordered)
|
||||||
|
self.assertEqual(resurrected.embedded._dynamic_fields.keys(),
|
||||||
|
pickle_doc.embedded._dynamic_fields.keys())
|
||||||
|
|
||||||
def test_picklable_on_signals(self):
|
def test_picklable_on_signals(self):
|
||||||
pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2'])
|
pickle_doc = PickleSignalsTest(number=1, string="One", lists=['1', '2'])
|
||||||
pickle_doc.embedded = PickleEmbedded()
|
pickle_doc.embedded = PickleEmbedded()
|
||||||
@ -2289,6 +2313,16 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(person.name, "Test User")
|
self.assertEqual(person.name, "Test User")
|
||||||
self.assertEqual(person.age, 42)
|
self.assertEqual(person.age, 42)
|
||||||
|
|
||||||
|
def test_mixed_creation_dynamic(self):
|
||||||
|
"""Ensure that document may be created using mixed arguments.
|
||||||
|
"""
|
||||||
|
class Person(DynamicDocument):
|
||||||
|
name = StringField()
|
||||||
|
|
||||||
|
person = Person("Test User", age=42)
|
||||||
|
self.assertEqual(person.name, "Test User")
|
||||||
|
self.assertEqual(person.age, 42)
|
||||||
|
|
||||||
def test_bad_mixed_creation(self):
|
def test_bad_mixed_creation(self):
|
||||||
"""Ensure that document gives correct error when duplicating arguments
|
"""Ensure that document gives correct error when duplicating arguments
|
||||||
"""
|
"""
|
||||||
|
@ -17,6 +17,14 @@ class PickleTest(Document):
|
|||||||
photo = FileField()
|
photo = FileField()
|
||||||
|
|
||||||
|
|
||||||
|
class PickleDyanmicEmbedded(DynamicEmbeddedDocument):
|
||||||
|
date = DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
class PickleDynamicTest(DynamicDocument):
|
||||||
|
number = IntField()
|
||||||
|
|
||||||
|
|
||||||
class PickleSignalsTest(Document):
|
class PickleSignalsTest(Document):
|
||||||
number = IntField()
|
number = IntField()
|
||||||
string = StringField(choices=(('One', '1'), ('Two', '2')))
|
string = StringField(choices=(('One', '1'), ('Two', '2')))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user