• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bagerard / mongoengine / 16950450531

13 Aug 2025 10:07PM UTC coverage: 94.028% (-0.5%) from 94.481%
16950450531

push

github

bagerard
add useful cr comment for .path in install_mongo

5322 of 5660 relevant lines covered (94.03%)

1.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.91
/mongoengine/document.py
1
import re
2✔
2

3
import pymongo
2✔
4
from bson.dbref import DBRef
2✔
5
from pymongo.read_preferences import ReadPreference
2✔
6

7
from mongoengine import signals
2✔
8
from mongoengine.base import (
2✔
9
    BaseDict,
10
    BaseDocument,
11
    BaseList,
12
    DocumentMetaclass,
13
    EmbeddedDocumentList,
14
    TopLevelDocumentMetaclass,
15
    _DocumentRegistry,
16
)
17
from mongoengine.base.utils import NonOrderedList
2✔
18
from mongoengine.common import _import_class
2✔
19
from mongoengine.connection import (
2✔
20
    DEFAULT_CONNECTION_NAME,
21
    _get_session,
22
    get_db,
23
)
24
from mongoengine.context_managers import (
2✔
25
    set_write_concern,
26
    switch_collection,
27
    switch_db,
28
)
29
from mongoengine.errors import (
2✔
30
    InvalidDocumentError,
31
    InvalidQueryError,
32
    SaveConditionError,
33
)
34
from mongoengine.pymongo_support import list_collection_names
2✔
35
from mongoengine.queryset import (
2✔
36
    NotUniqueError,
37
    OperationError,
38
    QuerySet,
39
    transform,
40
)
41

42
__all__ = (
2✔
43
    "Document",
44
    "EmbeddedDocument",
45
    "DynamicDocument",
46
    "DynamicEmbeddedDocument",
47
    "OperationError",
48
    "InvalidCollectionError",
49
    "NotUniqueError",
50
    "MapReduceDocument",
51
)
52

53

54
def includes_cls(fields):
2✔
55
    """Helper function used for ensuring and comparing indexes."""
56
    first_field = None
2✔
57
    if len(fields):
2✔
58
        if isinstance(fields[0], str):
2✔
59
            first_field = fields[0]
×
60
        elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
2✔
61
            first_field = fields[0][0]
2✔
62
    return first_field == "_cls"
2✔
63

64

65
class InvalidCollectionError(Exception):
2✔
66
    pass
2✔
67

68

69
class EmbeddedDocument(BaseDocument, metaclass=DocumentMetaclass):
2✔
70
    r"""A :class:`~mongoengine.Document` that isn't stored in its own
71
    collection.  :class:`~mongoengine.EmbeddedDocument`\ s should be used as
72
    fields on :class:`~mongoengine.Document`\ s through the
73
    :class:`~mongoengine.EmbeddedDocumentField` field type.
74

75
    A :class:`~mongoengine.EmbeddedDocument` subclass may be itself subclassed,
76
    to create a specialised version of the embedded document that will be
77
    stored in the same collection. To facilitate this behaviour a `_cls`
78
    field is added to documents (hidden though the MongoEngine interface).
79
    To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the
80
    :attr:`meta` dictionary.
81
    """
82

83
    __slots__ = ("_instance",)
2✔
84

85
    # my_metaclass is defined so that metaclass can be queried in Python 2 & 3
86
    my_metaclass = DocumentMetaclass
2✔
87

88
    # A generic embedded document doesn't have any immutable properties
89
    # that describe it uniquely, hence it shouldn't be hashable. You can
90
    # define your own __hash__ method on a subclass if you need your
91
    # embedded documents to be hashable.
92
    __hash__ = None
2✔
93

94
    def __init__(self, *args, **kwargs):
2✔
95
        super().__init__(*args, **kwargs)
2✔
96
        self._instance = None
2✔
97
        self._changed_fields = []
2✔
98

99
    def __eq__(self, other):
2✔
100
        if isinstance(other, self.__class__):
2✔
101
            return self._data == other._data
2✔
102
        return False
2✔
103

104
    def __ne__(self, other):
2✔
105
        return not self.__eq__(other)
2✔
106

107
    def __getstate__(self):
2✔
108
        data = super().__getstate__()
2✔
109
        data["_instance"] = None
2✔
110
        return data
2✔
111

112
    def __setstate__(self, state):
2✔
113
        super().__setstate__(state)
2✔
114
        self._instance = state["_instance"]
2✔
115

116
    def to_mongo(self, *args, **kwargs):
2✔
117
        data = super().to_mongo(*args, **kwargs)
2✔
118

119
        # remove _id from the SON if it's in it and it's None
120
        if "_id" in data and data["_id"] is None:
2✔
121
            del data["_id"]
2✔
122

123
        return data
2✔
124

125

126
class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
2✔
127
    """The base class used for defining the structure and properties of
128
    collections of documents stored in MongoDB. Inherit from this class, and
129
    add fields as class attributes to define a document's structure.
130
    Individual documents may then be created by making instances of the
131
    :class:`~mongoengine.Document` subclass.
132

133
    By default, the MongoDB collection used to store documents created using a
134
    :class:`~mongoengine.Document` subclass will be the name of the subclass
135
    converted to snake_case. A different collection may be specified by
136
    providing :attr:`collection` to the :attr:`meta` dictionary in the class
137
    definition.
138

139
    A :class:`~mongoengine.Document` subclass may be itself subclassed, to
140
    create a specialised version of the document that will be stored in the
141
    same collection. To facilitate this behaviour a `_cls`
142
    field is added to documents (hidden though the MongoEngine interface).
143
    To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the
144
    :attr:`meta` dictionary.
145

146
    A :class:`~mongoengine.Document` may use a **Capped Collection** by
147
    specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
148
    dictionary. :attr:`max_documents` is the maximum number of documents that
149
    is allowed to be stored in the collection, and :attr:`max_size` is the
150
    maximum size of the collection in bytes. :attr:`max_size` is rounded up
151
    to the next multiple of 256 by MongoDB internally and mongoengine before.
152
    Use also a multiple of 256 to avoid confusions.  If :attr:`max_size` is not
153
    specified and :attr:`max_documents` is, :attr:`max_size` defaults to
154
    10485760 bytes (10MB).
155

156
    Indexes may be created by specifying :attr:`indexes` in the :attr:`meta`
157
    dictionary. The value should be a list of field names or tuples of field
158
    names. Index direction may be specified by prefixing the field names with
159
    a **+** or **-** sign.
160

161
    Automatic index creation can be disabled by specifying
162
    :attr:`auto_create_index` in the :attr:`meta` dictionary. If this is set to
163
    False then indexes will not be created by MongoEngine.  This is useful in
164
    production systems where index creation is performed as part of a
165
    deployment system.
166

167
    By default, _cls will be added to the start of every index (that
168
    doesn't contain a list) if allow_inheritance is True. This can be
169
    disabled by either setting cls to False on the specific index or
170
    by setting index_cls to False on the meta dictionary for the document.
171

172
    By default, any extra attribute existing in stored data but not declared
173
    in your model will raise a :class:`~mongoengine.FieldDoesNotExist` error.
174
    This can be disabled by setting :attr:`strict` to ``False``
175
    in the :attr:`meta` dictionary.
176
    """
177

178
    # my_metaclass is defined so that metaclass can be queried in Python 2 & 3
179
    my_metaclass = TopLevelDocumentMetaclass
2✔
180

181
    __slots__ = ("__objects",)
2✔
182

183
    @property
2✔
184
    def pk(self):
2✔
185
        """Get the primary key."""
186
        if "id_field" not in self._meta:
2✔
187
            return None
2✔
188
        return getattr(self, self._meta["id_field"])
2✔
189

190
    @pk.setter
2✔
191
    def pk(self, value):
2✔
192
        """Set the primary key."""
193
        return setattr(self, self._meta["id_field"], value)
2✔
194

195
    def __hash__(self):
2✔
196
        """Return the hash based on the PK of this document. If it's new
197
        and doesn't have a PK yet, return the default object hash instead.
198
        """
199
        if self.pk is None:
2✔
200
            return super(BaseDocument, self).__hash__()
2✔
201

202
        return hash(self.pk)
2✔
203

204
    @classmethod
2✔
205
    def _get_db(cls):
2✔
206
        """Some Model using other db_alias"""
207
        return get_db(cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME))
2✔
208

209
    @classmethod
2✔
210
    def _disconnect(cls):
2✔
211
        """Detach the Document class from the (cached) database collection"""
212
        cls._collection = None
2✔
213

214
    @classmethod
2✔
215
    def _get_collection(cls):
2✔
216
        """Return the PyMongo collection corresponding to this document.
217

218
        Upon first call, this method:
219
        1. Initializes a :class:`~pymongo.collection.Collection` corresponding
220
           to this document.
221
        2. Creates indexes defined in this document's :attr:`meta` dictionary.
222
           This happens only if `auto_create_index` is True.
223
        """
224
        if not hasattr(cls, "_collection") or cls._collection is None:
2✔
225
            # Get the collection, either capped or regular.
226
            if cls._meta.get("max_size") or cls._meta.get("max_documents"):
2✔
227
                cls._collection = cls._get_capped_collection()
2✔
228
            elif cls._meta.get("timeseries"):
2✔
229
                cls._collection = cls._get_timeseries_collection()
×
230
            else:
231
                db = cls._get_db()
2✔
232
                collection_name = cls._get_collection_name()
2✔
233
                cls._collection = db[collection_name]
2✔
234

235
            # Ensure indexes on the collection unless auto_create_index was
236
            # set to False. Plus, there is no need to ensure indexes on slave.
237
            db = cls._get_db()
2✔
238
            if cls._meta.get("auto_create_index", True) and db.client.is_primary:
2✔
239
                cls.ensure_indexes()
2✔
240

241
        return cls._collection
2✔
242

243
    @classmethod
2✔
244
    def _get_capped_collection(cls):
2✔
245
        """Create a new or get an existing capped PyMongo collection."""
246
        db = cls._get_db()
2✔
247
        collection_name = cls._get_collection_name()
2✔
248

249
        # Get max document limit and max byte size from meta.
250
        max_size = cls._meta.get("max_size") or 10 * 2**20  # 10MB default
2✔
251
        max_documents = cls._meta.get("max_documents")
2✔
252

253
        # MongoDB will automatically raise the size to make it a multiple of
254
        # 256 bytes. We raise it here ourselves to be able to reliably compare
255
        # the options below.
256
        if max_size % 256:
2✔
257
            max_size = (max_size // 256 + 1) * 256
2✔
258

259
        # If the collection already exists and has different options
260
        # (i.e. isn't capped or has different max/size), raise an error.
261
        if collection_name in list_collection_names(
2✔
262
            db, include_system_collections=True
263
        ):
264
            collection = db[collection_name]
2✔
265
            options = collection.options()
2✔
266
            if options.get("max") != max_documents or options.get("size") != max_size:
2✔
267
                raise InvalidCollectionError(
2✔
268
                    'Cannot create collection "{}" as a capped '
269
                    "collection as it already exists".format(cls._collection)
270
                )
271

272
            return collection
2✔
273

274
        # Create a new capped collection.
275
        opts = {"capped": True, "size": max_size}
2✔
276
        if max_documents:
2✔
277
            opts["max"] = max_documents
2✔
278

279
        return db.create_collection(collection_name, session=_get_session(), **opts)
2✔
280

281
    @classmethod
2✔
282
    def _get_timeseries_collection(cls):
2✔
283
        """Create a new or get an existing timeseries PyMongo collection."""
284
        db = cls._get_db()
×
285
        collection_name = cls._get_collection_name()
×
286
        timeseries_opts = cls._meta.get("timeseries")
×
287

288
        if collection_name in list_collection_names(
×
289
            db, include_system_collections=True
290
        ):
291
            collection = db[collection_name]
×
292
            collection.options()
×
293
            return collection
×
294

295
        opts = {"expireAfterSeconds": timeseries_opts.pop("expireAfterSeconds", None)}
×
296
        return db.create_collection(
×
297
            name=collection_name,
298
            timeseries=timeseries_opts,
299
            **opts,
300
        )
301

302
    def to_mongo(self, *args, **kwargs):
2✔
303
        data = super().to_mongo(*args, **kwargs)
2✔
304

305
        # If '_id' is None, try and set it from self._data. If that
306
        # doesn't exist either, remove '_id' from the SON completely.
307
        if data["_id"] is None:
2✔
308
            if self._data.get("id") is None:
2✔
309
                del data["_id"]
2✔
310
            else:
311
                data["_id"] = self._data["id"]
2✔
312

313
        return data
2✔
314

315
    def modify(self, query=None, **update):
2✔
316
        """Perform an atomic update of the document in the database and reload
317
        the document object using updated version.
318

319
        Returns True if the document has been updated or False if the document
320
        in the database doesn't match the query.
321

322
        .. note:: All unsaved changes that have been made to the document are
323
            rejected if the method returns True.
324

325
        :param query: the update will be performed only if the document in the
326
            database matches the query
327
        :param update: Django-style update keyword arguments
328
        """
329
        if query is None:
2✔
330
            query = {}
2✔
331

332
        if self.pk is None:
2✔
333
            raise InvalidDocumentError("The document does not have a primary key.")
2✔
334

335
        id_field = self._meta["id_field"]
2✔
336
        query = query.copy() if isinstance(query, dict) else query.to_query(self)
2✔
337

338
        if id_field not in query:
2✔
339
            query[id_field] = self.pk
2✔
340
        elif query[id_field] != self.pk:
2✔
341
            raise InvalidQueryError(
2✔
342
                "Invalid document modify query: it must modify only this document."
343
            )
344

345
        # Need to add shard key to query, or you get an error
346
        query.update(self._object_key)
2✔
347

348
        updated = self._qs(**query).modify(new=True, **update)
2✔
349
        if updated is None:
2✔
350
            return False
2✔
351

352
        for field in self._fields_ordered:
2✔
353
            setattr(self, field, self._reload(field, updated[field]))
2✔
354

355
        self._changed_fields = updated._changed_fields
2✔
356
        self._created = False
2✔
357

358
        return True
2✔
359

360
    def save(
2✔
361
        self,
362
        force_insert=False,
363
        validate=True,
364
        clean=True,
365
        write_concern=None,
366
        cascade=None,
367
        cascade_kwargs=None,
368
        _refs=None,
369
        save_condition=None,
370
        signal_kwargs=None,
371
        **kwargs,
372
    ):
373
        """Save the :class:`~mongoengine.Document` to the database. If the
374
        document already exists, it will be updated, otherwise it will be
375
        created. Returns the saved object instance.
376

377
        :param force_insert: only try to create a new document, don't allow
378
            updates of existing documents.
379
        :param validate: validates the document; set to ``False`` to skip.
380
        :param clean: call the document clean method, requires `validate` to be
381
            True.
382
        :param write_concern: Extra keyword arguments are passed down to
383
            :meth:`~pymongo.collection.Collection.save` OR
384
            :meth:`~pymongo.collection.Collection.insert`
385
            which will be used as options for the resultant
386
            ``getLastError`` command.  For example,
387
            ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
388
            wait until at least two servers have recorded the write and
389
            will force an fsync on the primary server.
390
        :param cascade: Sets the flag for cascading saves.  You can set a
391
            default by setting "cascade" in the document __meta__
392
        :param cascade_kwargs: (optional) kwargs dictionary to be passed throw
393
            to cascading saves.  Implies ``cascade=True``.
394
        :param _refs: A list of processed references used in cascading saves
395
        :param save_condition: only perform save if matching record in db
396
            satisfies condition(s) (e.g. version number).
397
            Raises :class:`OperationError` if the conditions are not satisfied
398
        :param signal_kwargs: (optional) kwargs dictionary to be passed to
399
            the signal calls.
400

401
        .. versionchanged:: 0.5
402
            In existing documents it only saves changed fields using
403
            set / unset.  Saves are cascaded and any
404
            :class:`~bson.dbref.DBRef` objects that have changes are
405
            saved as well.
406
        .. versionchanged:: 0.6
407
            Added cascading saves
408
        .. versionchanged:: 0.8
409
            Cascade saves are optional and default to False.  If you want
410
            fine grain control then you can turn off using document
411
            meta['cascade'] = True.  Also you can pass different kwargs to
412
            the cascade save using cascade_kwargs which overwrites the
413
            existing kwargs with custom values.
414
        .. versionchanged:: 0.26
415
           save() no longer calls :meth:`~mongoengine.Document.ensure_indexes`
416
           unless ``meta['auto_create_index_on_save']`` is set to True.
417

418
        """
419
        signal_kwargs = signal_kwargs or {}
2✔
420

421
        if self._meta.get("abstract"):
2✔
422
            raise InvalidDocumentError("Cannot save an abstract document.")
2✔
423

424
        signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
2✔
425

426
        if validate:
2✔
427
            self.validate(clean=clean)
2✔
428

429
        if write_concern is None:
2✔
430
            write_concern = {}
2✔
431

432
        doc_id = self.to_mongo(fields=[self._meta["id_field"]])
2✔
433
        created = "_id" not in doc_id or self._created or force_insert
2✔
434

435
        signals.pre_save_post_validation.send(
2✔
436
            self.__class__, document=self, created=created, **signal_kwargs
437
        )
438
        # it might be refreshed by the pre_save_post_validation hook, e.g., for etag generation
439
        doc = self.to_mongo()
2✔
440

441
        # Initialize the Document's underlying pymongo.Collection (+create indexes) if not already initialized
442
        # Important to do this here to avoid that the index creation gets wrapped in the try/except block below
443
        # and turned into mongoengine.OperationError
444
        if self._collection is None:
2✔
445
            _ = self._get_collection()
2✔
446
        elif self._meta.get("auto_create_index_on_save", False):
2✔
447
            # ensure_indexes is called as part of _get_collection so no need to re-call it again here
448
            self.ensure_indexes()
2✔
449

450
        try:
2✔
451
            # Save a new document or update an existing one
452
            if created:
2✔
453
                object_id = self._save_create(
2✔
454
                    doc=doc, force_insert=force_insert, write_concern=write_concern
455
                )
456
            else:
457
                object_id, created = self._save_update(
2✔
458
                    doc, save_condition, write_concern
459
                )
460

461
            if cascade is None:
2✔
462
                cascade = self._meta.get("cascade", False) or cascade_kwargs is not None
2✔
463

464
            if cascade:
2✔
465
                kwargs = {
2✔
466
                    "force_insert": force_insert,
467
                    "validate": validate,
468
                    "write_concern": write_concern,
469
                    "cascade": cascade,
470
                }
471
                if cascade_kwargs:  # Allow granular control over cascades
2✔
472
                    kwargs.update(cascade_kwargs)
2✔
473
                kwargs["_refs"] = _refs
2✔
474
                self.cascade_save(**kwargs)
2✔
475

476
        except pymongo.errors.DuplicateKeyError as err:
2✔
477
            message = "Tried to save duplicate unique keys (%s)"
2✔
478
            raise NotUniqueError(message % err)
2✔
479
        except pymongo.errors.OperationFailure as err:
2✔
480
            message = "Could not save document (%s)"
×
481
            if re.match("^E1100[01] duplicate key", str(err)):
×
482
                # E11000 - duplicate key error index
483
                # E11001 - duplicate key on update
484
                message = "Tried to save duplicate unique keys (%s)"
×
485
                raise NotUniqueError(message % err)
×
486
            raise OperationError(message % err)
×
487

488
        # Make sure we store the PK on this document now that it's saved
489
        id_field = self._meta["id_field"]
2✔
490
        if created or id_field not in self._meta.get("shard_key", []):
2✔
491
            self[id_field] = self._fields[id_field].to_python(object_id)
2✔
492

493
        signals.post_save.send(
2✔
494
            self.__class__, document=self, created=created, **signal_kwargs
495
        )
496

497
        self._clear_changed_fields()
2✔
498
        self._created = False
2✔
499

500
        return self
2✔
501

502
    def _save_create(self, doc, force_insert, write_concern):
2✔
503
        """Save a new document.
504

505
        Helper method, should only be used inside save().
506
        """
507
        collection = self._get_collection()
2✔
508
        with set_write_concern(collection, write_concern) as wc_collection:
2✔
509
            if force_insert:
2✔
510
                return wc_collection.insert_one(doc, session=_get_session()).inserted_id
2✔
511
            # insert_one will provoke UniqueError alongside save does not
512
            # therefore, it need to catch and call replace_one.
513
            if "_id" in doc:
2✔
514
                select_dict = {"_id": doc["_id"]}
2✔
515
                select_dict = self._integrate_shard_key(doc, select_dict)
2✔
516
                raw_object = wc_collection.find_one_and_replace(
2✔
517
                    select_dict, doc, session=_get_session()
518
                )
519
                if raw_object:
2✔
520
                    return doc["_id"]
2✔
521

522
            object_id = wc_collection.insert_one(
2✔
523
                doc, session=_get_session()
524
            ).inserted_id
525

526
        return object_id
2✔
527

528
    def _get_update_doc(self):
2✔
529
        """Return a dict containing all the $set and $unset operations
530
        that should be sent to MongoDB based on the changes made to this
531
        Document.
532
        """
533
        updates, removals = self._delta()
2✔
534

535
        update_doc = {}
2✔
536
        if updates:
2✔
537
            update_doc["$set"] = updates
2✔
538
        if removals:
2✔
539
            update_doc["$unset"] = removals
2✔
540

541
        return update_doc
2✔
542

543
    def _integrate_shard_key(self, doc, select_dict):
2✔
544
        """Integrates the collection's shard key to the `select_dict`, which will be used for the query.
545
        The value from the shard key is taken from the `doc` and finally the select_dict is returned.
546
        """
547

548
        # Need to add shard key to query, or you get an error
549
        shard_key = self._meta.get("shard_key", tuple())
2✔
550
        for k in shard_key:
2✔
551
            path = self._lookup_field(k.split("."))
2✔
552
            actual_key = [p.db_field for p in path]
2✔
553
            val = doc
2✔
554
            for ak in actual_key:
2✔
555
                val = val[ak]
2✔
556
            select_dict[".".join(actual_key)] = val
2✔
557

558
        return select_dict
2✔
559

560
    def _save_update(self, doc, save_condition, write_concern):
2✔
561
        """Update an existing document.
562

563
        Helper method, should only be used inside save().
564
        """
565
        collection = self._get_collection()
2✔
566
        object_id = doc["_id"]
2✔
567
        created = False
2✔
568

569
        select_dict = {}
2✔
570
        if save_condition is not None:
2✔
571
            select_dict = transform.query(self.__class__, **save_condition)
2✔
572

573
        select_dict["_id"] = object_id
2✔
574

575
        select_dict = self._integrate_shard_key(doc, select_dict)
2✔
576

577
        update_doc = self._get_update_doc()
2✔
578
        if update_doc:
2✔
579
            upsert = save_condition is None
2✔
580
            with set_write_concern(collection, write_concern) as wc_collection:
2✔
581
                last_error = wc_collection.update_one(
2✔
582
                    select_dict, update_doc, upsert=upsert, session=_get_session()
583
                ).raw_result
584
            if not upsert and last_error["n"] == 0:
2✔
585
                raise SaveConditionError(
2✔
586
                    "Race condition preventing document update detected"
587
                )
588
            if last_error is not None:
2✔
589
                updated_existing = last_error.get("updatedExisting")
2✔
590
                if updated_existing is False:
2✔
591
                    created = True
2✔
592
                    # !!! This is bad, means we accidentally created a new,
593
                    # potentially corrupted document. See
594
                    # https://github.com/MongoEngine/mongoengine/issues/564
595

596
        return object_id, created
2✔
597

598
    def cascade_save(self, **kwargs):
2✔
599
        """Recursively save any references and generic references on the
600
        document.
601
        """
602
        _refs = kwargs.get("_refs") or []
2✔
603

604
        ReferenceField = _import_class("ReferenceField")
2✔
605
        GenericReferenceField = _import_class("GenericReferenceField")
2✔
606

607
        for name, cls in self._fields.items():
2✔
608
            if not isinstance(cls, (ReferenceField, GenericReferenceField)):
2✔
609
                continue
2✔
610

611
            ref = self._data.get(name)
2✔
612
            if not ref or isinstance(ref, DBRef):
2✔
613
                continue
2✔
614

615
            if not getattr(ref, "_changed_fields", True):
2✔
616
                continue
2✔
617

618
            ref_id = f"{ref.__class__.__name__},{str(ref._data)}"
2✔
619
            if ref and ref_id not in _refs:
2✔
620
                _refs.append(ref_id)
2✔
621
                kwargs["_refs"] = _refs
2✔
622
                ref.save(**kwargs)
2✔
623
                ref._changed_fields = []
2✔
624

625
    @property
2✔
626
    def _qs(self):
2✔
627
        """Return the default queryset corresponding to this document."""
628
        if not hasattr(self, "__objects"):
2✔
629
            queryset_class = self._meta.get("queryset_class", QuerySet)
2✔
630
            self.__objects = queryset_class(self.__class__, self._get_collection())
2✔
631
        return self.__objects
2✔
632

633
    @property
2✔
634
    def _object_key(self):
2✔
635
        """Return a query dict that can be used to fetch this document.
636

637
        Most of the time the dict is a simple PK lookup, but in case of
638
        a sharded collection with a compound shard key, it can contain a more
639
        complex query.
640

641
        Note that the dict returned by this method uses MongoEngine field
642
        names instead of PyMongo field names (e.g. "pk" instead of "_id",
643
        "some__nested__field" instead of "some.nested.field", etc.).
644
        """
645
        select_dict = {"pk": self.pk}
2✔
646
        shard_key = self.__class__._meta.get("shard_key", tuple())
2✔
647
        for k in shard_key:
2✔
648
            val = self
2✔
649
            field_parts = k.split(".")
2✔
650
            for part in field_parts:
2✔
651
                val = getattr(val, part)
2✔
652
            select_dict["__".join(field_parts)] = val
2✔
653
        return select_dict
2✔
654

655
    def update(self, **kwargs):
2✔
656
        """Performs an update on the :class:`~mongoengine.Document`
657
        A convenience wrapper to :meth:`~mongoengine.QuerySet.update`.
658

659
        Raises :class:`OperationError` if called on an object that has not yet
660
        been saved.
661
        """
662
        if self.pk is None:
2✔
663
            if kwargs.get("upsert", False):
2✔
664
                query = self.to_mongo()
2✔
665
                if "_cls" in query:
2✔
666
                    del query["_cls"]
2✔
667
                return self._qs.filter(**query).update_one(**kwargs)
2✔
668
            else:
669
                raise OperationError("attempt to update a document not yet saved")
2✔
670

671
        # Need to add shard key to query, or you get an error
672
        return self._qs.filter(**self._object_key).update_one(**kwargs)
2✔
673

674
    def delete(self, signal_kwargs=None, **write_concern):
2✔
675
        """Delete the :class:`~mongoengine.Document` from the database. This
676
        will only take effect if the document has been previously saved.
677

678
        :param signal_kwargs: (optional) kwargs dictionary to be passed to
679
            the signal calls.
680
        :param write_concern: Extra keyword arguments are passed down which
681
            will be used as options for the resultant ``getLastError`` command.
682
            For example, ``save(..., w: 2, fsync: True)`` will
683
            wait until at least two servers have recorded the write and
684
            will force an fsync on the primary server.
685
        """
686
        signal_kwargs = signal_kwargs or {}
2✔
687
        signals.pre_delete.send(self.__class__, document=self, **signal_kwargs)
2✔
688

689
        # Delete FileFields separately
690
        FileField = _import_class("FileField")
2✔
691
        for name, field in self._fields.items():
2✔
692
            if isinstance(field, FileField):
2✔
693
                getattr(self, name).delete()
2✔
694

695
        try:
2✔
696
            self._qs.filter(**self._object_key).delete(
2✔
697
                write_concern=write_concern, _from_doc_delete=True
698
            )
699
        except pymongo.errors.OperationFailure as err:
2✔
700
            message = "Could not delete document (%s)" % err.args
×
701
            raise OperationError(message)
×
702
        signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
2✔
703

704
    def switch_db(self, db_alias, keep_created=True):
2✔
705
        """
706
        Temporarily switch the database for a document instance.
707

708
        Only really useful for archiving off data and calling `save()`::
709

710
            user = User.objects.get(id=user_id)
711
            user.switch_db('archive-db')
712
            user.save()
713

714
        :param str db_alias: The database alias to use for saving the document
715

716
        :param bool keep_created: keep self._created value after switching db, else is reset to True
717

718

719
        .. seealso::
720
            Use :class:`~mongoengine.context_managers.switch_collection`
721
            if you need to read from another collection
722
        """
723
        with switch_db(self.__class__, db_alias) as cls:
2✔
724
            collection = cls._get_collection()
2✔
725
            db = cls._get_db()
2✔
726
        self._get_collection = lambda: collection
2✔
727
        self._get_db = lambda: db
2✔
728
        self._collection = collection
2✔
729
        self._created = True if not keep_created else self._created
2✔
730
        self.__objects = self._qs
2✔
731
        self.__objects._collection_obj = collection
2✔
732
        return self
2✔
733

734
    def switch_collection(self, collection_name, keep_created=True):
2✔
735
        """
736
        Temporarily switch the collection for a document instance.
737

738
        Only really useful for archiving off data and calling `save()`::
739

740
            user = User.objects.get(id=user_id)
741
            user.switch_collection('old-users')
742
            user.save()
743

744
        :param str collection_name: The database alias to use for saving the
745
            document
746

747
        :param bool keep_created: keep self._created value after switching collection, else is reset to True
748

749

750
        .. seealso::
751
            Use :class:`~mongoengine.context_managers.switch_db`
752
            if you need to read from another database
753
        """
754
        with switch_collection(self.__class__, collection_name) as cls:
2✔
755
            collection = cls._get_collection()
2✔
756
        self._get_collection = lambda: collection
2✔
757
        self._collection = collection
2✔
758
        self._created = True if not keep_created else self._created
2✔
759
        self.__objects = self._qs
2✔
760
        self.__objects._collection_obj = collection
2✔
761
        return self
2✔
762

763
    def select_related(self, max_depth=1):
2✔
764
        """Handles dereferencing of :class:`~bson.dbref.DBRef` objects to
765
        a maximum depth in order to cut down the number queries to mongodb.
766
        """
767
        DeReference = _import_class("DeReference")
2✔
768
        DeReference()([self], max_depth + 1)
2✔
769
        return self
2✔
770

771
    def reload(self, *fields, **kwargs):
2✔
772
        """Reloads all attributes from the database.
773

774
        :param fields: (optional) args list of fields to reload
775
        :param max_depth: (optional) depth of dereferencing to follow
776
        """
777
        max_depth = 1
2✔
778
        if fields and isinstance(fields[0], int):
2✔
779
            max_depth = fields[0]
2✔
780
            fields = fields[1:]
2✔
781
        elif "max_depth" in kwargs:
2✔
782
            max_depth = kwargs["max_depth"]
×
783

784
        if self.pk is None:
2✔
785
            raise self.DoesNotExist("Document does not exist")
2✔
786

787
        obj = (
2✔
788
            self._qs.read_preference(ReadPreference.PRIMARY)
789
            .filter(**self._object_key)
790
            .only(*fields)
791
            .limit(1)
792
            .select_related(max_depth=max_depth)
793
        )
794

795
        if obj:
2✔
796
            obj = obj[0]
2✔
797
        else:
798
            raise self.DoesNotExist("Document does not exist")
2✔
799
        for field in obj._data:
2✔
800
            if not fields or field in fields:
2✔
801
                try:
2✔
802
                    setattr(self, field, self._reload(field, obj[field]))
2✔
803
                except (KeyError, AttributeError):
×
804
                    try:
×
805
                        # If field is a special field, e.g. items is stored as _reserved_items,
806
                        # a KeyError is thrown. So try to retrieve the field from _data
807
                        setattr(self, field, self._reload(field, obj._data.get(field)))
×
808
                    except KeyError:
×
809
                        # If field is removed from the database while the object
810
                        # is in memory, a reload would cause a KeyError
811
                        # i.e. obj.update(unset__field=1) followed by obj.reload()
812
                        delattr(self, field)
×
813

814
        self._changed_fields = (
2✔
815
            list(set(self._changed_fields) - set(fields))
816
            if fields
817
            else obj._changed_fields
818
        )
819
        self._created = False
2✔
820
        return self
2✔
821

822
    def _reload(self, key, value):
2✔
823
        """Used by :meth:`~mongoengine.Document.reload` to ensure the
824
        correct instance is linked to self.
825
        """
826
        if isinstance(value, BaseDict):
2✔
827
            value = [(k, self._reload(k, v)) for k, v in value.items()]
2✔
828
            value = BaseDict(value, self, key)
2✔
829
        elif isinstance(value, EmbeddedDocumentList):
2✔
830
            value = [self._reload(key, v) for v in value]
×
831
            value = EmbeddedDocumentList(value, self, key)
×
832
        elif isinstance(value, BaseList):
2✔
833
            value = [self._reload(key, v) for v in value]
2✔
834
            value = BaseList(value, self, key)
2✔
835
        elif isinstance(value, (EmbeddedDocument, DynamicEmbeddedDocument)):
2✔
836
            value._instance = None
2✔
837
            value._changed_fields = []
2✔
838
        return value
2✔
839

840
    def to_dbref(self):
2✔
841
        """Returns an instance of :class:`~bson.dbref.DBRef` useful in
842
        `__raw__` queries."""
843
        if self.pk is None:
2✔
844
            msg = "Only saved documents can have a valid dbref"
2✔
845
            raise OperationError(msg)
2✔
846
        return DBRef(self.__class__._get_collection_name(), self.pk)
2✔
847

848
    @classmethod
2✔
849
    def register_delete_rule(cls, document_cls, field_name, rule):
2✔
850
        """This method registers the delete rules to apply when removing this
851
        object.
852
        """
853
        classes = [
2✔
854
            _DocumentRegistry.get(class_name)
855
            for class_name in cls._subclasses
856
            if class_name != cls.__name__
857
        ] + [cls]
858
        documents = [
2✔
859
            _DocumentRegistry.get(class_name)
860
            for class_name in document_cls._subclasses
861
            if class_name != document_cls.__name__
862
        ] + [document_cls]
863

864
        for klass in classes:
2✔
865
            for document_cls in documents:
2✔
866
                delete_rules = klass._meta.get("delete_rules") or {}
2✔
867
                delete_rules[(document_cls, field_name)] = rule
2✔
868
                klass._meta["delete_rules"] = delete_rules
2✔
869

870
    @classmethod
2✔
871
    def drop_collection(cls):
2✔
872
        """Drops the entire collection associated with this
873
        :class:`~mongoengine.Document` type from the database.
874

875
        Raises :class:`OperationError` if the document has no collection set
876
        (i.g. if it is `abstract`)
877
        """
878
        coll_name = cls._get_collection_name()
2✔
879
        if not coll_name:
2✔
880
            raise OperationError(
2✔
881
                "Document %s has no collection defined (is it abstract ?)" % cls
882
            )
883
        cls._collection = None
2✔
884
        db = cls._get_db()
2✔
885
        db.drop_collection(coll_name, session=_get_session())
2✔
886

887
    @classmethod
2✔
888
    def create_index(cls, keys, background=False, **kwargs):
2✔
889
        """Creates the given indexes if required.
890

891
        :param keys: a single index key or a list of index keys (to
892
            construct a multi-field index); keys may be prefixed with a **+**
893
            or a **-** to determine the index ordering
894
        :param background: Allows index creation in the background
895
        """
896
        index_spec = cls._build_index_spec(keys)
2✔
897
        index_spec = index_spec.copy()
2✔
898
        fields = index_spec.pop("fields")
2✔
899
        index_spec["background"] = background
2✔
900
        index_spec.update(kwargs)
2✔
901

902
        return cls._get_collection().create_index(
2✔
903
            fields, session=_get_session(), **index_spec
904
        )
905

906
    @classmethod
2✔
907
    def ensure_indexes(cls):
2✔
908
        """Checks the document meta data and ensures all the indexes exist.
909

910
        Global defaults can be set in the meta - see :doc:`guide/defining-documents`
911

912
        By default, this will get called automatically upon first interaction with the
913
        Document collection (query, save, etc) so unless you disabled `auto_create_index`, you
914
        shouldn't have to call this manually.
915

916
        This also gets called upon every call to Document.save if `auto_create_index_on_save` is set to True
917

918
        If called multiple times, MongoDB will not re-recreate indexes if they exist already
919

920
        .. note:: You can disable automatic index creation by setting
921
                  `auto_create_index` to False in the documents meta data
922
        """
923
        background = cls._meta.get("index_background", False)
2✔
924
        index_opts = cls._meta.get("index_opts") or {}
2✔
925
        index_cls = cls._meta.get("index_cls", True)
2✔
926

927
        collection = cls._get_collection()
2✔
928

929
        # determine if an index which we are creating includes
930
        # _cls as its first field; if so, we can avoid creating
931
        # an extra index on _cls, as mongodb will use the existing
932
        # index to service queries against _cls
933
        cls_indexed = False
2✔
934

935
        # Ensure document-defined indexes are created
936
        if cls._meta["index_specs"]:
2✔
937
            index_spec = cls._meta["index_specs"]
2✔
938
            for spec in index_spec:
2✔
939
                spec = spec.copy()
2✔
940
                fields = spec.pop("fields")
2✔
941
                cls_indexed = cls_indexed or includes_cls(fields)
2✔
942
                opts = index_opts.copy()
2✔
943
                opts.update(spec)
2✔
944

945
                # we shouldn't pass 'cls' to the collection.ensureIndex options
946
                # because of https://jira.mongodb.org/browse/SERVER-769
947
                if "cls" in opts:
2✔
948
                    del opts["cls"]
×
949

950
                collection.create_index(
2✔
951
                    fields, background=background, session=_get_session(), **opts
952
                )
953

954
        # If _cls is being used (for polymorphism), it needs an index,
955
        # only if another index doesn't begin with _cls
956
        if index_cls and not cls_indexed and cls._meta.get("allow_inheritance"):
2✔
957
            # we shouldn't pass 'cls' to the collection.ensureIndex options
958
            # because of https://jira.mongodb.org/browse/SERVER-769
959
            if "cls" in index_opts:
2✔
960
                del index_opts["cls"]
×
961

962
            collection.create_index(
2✔
963
                "_cls", background=background, session=_get_session(), **index_opts
964
            )
965

966
    @classmethod
2✔
967
    def list_indexes(cls):
2✔
968
        """Lists all indexes that should be created for the Document collection.
969
        It includes all the indexes from super- and sub-classes.
970

971
        Note that it will only return the indexes' fields, not the indexes' options
972
        """
973
        if cls._meta.get("abstract"):
2✔
974
            return []
×
975

976
        # get all the base classes, subclasses and siblings
977
        classes = []
2✔
978

979
        def get_classes(cls):
2✔
980
            if cls not in classes and isinstance(cls, TopLevelDocumentMetaclass):
2✔
981
                classes.append(cls)
2✔
982

983
            for base_cls in cls.__bases__:
2✔
984
                if (
2✔
985
                    isinstance(base_cls, TopLevelDocumentMetaclass)
986
                    and base_cls != Document
987
                    and not base_cls._meta.get("abstract")
988
                    and base_cls._get_collection().full_name
989
                    == cls._get_collection().full_name
990
                    and base_cls not in classes
991
                ):
992
                    classes.append(base_cls)
2✔
993
                    get_classes(base_cls)
2✔
994
            for subclass in cls.__subclasses__():
2✔
995
                if (
2✔
996
                    isinstance(base_cls, TopLevelDocumentMetaclass)
997
                    and subclass._get_collection().full_name
998
                    == cls._get_collection().full_name
999
                    and subclass not in classes
1000
                ):
1001
                    classes.append(subclass)
2✔
1002
                    get_classes(subclass)
2✔
1003

1004
        get_classes(cls)
2✔
1005

1006
        # get the indexes spec for all the gathered classes
1007
        def get_indexes_spec(cls):
2✔
1008
            indexes = []
2✔
1009

1010
            if cls._meta["index_specs"]:
2✔
1011
                index_spec = cls._meta["index_specs"]
2✔
1012
                for spec in index_spec:
2✔
1013
                    spec = spec.copy()
2✔
1014
                    fields = spec.pop("fields")
2✔
1015
                    indexes.append(fields)
2✔
1016
            return indexes
2✔
1017

1018
        indexes = []
2✔
1019
        for klass in classes:
2✔
1020
            for index in get_indexes_spec(klass):
2✔
1021
                if index not in indexes:
2✔
1022
                    indexes.append(index)
2✔
1023

1024
        # finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed
1025
        if [("_id", 1)] not in indexes:
2✔
1026
            indexes.append([("_id", 1)])
2✔
1027
        if cls._meta.get("index_cls", True) and cls._meta.get("allow_inheritance"):
2✔
1028
            indexes.append([("_cls", 1)])
2✔
1029

1030
        return indexes
2✔
1031

1032
    @classmethod
2✔
1033
    def compare_indexes(cls):
2✔
1034
        """Compares the indexes defined in MongoEngine with the ones
1035
        existing in the database. Returns any missing/extra indexes.
1036
        """
1037

1038
        required = cls.list_indexes()
2✔
1039

1040
        existing = []
2✔
1041
        collection = cls._get_collection()
2✔
1042
        for info in collection.index_information(session=_get_session()).values():
2✔
1043
            if "_fts" in info["key"][0]:
2✔
1044
                # Useful for text indexes (but not only)
1045
                index_type = info["key"][0][1]
2✔
1046
                text_index_fields = info.get("weights").keys()
2✔
1047
                # Use NonOrderedList to avoid order comparison, see #2612
1048
                existing.append(
2✔
1049
                    NonOrderedList([(key, index_type) for key in text_index_fields])
1050
                )
1051
            else:
1052
                existing.append(info["key"])
2✔
1053

1054
        missing = [index for index in required if index not in existing]
2✔
1055
        extra = [index for index in existing if index not in required]
2✔
1056

1057
        # if { _cls: 1 } is missing, make sure it's *really* necessary
1058
        if [("_cls", 1)] in missing:
2✔
1059
            cls_obsolete = False
×
1060
            for index in existing:
×
1061
                if includes_cls(index) and index not in extra:
×
1062
                    cls_obsolete = True
×
1063
                    break
×
1064
            if cls_obsolete:
×
1065
                missing.remove([("_cls", 1)])
×
1066

1067
        return {"missing": missing, "extra": extra}
2✔
1068

1069

1070
class DynamicDocument(Document, metaclass=TopLevelDocumentMetaclass):
2✔
1071
    """A Dynamic Document class allowing flexible, expandable and uncontrolled
1072
    schemas.  As a :class:`~mongoengine.Document` subclass, acts in the same
1073
    way as an ordinary document but has expanded style properties.  Any data
1074
    passed or set against the :class:`~mongoengine.DynamicDocument` that is
1075
    not a field is automatically converted into a
1076
    :class:`~mongoengine.fields.DynamicField` and data can be attributed to that
1077
    field.
1078

1079
    .. note::
1080

1081
        There is one caveat on Dynamic Documents: undeclared fields cannot start with `_`
1082
    """
1083

1084
    # my_metaclass is defined so that metaclass can be queried in Python 2 & 3
1085
    my_metaclass = TopLevelDocumentMetaclass
2✔
1086

1087
    _dynamic = True
2✔
1088

1089
    def __delattr__(self, *args, **kwargs):
2✔
1090
        """Delete the attribute by setting to None and allowing _delta
1091
        to unset it.
1092
        """
1093
        field_name = args[0]
2✔
1094
        if field_name in self._dynamic_fields:
2✔
1095
            setattr(self, field_name, None)
2✔
1096
            self._dynamic_fields[field_name].null = False
2✔
1097
        else:
1098
            super().__delattr__(*args, **kwargs)
×
1099

1100

1101
class DynamicEmbeddedDocument(EmbeddedDocument, metaclass=DocumentMetaclass):
2✔
1102
    """A Dynamic Embedded Document class allowing flexible, expandable and
1103
    uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more
1104
    information about dynamic documents.
1105
    """
1106

1107
    # my_metaclass is defined so that metaclass can be queried in Python 2 & 3
1108
    my_metaclass = DocumentMetaclass
2✔
1109

1110
    _dynamic = True
2✔
1111

1112
    def __delattr__(self, *args, **kwargs):
2✔
1113
        """Delete the attribute by setting to None and allowing _delta
1114
        to unset it.
1115
        """
1116
        field_name = args[0]
2✔
1117
        if field_name in self._fields:
2✔
1118
            default = self._fields[field_name].default
2✔
1119
            if callable(default):
2✔
1120
                default = default()
2✔
1121
            setattr(self, field_name, default)
2✔
1122
        else:
1123
            setattr(self, field_name, None)
×
1124

1125

1126
class MapReduceDocument:
2✔
1127
    """A document returned from a map/reduce query.
1128

1129
    :param collection: An instance of :class:`~pymongo.Collection`
1130
    :param key: Document/result key, often an instance of
1131
                :class:`~bson.objectid.ObjectId`. If supplied as
1132
                an ``ObjectId`` found in the given ``collection``,
1133
                the object can be accessed via the ``object`` property.
1134
    :param value: The result(s) for this key.
1135
    """
1136

1137
    def __init__(self, document, collection, key, value):
2✔
1138
        self._document = document
2✔
1139
        self._collection = collection
2✔
1140
        self.key = key
2✔
1141
        self.value = value
2✔
1142

1143
    @property
2✔
1144
    def object(self):
2✔
1145
        """Lazy-load the object referenced by ``self.key``. ``self.key``
1146
        should be the ``primary_key``.
1147
        """
1148
        id_field = self._document()._meta["id_field"]
2✔
1149
        id_field_type = type(id_field)
2✔
1150

1151
        if not isinstance(self.key, id_field_type):
2✔
1152
            try:
2✔
1153
                self.key = id_field_type(self.key)
2✔
1154
            except Exception:
×
1155
                raise Exception("Could not cast key as %s" % id_field_type.__name__)
×
1156

1157
        if not hasattr(self, "_key_object"):
2✔
1158
            self._key_object = self._document.objects.with_id(self.key)
2✔
1159
            return self._key_object
2✔
1160
        return self._key_object
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc