From b1eeb77ddcdb0432b36740af5fe276ec791a40d8 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Tue, 21 Aug 2012 17:45:51 +0100 Subject: [PATCH] Added FutureWarning - save will default to `cascade=False` in 0.8 --- docs/changelog.rst | 3 +- docs/upgrade.rst | 14 +++++++ mongoengine/document.py | 89 ++++++++++++++++++++++++----------------- tests/test_document.py | 34 ++++++++++++++++ 4 files changed, 103 insertions(+), 37 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2feda743..4b55f517 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Changes in 0.7.X ================= +- Added FutureWarning - save will default to `cascade=False` in 0.8 - Added example of indexing embedded document fields (MongoEngine/mongoengine#75) - Fixed ImageField resizing when forcing size (MongoEngine/mongoengine#80) - Add flexibility for fields handling bad data (MongoEngine/mongoengine#78) @@ -99,7 +100,7 @@ Changes in 0.6.8 ================ - Fixed FileField losing reference when no default set - Removed possible race condition from FileField (grid_file) -- Added assignment to save, can now do: b = MyDoc(**kwargs).save() +- Added assignment to save, can now do: `b = MyDoc(**kwargs).save()` - Added support for pull operations on nested EmbeddedDocuments - Added support for choices with GenericReferenceFields - Added support for choices with GenericEmbeddedDocumentFields diff --git a/docs/upgrade.rst b/docs/upgrade.rst index bfb5cc51..4f92fdfd 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -2,6 +2,20 @@ Upgrading ========= +0.6 to 0.7 +========== + +Saves will raise a `FutureWarning` if they cascade and cascade hasn't been set to +True. This is because in 0.8 it will default to False. If you require cascading +saves then either set it in the `meta` or pass via `save` :: + + # At the class level: + class Person(Document): + meta = {'cascade': True} + + # Or in code: + my_document.save(cascade=True) + 0.5 to 0.6 ========== diff --git a/mongoengine/document.py b/mongoengine/document.py index 1b55e433..da73f88b 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,15 +1,18 @@ +import warnings + import pymongo from bson.dbref import DBRef - from mongoengine import signals, queryset + from base import (DocumentMetaclass, TopLevelDocumentMetaclass, BaseDocument, BaseDict, BaseList) from queryset import OperationError from connection import get_db, DEFAULT_CONNECTION_NAME __all__ = ['Document', 'EmbeddedDocument', 'DynamicDocument', - 'DynamicEmbeddedDocument', 'OperationError', 'InvalidCollectionError'] + 'DynamicEmbeddedDocument', 'OperationError', + 'InvalidCollectionError'] class InvalidCollectionError(Exception): @@ -134,8 +137,9 @@ class Document(BaseDocument): options = cls._collection.options() if options.get('max') != max_documents or \ options.get('size') != max_size: - msg = ('Cannot create collection "%s" as a capped ' - 'collection as it already exists') % cls._collection + msg = (('Cannot create collection "%s" as a capped ' + 'collection as it already exists') + % cls._collection) raise InvalidCollectionError(msg) else: # Create the collection as a capped collection @@ -149,8 +153,9 @@ class Document(BaseDocument): cls._collection = db[collection_name] return cls._collection - def save(self, safe=True, force_insert=False, validate=True, write_options=None, - cascade=None, cascade_kwargs=None, _refs=None): + def save(self, safe=True, force_insert=False, validate=True, + write_options=None, cascade=None, cascade_kwargs=None, + _refs=None): """Save the :class:`~mongoengine.Document` to the database. If the document already exists, it will be updated, otherwise it will be created. @@ -163,27 +168,30 @@ class Document(BaseDocument): updates of existing documents :param validate: validates the document; set to ``False`` to skip. :param write_options: Extra keyword arguments are passed down to - :meth:`~pymongo.collection.Collection.save` OR - :meth:`~pymongo.collection.Collection.insert` - which will be used as options for the resultant ``getLastError`` command. - For example, ``save(..., write_options={w: 2, fsync: True}, ...)`` will - wait until at least two servers have recorded the write and will force an - fsync on each server being written to. - :param cascade: Sets the flag for cascading saves. You can set a default by setting - "cascade" in the document __meta__ - :param cascade_kwargs: optional kwargs dictionary to be passed throw to cascading saves + :meth:`~pymongo.collection.Collection.save` OR + :meth:`~pymongo.collection.Collection.insert` + which will be used as options for the resultant + ``getLastError`` command. For example, + ``save(..., write_options={w: 2, fsync: True}, ...)`` will + wait until at least two servers have recorded the write and + will force an fsync on the primary server. + :param cascade: Sets the flag for cascading saves. You can set a + default by setting "cascade" in the document __meta__ + :param cascade_kwargs: optional kwargs dictionary to be passed throw + to cascading saves :param _refs: A list of processed references used in cascading saves .. versionchanged:: 0.5 - In existing documents it only saves changed fields using set / unset - Saves are cascaded and any :class:`~bson.dbref.DBRef` objects - that have changes are saved as well. + In existing documents it only saves changed fields using + set / unset. Saves are cascaded and any + :class:`~bson.dbref.DBRef` objects that have changes are + saved as well. .. versionchanged:: 0.6 - Cascade saves are optional = defaults to True, if you want fine grain - control then you can turn off using document meta['cascade'] = False - Also you can pass different kwargs to the cascade save using cascade_kwargs - which overwrites the existing kwargs with custom values - + Cascade saves are optional = defaults to True, if you want + fine grain control then you can turn off using document + meta['cascade'] = False Also you can pass different kwargs to + the cascade save using cascade_kwargs which overwrites the + existing kwargs with custom values """ signals.pre_save.send(self.__class__, document=self) @@ -201,9 +209,11 @@ class Document(BaseDocument): collection = self.__class__.objects._collection if created: if force_insert: - object_id = collection.insert(doc, safe=safe, **write_options) + object_id = collection.insert(doc, safe=safe, + **write_options) else: - object_id = collection.save(doc, safe=safe, **write_options) + object_id = collection.save(doc, safe=safe, + **write_options) else: object_id = doc['_id'] updates, removals = self._delta() @@ -216,11 +226,15 @@ class Document(BaseDocument): upsert = self._created if updates: - collection.update(select_dict, {"$set": updates}, upsert=upsert, safe=safe, **write_options) + collection.update(select_dict, {"$set": updates}, + upsert=upsert, safe=safe, **write_options) if removals: - collection.update(select_dict, {"$unset": removals}, upsert=upsert, safe=safe, **write_options) + collection.update(select_dict, {"$unset": removals}, + upsert=upsert, safe=safe, **write_options) - cascade = self._meta.get('cascade', True) if cascade is None else cascade + warn_cascade = not cascade and 'cascade' not in self._meta + cascade = (self._meta.get('cascade', True) + if cascade is None else cascade) if cascade: kwargs = { "safe": safe, @@ -232,8 +246,7 @@ class Document(BaseDocument): if cascade_kwargs: # Allow granular control over cascades kwargs.update(cascade_kwargs) kwargs['_refs'] = _refs - #self._changed_fields = [] - self.cascade_save(**kwargs) + self.cascade_save(warn_cascade=warn_cascade, **kwargs) except pymongo.errors.OperationFailure, err: message = 'Could not save document (%s)' @@ -249,23 +262,27 @@ class Document(BaseDocument): signals.post_save.send(self.__class__, document=self, created=created) return self - def cascade_save(self, *args, **kwargs): - """Recursively saves any references / generic references on an object""" + def cascade_save(self, warn_cascade=None, *args, **kwargs): + """Recursively saves any references / + generic references on an objects""" import fields _refs = kwargs.get('_refs', []) or [] for name, cls in self._fields.items(): - if not isinstance(cls, (fields.ReferenceField, fields.GenericReferenceField)): + if not isinstance(cls, (fields.ReferenceField, + fields.GenericReferenceField)): continue ref = getattr(self, name) - if not ref: - continue - if isinstance(ref, DBRef): + if not ref or isinstance(ref, DBRef): continue ref_id = "%s,%s" % (ref.__class__.__name__, str(ref._data)) if ref and ref_id not in _refs: + if warn_cascade: + msg = ("Cascading saves will default to off in 0.8, " + "please explicitly set `.save(cascade=True)`") + warnings.warn(msg, FutureWarning) _refs.append(ref_id) kwargs["_refs"] = _refs ref.save(**kwargs) diff --git a/tests/test_document.py b/tests/test_document.py index f1c65b1c..fdca119f 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -1508,6 +1508,40 @@ class DocumentTest(unittest.TestCase): p1.reload() self.assertEqual(p1.name, p.parent.name) + def test_cascade_warning(self): + + self.warning_list = [] + showwarning_default = warnings.showwarning + + def append_to_warning_list(message, category, *args): + self.warning_list.append({"message": message, + "category": category}) + + # add warnings to self.warning_list instead of stderr + warnings.showwarning = append_to_warning_list + + class Person(Document): + name = StringField() + parent = ReferenceField('self') + + Person.drop_collection() + + p1 = Person(name="Wilson Snr") + p1.parent = None + p1.save() + + p2 = Person(name="Wilson Jr") + p2.parent = p1 + p2.save() + + # restore default handling of warnings + warnings.showwarning = showwarning_default + self.assertEqual(len(self.warning_list), 1) + warning = self.warning_list[0] + self.assertEqual(FutureWarning, warning["category"]) + self.assertTrue("Cascading saves will default to off in 0.8" + in str(warning["message"])) + def test_save_cascade_kwargs(self): class Person(Document):