Compare commits

...

6 Commits

Author SHA1 Message Date
Stefan Wojcik
a659f9aa8d Add a changelog entry [ci skip] 2019-06-21 15:59:04 +02:00
Stefan Wojcik
f884839d17 Implement BaseDocument.to_dict
`BaseDocument.to_dict` serializes a document/embedded document into a dict,
which can be easily consumed by other modules (which in this case don't need
to be aware of MongoEngine-specific object types).

The output dict contains key-value pairs where:
* Keys are field names as they're defined on the document (as opposed to e.g.
  how they're stored in MongoDB).
* Values are field values in their deserialized form (i.e. the same form that
  you get when you access `doc.some_field_name`).
2019-06-21 15:45:33 +02:00
Stefan Wojcik
a4fe091a51 Cleaner code & comments in BaseField.__set__ 2019-06-21 13:51:53 +02:00
Stefan Wojcik
216217e2c6 Datastructures comments: fix typos and tweak formatting [ci skip] 2019-06-21 13:48:24 +02:00
Stefan Wojcik
799775b3a7 Slightly cleaner docstring of BaseQuerySet.no_sub_classes [ci skip] 2019-06-20 12:18:58 +02:00
Stefan Wójcik
ae0384df29 Improve Document.meta.shard_key docs (#2099)
This closes #2096. Previous documentation of the shard_key meta attribute was
missing the crucial point that it really only matters if your collection is
sharded over a compound index.
2019-06-20 11:25:51 +02:00
8 changed files with 143 additions and 33 deletions

View File

@@ -6,6 +6,7 @@ Changelog
Development Development
=========== ===========
- (Fill this out as you fix issues and develop your features). - (Fill this out as you fix issues and develop your features).
- Add a `BaseDocument.to_dict` method #2101
Changes in 0.18.1 Changes in 0.18.1
================= =================

View File

@@ -714,11 +714,16 @@ subsequent calls to :meth:`~mongoengine.queryset.QuerySet.order_by`. ::
Shard keys Shard keys
========== ==========
If your collection is sharded, then you need to specify the shard key as a tuple, If your collection is sharded by multiple keys, then you can improve shard
using the :attr:`shard_key` attribute of :attr:`~mongoengine.Document.meta`. routing (and thus the performance of your application) by specifying the shard
This ensures that the shard key is sent with the query when calling the key, using the :attr:`shard_key` attribute of
:meth:`~mongoengine.document.Document.save` or :attr:`~mongoengine.Document.meta`. The shard key should be defined as a tuple.
:meth:`~mongoengine.document.Document.update` method on an existing
This ensures that the full shard key is sent with the query when calling
methods such as :meth:`~mongoengine.document.Document.save`,
:meth:`~mongoengine.document.Document.update`,
:meth:`~mongoengine.document.Document.modify`, or
:meth:`~mongoengine.document.Document.delete` on an existing
:class:`~mongoengine.Document` instance:: :class:`~mongoengine.Document` instance::
class LogEntry(Document): class LogEntry(Document):
@@ -728,7 +733,8 @@ This ensures that the shard key is sent with the query when calling the
data = StringField() data = StringField()
meta = { meta = {
'shard_key': ('machine', 'timestamp',) 'shard_key': ('machine', 'timestamp'),
'indexes': ('machine', 'timestamp'),
} }
.. _document-inheritance: .. _document-inheritance:

View File

@@ -11,18 +11,20 @@ __all__ = ('BaseDict', 'StrictDict', 'BaseList', 'EmbeddedDocumentList', 'LazyRe
def mark_as_changed_wrapper(parent_method): def mark_as_changed_wrapper(parent_method):
"""Decorators that ensures _mark_as_changed method gets called""" """Decorator that ensures _mark_as_changed method gets called."""
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
result = parent_method(self, *args, **kwargs) # Can't use super() in the decorator # Can't use super() in the decorator.
result = parent_method(self, *args, **kwargs)
self._mark_as_changed() self._mark_as_changed()
return result return result
return wrapper return wrapper
def mark_key_as_changed_wrapper(parent_method): def mark_key_as_changed_wrapper(parent_method):
"""Decorators that ensures _mark_as_changed method gets called with the key argument""" """Decorator that ensures _mark_as_changed method gets called with the key argument"""
def wrapper(self, key, *args, **kwargs): def wrapper(self, key, *args, **kwargs):
result = parent_method(self, key, *args, **kwargs) # Can't use super() in the decorator # Can't use super() in the decorator.
result = parent_method(self, key, *args, **kwargs)
self._mark_as_changed(key) self._mark_as_changed(key)
return result return result
return wrapper return wrapper

View File

@@ -309,9 +309,7 @@ class BaseDocument(object):
return self._data['_text_score'] return self._data['_text_score']
def to_mongo(self, use_db_field=True, fields=None): def to_mongo(self, use_db_field=True, fields=None):
""" """Return as SON data ready for use with MongoDB."""
Return as SON data ready for use with MongoDB.
"""
fields = fields or [] fields = fields or []
data = SON() data = SON()
@@ -412,12 +410,35 @@ class BaseDocument(object):
message = 'ValidationError (%s:%s) ' % (self._class_name, pk) message = 'ValidationError (%s:%s) ' % (self._class_name, pk)
raise ValidationError(message, errors=errors) raise ValidationError(message, errors=errors)
def to_dict(self):
"""Serialize this document into a dict.
Return field names as they're defined on the document (as opposed to
e.g. how they're stored in MongoDB). Return values in their
deserialized form (i.e. the same form that you get when you access
`doc.some_field_name`). Serialize embedded documents recursively.
The resultant dict can be consumed easily by other modules which
don't need to be aware of MongoEngine-specific object types.
:return dict: dictionary of field name & value pairs.
"""
data_dict = {}
for field_name in self._fields:
value = getattr(self, field_name)
if isinstance(value, BaseDocument):
data_dict[field_name] = value.to_dict()
else:
data_dict[field_name] = value
return data_dict
def to_json(self, *args, **kwargs): def to_json(self, *args, **kwargs):
"""Convert this document to JSON. """Convert this document to JSON.
:param use_db_field: Serialize field names as they appear in :param use_db_field: Serialize field names as they appear in
MongoDB (as opposed to attribute names on this document). MongoDB (as opposed to attribute names on this document).
Defaults to True. Defaults to True.
:return str: string representing the jsonified document.
""" """
use_db_field = kwargs.pop('use_db_field', True) use_db_field = kwargs.pop('use_db_field', True)
return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
@@ -426,12 +447,13 @@ class BaseDocument(object):
def from_json(cls, json_data, created=False): def from_json(cls, json_data, created=False):
"""Converts json data to a Document instance """Converts json data to a Document instance
:param json_data: The json data to load into the Document :param str json_data: The json data to load into the Document
:param created: If True, the document will be considered as a brand new document :param bool created: If True, the document will be considered as
If False and an id is provided, it will consider that the data being a brand new document. If False and an ID is provided, it will
loaded corresponds to what's already in the database (This has an impact of subsequent call to .save()) consider that the data being loaded corresponds to what's already
If False and no id is provided, it will consider the data as a new document in the database (This has an impact of subsequent call to .save())
(default ``False``) If False and no id is provided, it will consider the data as a new
document (default ``False``)
""" """
return cls._from_son(json_util.loads(json_data), created=created) return cls._from_son(json_util.loads(json_data), created=created)

View File

@@ -128,10 +128,9 @@ class BaseField(object):
return instance._data.get(self.name) return instance._data.get(self.name)
def __set__(self, instance, value): def __set__(self, instance, value):
"""Descriptor for assigning a value to a field in a document. """Descriptor for assigning a value to a field in a document."""
""" # If setting to None and there is a default value provided for this
# If setting to None and there is a default # field, then set the value to the default value.
# Then set the value to the default value
if value is None: if value is None:
if self.null: if self.null:
value = None value = None
@@ -142,12 +141,16 @@ class BaseField(object):
if instance._initialised: if instance._initialised:
try: try:
if (self.name not in instance._data or value_has_changed = (
instance._data[self.name] != value): self.name not in instance._data or
instance._data[self.name] != value
)
if value_has_changed:
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
except Exception: except Exception:
# Values cant be compared eg: naive and tz datetimes # Some values can't be compared and throw an error when we
# So mark it as changed # attempt to do so (e.g. tz-naive and tz-aware datetimes).
# Mark the field as changed in such cases.
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
@@ -157,6 +160,7 @@ class BaseField(object):
for v in value: for v in value:
if isinstance(v, EmbeddedDocument): if isinstance(v, EmbeddedDocument):
v._instance = weakref.proxy(instance) v._instance = weakref.proxy(instance)
instance._data[self.name] = value instance._data[self.name] = value
def error(self, message='', errors=None, field_name=None): def error(self, message='', errors=None, field_name=None):

View File

@@ -544,7 +544,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
@property @property
def _qs(self): def _qs(self):
"""Return the queryset to use for updating / reloading / deletions.""" """Return the default queryset corresponding to this document."""
if not hasattr(self, '__objects'): if not hasattr(self, '__objects'):
self.__objects = QuerySet(self, self._get_collection()) self.__objects = QuerySet(self, self._get_collection())
return self.__objects return self.__objects
@@ -552,9 +552,11 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
@property @property
def _object_key(self): def _object_key(self):
"""Get the query dict that can be used to fetch this object from """Get the query dict that can be used to fetch this object from
the database. Most of the time it's a simple PK lookup, but in the database.
case of a sharded collection with a compound shard key, it can
contain a more complex query. Most of the time the dict is a simple PK lookup, but in case of
a sharded collection with a compound shard key, it can contain a more
complex query.
""" """
select_dict = {'pk': self.pk} select_dict = {'pk': self.pk}
shard_key = self.__class__._meta.get('shard_key', tuple()) shard_key = self.__class__._meta.get('shard_key', tuple())

View File

@@ -73,6 +73,7 @@ class BaseQuerySet(object):
self._initial_query = { self._initial_query = {
'_cls': {'$in': self._document._subclasses}} '_cls': {'$in': self._document._subclasses}}
self._loaded_fields = QueryFieldList(always_include=['_cls']) self._loaded_fields = QueryFieldList(always_include=['_cls'])
self._cursor_obj = None self._cursor_obj = None
self._limit = None self._limit = None
self._skip = None self._skip = None
@@ -707,8 +708,9 @@ class BaseQuerySet(object):
return queryset return queryset
def no_sub_classes(self): def no_sub_classes(self):
""" """Filter for only the instances of this specific document.
Only return instances of this document and not any inherited documents
Do NOT return any inherited documents.
""" """
if self._document._meta.get('allow_inheritance') is True: if self._document._meta.get('allow_inheritance') is True:
self._initial_query = {'_cls': self._document._class_name} self._initial_query = {'_cls': self._document._class_name}

View File

@@ -3525,5 +3525,76 @@ class InstanceTest(MongoDBTestCase):
User.objects().select_related() User.objects().select_related()
class DocumentToDictTest(MongoDBTestCase):
"""Class for testing the BaseDocument.to_dict method."""
def test_to_dict(self):
class Person(Document):
name = StringField()
age = IntField()
p = Person(name='Tom', age=30)
self.assertEqual(p.to_dict(), {'id': None, 'name': 'Tom', 'age': 30})
def test_to_dict_with_a_persisted_doc(self):
class Person(Document):
name = StringField()
age = IntField()
p = Person.objects.create(name='Tom', age=30)
p_dict = p.to_dict()
self.assertTrue(p_dict['id'])
self.assertEqual(p_dict['name'], 'Tom')
self.assertEqual(p_dict['age'], 30)
def test_to_dict_empty_doc(self):
class Person(Document):
name = StringField()
age = IntField()
p = Person()
self.assertEqual(p.to_dict(), {'id': None, 'name': None, 'age': None})
def test_to_dict_with_default_values(self):
class Person(Document):
name = StringField(default='Unknown')
age = IntField(default=0)
p = Person()
self.assertEqual(
p.to_dict(),
{'id': None, 'name': 'Unknown', 'age': 0}
)
def test_to_dict_with_a_db_field(self):
class Person(Document):
name = StringField(db_field='db_name')
p = Person(name='Tom')
self.assertEqual(p.to_dict(), {'id': None, 'name': 'Tom'})
def test_to_dict_with_a_primary_key(self):
class Person(Document):
username = StringField(primary_key=True)
p = Person(username='tomtom')
self.assertEqual(p.to_dict(), {'username': 'tomtom'})
def test_to_dict_with_an_embedded_document(self):
class Book(EmbeddedDocument):
title = StringField()
class Author(Document):
name = StringField()
book = EmbeddedDocumentField(Book)
a = Author(name='Yuval', book=Book(title='Sapiens'))
self.assertEqual(a.to_dict(), {
'id': None,
'name': 'Yuval',
'book': {'title': 'Sapiens'}
})
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()