• 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

96.47
/mongoengine/base/document.py
1
import copy
2✔
2
import numbers
2✔
3
import warnings
2✔
4
from functools import partial
2✔
5

6
import pymongo
2✔
7
from bson import SON, DBRef, ObjectId, json_util
2✔
8

9
from mongoengine import signals
2✔
10
from mongoengine.base.common import _DocumentRegistry
2✔
11
from mongoengine.base.datastructures import (
2✔
12
    BaseDict,
13
    BaseList,
14
    EmbeddedDocumentList,
15
    LazyReference,
16
    StrictDict,
17
)
18
from mongoengine.base.fields import ComplexBaseField
2✔
19
from mongoengine.common import _import_class
2✔
20
from mongoengine.errors import (
2✔
21
    FieldDoesNotExist,
22
    InvalidDocumentError,
23
    LookUpError,
24
    OperationError,
25
    ValidationError,
26
)
27
from mongoengine.pymongo_support import LEGACY_JSON_OPTIONS
2✔
28

29
__all__ = ("BaseDocument", "NON_FIELD_ERRORS")
2✔
30

31
NON_FIELD_ERRORS = "__all__"
2✔
32

33
try:
2✔
34
    GEOHAYSTACK = pymongo.GEOHAYSTACK
2✔
35
except AttributeError:
×
36
    GEOHAYSTACK = None
×
37

38

39
class BaseDocument:
2✔
40
    # TODO simplify how `_changed_fields` is used.
41
    # Currently, handling of `_changed_fields` seems unnecessarily convoluted:
42
    # 1. `BaseDocument` defines `_changed_fields` in its `__slots__`, yet it's
43
    #    not setting it to `[]` (or any other value) in `__init__`.
44
    # 2. `EmbeddedDocument` sets `_changed_fields` to `[]` it its overloaded
45
    #    `__init__`.
46
    # 3. `Document` does NOT set `_changed_fields` upon initialization. The
47
    #    field is primarily set via `_from_son` or `_clear_changed_fields`,
48
    #    though there are also other methods that manipulate it.
49
    # 4. The codebase is littered with `hasattr` calls for `_changed_fields`.
50
    __slots__ = (
2✔
51
        "_changed_fields",
52
        "_initialised",
53
        "_created",
54
        "_data",
55
        "_dynamic_fields",
56
        "_auto_id_field",
57
        "_db_field_map",
58
        "__weakref__",
59
    )
60

61
    _dynamic = False
2✔
62
    _dynamic_lock = True
2✔
63
    STRICT = False
2✔
64

65
    def __init__(self, *args, **values):
2✔
66
        """
67
        Initialise a document or an embedded document.
68

69
        :param values: A dictionary of keys and values for the document.
70
            It may contain additional reserved keywords, e.g. "__auto_convert".
71
        :param __auto_convert: If True, supplied values will be converted
72
            to Python-type values via each field's `to_python` method.
73
        :param _created: Indicates whether this is a brand new document
74
            or whether it's already been persisted before. Defaults to true.
75
        """
76
        self._initialised = False
2✔
77
        self._created = True
2✔
78

79
        if args:
2✔
80
            raise TypeError(
2✔
81
                "Instantiating a document with positional arguments is not "
82
                "supported. Please use `field_name=value` keyword arguments."
83
            )
84

85
        __auto_convert = values.pop("__auto_convert", True)
2✔
86

87
        _created = values.pop("_created", True)
2✔
88

89
        signals.pre_init.send(self.__class__, document=self, values=values)
2✔
90

91
        # Check if there are undefined fields supplied to the constructor,
92
        # if so raise an Exception.
93
        if not self._dynamic and (self._meta.get("strict", True) or _created):
2✔
94
            _undefined_fields = set(values.keys()) - set(
2✔
95
                list(self._fields.keys()) + ["id", "pk", "_cls", "_text_score"]
96
            )
97
            if _undefined_fields:
2✔
98
                msg = f'The fields "{_undefined_fields}" do not exist on the document "{self._class_name}"'
2✔
99
                raise FieldDoesNotExist(msg)
2✔
100

101
        if self.STRICT and not self._dynamic:
2✔
102
            self._data = StrictDict.create(allowed_keys=self._fields_ordered)()
×
103
        else:
104
            self._data = {}
2✔
105

106
        self._dynamic_fields = SON()
2✔
107

108
        # Assign default values for fields
109
        # not set in the constructor
110
        for field_name in self._fields:
2✔
111
            if field_name in values:
2✔
112
                continue
2✔
113
            value = getattr(self, field_name, None)
2✔
114
            setattr(self, field_name, value)
2✔
115

116
        if "_cls" not in values:
2✔
117
            self._cls = self._class_name
2✔
118

119
        # Set actual values
120
        dynamic_data = {}
2✔
121
        FileField = _import_class("FileField")
2✔
122
        for key, value in values.items():
2✔
123
            field = self._fields.get(key)
2✔
124
            if field or key in ("id", "pk", "_cls"):
2✔
125
                if __auto_convert and value is not None:
2✔
126
                    if field and not isinstance(field, FileField):
2✔
127
                        value = field.to_python(value)
2✔
128
                setattr(self, key, value)
2✔
129
            else:
130
                if self._dynamic:
2✔
131
                    dynamic_data[key] = value
2✔
132
                else:
133
                    # For strict Document
134
                    self._data[key] = value
2✔
135

136
        # Set any get_<field>_display methods
137
        self.__set_field_display()
2✔
138

139
        if self._dynamic:
2✔
140
            self._dynamic_lock = False
2✔
141
            for key, value in dynamic_data.items():
2✔
142
                setattr(self, key, value)
2✔
143

144
        # Flag initialised
145
        self._initialised = True
2✔
146
        self._created = _created
2✔
147

148
        signals.post_init.send(self.__class__, document=self)
2✔
149

150
    def __delattr__(self, *args, **kwargs):
2✔
151
        """Handle deletions of fields"""
152
        field_name = args[0]
2✔
153
        if field_name in self._fields:
2✔
154
            default = self._fields[field_name].default
2✔
155
            if callable(default):
2✔
156
                default = default()
2✔
157
            setattr(self, field_name, default)
2✔
158
        else:
159
            super().__delattr__(*args, **kwargs)
×
160

161
    def __setattr__(self, name, value):
2✔
162
        # Handle dynamic data only if an initialised dynamic document
163
        if self._dynamic and not self._dynamic_lock:
2✔
164
            if name not in self._fields_ordered and not name.startswith("_"):
2✔
165
                DynamicField = _import_class("DynamicField")
2✔
166
                field = DynamicField(db_field=name, null=True)
2✔
167
                field.name = name
2✔
168
                self._dynamic_fields[name] = field
2✔
169
                self._fields_ordered += (name,)
2✔
170

171
            if not name.startswith("_"):
2✔
172
                value = self.__expand_dynamic_values(name, value)
2✔
173

174
            # Handle marking data as changed
175
            if name in self._dynamic_fields:
2✔
176
                self._data[name] = value
2✔
177
                if hasattr(self, "_changed_fields"):
2✔
178
                    self._mark_as_changed(name)
2✔
179
        try:
2✔
180
            self__created = self._created
2✔
181
        except AttributeError:
2✔
182
            self__created = True
2✔
183

184
        if (
2✔
185
            self._is_document
186
            and not self__created
187
            and name in self._meta.get("shard_key", tuple())
188
            and self._data.get(name) != value
189
        ):
190
            msg = "Shard Keys are immutable. Tried to update %s" % name
2✔
191
            raise OperationError(msg)
2✔
192

193
        try:
2✔
194
            self__initialised = self._initialised
2✔
195
        except AttributeError:
2✔
196
            self__initialised = False
2✔
197

198
        # Check if the user has created a new instance of a class
199
        if (
2✔
200
            self._is_document
201
            and self__initialised
202
            and self__created
203
            and name == self._meta.get("id_field")
204
        ):
205
            # When setting the ID field of an instance already instantiated and that was user-created (i.e not saved in db yet)
206
            # Typically this is when calling .save()
207
            super().__setattr__("_created", False)
2✔
208

209
        super().__setattr__(name, value)
2✔
210

211
    def __getstate__(self):
2✔
212
        data = {}
2✔
213
        for k in (
2✔
214
            "_changed_fields",
215
            "_initialised",
216
            "_created",
217
            "_dynamic_fields",
218
            "_fields_ordered",
219
        ):
220
            if hasattr(self, k):
2✔
221
                data[k] = getattr(self, k)
2✔
222
        data["_data"] = self.to_mongo()
2✔
223
        return data
2✔
224

225
    def __setstate__(self, data):
2✔
226
        if isinstance(data["_data"], SON):
2✔
227
            data["_data"] = self.__class__._from_son(data["_data"])._data
2✔
228
        for k in (
2✔
229
            "_changed_fields",
230
            "_initialised",
231
            "_created",
232
            "_data",
233
            "_dynamic_fields",
234
        ):
235
            if k in data:
2✔
236
                setattr(self, k, data[k])
2✔
237
        if "_fields_ordered" in data:
2✔
238
            if self._dynamic:
2✔
239
                self._fields_ordered = data["_fields_ordered"]
2✔
240
            else:
241
                _super_fields_ordered = type(self)._fields_ordered
2✔
242
                self._fields_ordered = _super_fields_ordered
2✔
243

244
        dynamic_fields = data.get("_dynamic_fields") or SON()
2✔
245
        for k in dynamic_fields.keys():
2✔
246
            setattr(self, k, data["_data"].get(k))
2✔
247

248
    def __iter__(self):
2✔
249
        return iter(self._fields_ordered)
2✔
250

251
    def __getitem__(self, name):
2✔
252
        """Dictionary-style field access, return a field's value if present."""
253
        try:
2✔
254
            if name in self._fields_ordered:
2✔
255
                return getattr(self, name)
2✔
256
        except AttributeError:
×
257
            pass
×
258
        raise KeyError(name)
2✔
259

260
    def __setitem__(self, name, value):
2✔
261
        """Dictionary-style field access, set a field's value."""
262
        # Ensure that the field exists before settings its value
263
        if not self._dynamic and name not in self._fields:
2✔
264
            raise KeyError(name)
2✔
265
        return setattr(self, name, value)
2✔
266

267
    def __contains__(self, name):
2✔
268
        try:
2✔
269
            val = getattr(self, name)
2✔
270
            return val is not None
2✔
271
        except AttributeError:
2✔
272
            return False
2✔
273

274
    def __len__(self):
2✔
275
        return len(self._data)
2✔
276

277
    def __repr__(self):
2✔
278
        try:
2✔
279
            u = self.__str__()
2✔
280
        except (UnicodeEncodeError, UnicodeDecodeError):
×
281
            u = "[Bad Unicode data]"
×
282
        repr_type = str if u is None else type(u)
2✔
283
        return repr_type(f"<{self.__class__.__name__}: {u}>")
2✔
284

285
    def __str__(self):
2✔
286
        # TODO this could be simpler?
287
        if hasattr(self, "__unicode__"):
2✔
288
            return self.__unicode__()
2✔
289
        return "%s object" % self.__class__.__name__
2✔
290

291
    def __eq__(self, other):
2✔
292
        if (
2✔
293
            isinstance(other, self.__class__)
294
            and hasattr(other, "id")
295
            and other.id is not None
296
        ):
297
            return self.id == other.id
2✔
298
        if isinstance(other, DBRef):
2✔
299
            return (
2✔
300
                self._get_collection_name() == other.collection and self.id == other.id
301
            )
302
        if self.id is None:
2✔
303
            return self is other
2✔
304
        return False
2✔
305

306
    def __ne__(self, other):
2✔
307
        return not self.__eq__(other)
2✔
308

309
    def clean(self):
2✔
310
        """
311
        Hook for doing document level data cleaning (usually validation or assignment)
312
        before validation is run.
313

314
        Any ValidationError raised by this method will not be associated with
315
        a particular field; it will have a special-case association with the
316
        field defined by NON_FIELD_ERRORS.
317
        """
318
        pass
2✔
319

320
    def get_text_score(self):
2✔
321
        """
322
        Get text score from text query
323
        """
324

325
        if "_text_score" not in self._data:
2✔
326
            raise InvalidDocumentError(
×
327
                "This document is not originally built from a text query (or text_score was not set on search_text() call)"
328
            )
329

330
        return self._data["_text_score"]
2✔
331

332
    def to_mongo(self, use_db_field=True, fields=None):
2✔
333
        """
334
        Return as SON data ready for use with MongoDB.
335
        """
336
        fields = fields or []
2✔
337

338
        data = SON()
2✔
339
        data["_id"] = None
2✔
340
        data["_cls"] = self._class_name
2✔
341

342
        # only root fields ['test1.a', 'test2'] => ['test1', 'test2']
343
        root_fields = {f.split(".")[0] for f in fields}
2✔
344

345
        for field_name in self:
2✔
346
            if root_fields and field_name not in root_fields:
2✔
347
                continue
2✔
348

349
            value = self._data.get(field_name, None)
2✔
350
            field = self._fields.get(field_name)
2✔
351

352
            if field is None and self._dynamic:
2✔
353
                field = self._dynamic_fields.get(field_name)
2✔
354

355
            if value is not None:
2✔
356
                f_inputs = field.to_mongo.__code__.co_varnames
2✔
357
                ex_vars = {}
2✔
358
                if fields and "fields" in f_inputs:
2✔
359
                    key = "%s." % field_name
2✔
360
                    embedded_fields = [
2✔
361
                        i.replace(key, "") for i in fields if i.startswith(key)
362
                    ]
363

364
                    ex_vars["fields"] = embedded_fields
2✔
365

366
                if "use_db_field" in f_inputs:
2✔
367
                    ex_vars["use_db_field"] = use_db_field
2✔
368

369
                value = field.to_mongo(value, **ex_vars)
2✔
370

371
            # Handle self generating fields
372
            if value is None and field._auto_gen:
2✔
373
                value = field.generate()
2✔
374
                self._data[field_name] = value
2✔
375

376
            if value is not None or field.null:
2✔
377
                if use_db_field:
2✔
378
                    data[field.db_field] = value
2✔
379
                else:
380
                    data[field.name] = value
2✔
381

382
        # Only add _cls if allow_inheritance is True
383
        if not self._meta.get("allow_inheritance"):
2✔
384
            data.pop("_cls")
2✔
385

386
        return data
2✔
387

388
    def validate(self, clean=True):
2✔
389
        """Ensure that all fields' values are valid and that required fields
390
        are present.
391

392
        Raises :class:`ValidationError` if any of the fields' values are found
393
        to be invalid.
394
        """
395
        # Ensure that each field is matched to a valid value
396
        errors = {}
2✔
397
        if clean:
2✔
398
            try:
2✔
399
                self.clean()
2✔
400
            except ValidationError as error:
2✔
401
                errors[NON_FIELD_ERRORS] = error
2✔
402

403
        # Get a list of tuples of field names and their current values
404
        fields = [
2✔
405
            (
406
                self._fields.get(name, self._dynamic_fields.get(name)),
407
                self._data.get(name),
408
            )
409
            for name in self._fields_ordered
410
        ]
411

412
        EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
2✔
413
        GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField")
2✔
414

415
        for field, value in fields:
2✔
416
            if value is not None:
2✔
417
                try:
2✔
418
                    if isinstance(
2✔
419
                        field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)
420
                    ):
421
                        field._validate(value, clean=clean)
2✔
422
                    else:
423
                        field._validate(value)
2✔
424
                except ValidationError as error:
2✔
425
                    errors[field.name] = error.errors or error
2✔
426
                except (ValueError, AttributeError, AssertionError) as error:
2✔
427
                    errors[field.name] = error
2✔
428
            elif field.required and not getattr(field, "_auto_gen", False):
2✔
429
                errors[field.name] = ValidationError(
2✔
430
                    "Field is required", field_name=field.name
431
                )
432

433
        if errors:
2✔
434
            pk = "None"
2✔
435
            if hasattr(self, "pk"):
2✔
436
                pk = self.pk
2✔
437
            elif self._instance and hasattr(self._instance, "pk"):
2✔
438
                pk = self._instance.pk
2✔
439
            message = f"ValidationError ({self._class_name}:{pk}) "
2✔
440
            raise ValidationError(message, errors=errors)
2✔
441

442
    def to_json(self, *args, **kwargs):
2✔
443
        """Convert this document to JSON.
444

445
        :param use_db_field: Serialize field names as they appear in
446
            MongoDB (as opposed to attribute names on this document).
447
            Defaults to True.
448
        """
449
        use_db_field = kwargs.pop("use_db_field", True)
2✔
450
        if "json_options" not in kwargs:
2✔
451
            warnings.warn(
2✔
452
                "No 'json_options' are specified! Falling back to "
453
                "LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. "
454
                "For use with other MongoDB drivers specify the UUID "
455
                "representation to use. This will be changed to "
456
                "uuid_representation=UNSPECIFIED in a future release.",
457
                DeprecationWarning,
458
                stacklevel=2,
459
            )
460
            kwargs["json_options"] = LEGACY_JSON_OPTIONS
2✔
461
        return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
2✔
462

463
    @classmethod
2✔
464
    def from_json(cls, json_data, created=False, **kwargs):
2✔
465
        """Converts json data to a Document instance.
466

467
        :param str json_data: The json data to load into the Document.
468
        :param bool created: Boolean defining whether to consider the newly
469
            instantiated document as brand new or as persisted already:
470
            * If True, consider the document as brand new, no matter what data
471
              it's loaded with (i.e., even if an ID is loaded).
472
            * If False and an ID is NOT provided, consider the document as
473
              brand new.
474
            * If False and an ID is provided, assume that the object has
475
              already been persisted (this has an impact on the subsequent
476
              call to .save()).
477
            * Defaults to ``False``.
478
        """
479
        # TODO should `created` default to False? If the object already exists
480
        # in the DB, you would likely retrieve it from MongoDB itself through
481
        # a query, not load it from JSON data.
482
        if "json_options" not in kwargs:
2✔
483
            warnings.warn(
2✔
484
                "No 'json_options' are specified! Falling back to "
485
                "LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. "
486
                "For use with other MongoDB drivers specify the UUID "
487
                "representation to use. This will be changed to "
488
                "uuid_representation=UNSPECIFIED in a future release.",
489
                DeprecationWarning,
490
                stacklevel=2,
491
            )
492
            kwargs["json_options"] = LEGACY_JSON_OPTIONS
2✔
493
        return cls._from_son(json_util.loads(json_data, **kwargs), created=created)
2✔
494

495
    def __expand_dynamic_values(self, name, value):
2✔
496
        """Expand any dynamic values to their correct types / values."""
497
        if not isinstance(value, (dict, list, tuple)):
2✔
498
            return value
2✔
499

500
        # If the value is a dict with '_cls' in it, turn it into a document
501
        is_dict = isinstance(value, dict)
2✔
502
        if is_dict and "_cls" in value:
2✔
503
            cls = _DocumentRegistry.get(value["_cls"])
2✔
504
            return cls(**value)
2✔
505

506
        if is_dict:
2✔
507
            value = {k: self.__expand_dynamic_values(k, v) for k, v in value.items()}
2✔
508
        else:
509
            value = [self.__expand_dynamic_values(name, v) for v in value]
2✔
510

511
        # Convert lists / values so we can watch for any changes on them
512
        EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField")
2✔
513
        if isinstance(value, (list, tuple)) and not isinstance(value, BaseList):
2✔
514
            if issubclass(type(self), EmbeddedDocumentListField):
2✔
515
                value = EmbeddedDocumentList(value, self, name)
×
516
            else:
517
                value = BaseList(value, self, name)
2✔
518
        elif isinstance(value, dict) and not isinstance(value, BaseDict):
2✔
519
            value = BaseDict(value, self, name)
2✔
520

521
        return value
2✔
522

523
    def _mark_as_changed(self, key):
2✔
524
        """Mark a key as explicitly changed by the user."""
525
        if not hasattr(self, "_changed_fields"):
2✔
526
            return
2✔
527

528
        if "." in key:
2✔
529
            key, rest = key.split(".", 1)
2✔
530
            key = self._db_field_map.get(key, key)
2✔
531
            key = f"{key}.{rest}"
2✔
532
        else:
533
            key = self._db_field_map.get(key, key)
2✔
534

535
        if key not in self._changed_fields:
2✔
536
            levels, idx = key.split("."), 1
2✔
537
            while idx <= len(levels):
2✔
538
                if ".".join(levels[:idx]) in self._changed_fields:
2✔
539
                    break
×
540
                idx += 1
2✔
541
            else:
542
                self._changed_fields.append(key)
2✔
543
                # remove lower level changed fields
544
                level = ".".join(levels[:idx]) + "."
2✔
545
                remove = self._changed_fields.remove
2✔
546
                for field in self._changed_fields[:]:
2✔
547
                    if field.startswith(level):
2✔
548
                        remove(field)
2✔
549

550
    def _clear_changed_fields(self):
2✔
551
        """Using _get_changed_fields iterate and remove any fields that
552
        are marked as changed.
553
        """
554
        ReferenceField = _import_class("ReferenceField")
2✔
555
        GenericReferenceField = _import_class("GenericReferenceField")
2✔
556

557
        for changed in self._get_changed_fields():
2✔
558
            parts = changed.split(".")
2✔
559
            data = self
2✔
560
            for part in parts:
2✔
561
                if isinstance(data, list):
2✔
562
                    try:
2✔
563
                        data = data[int(part)]
2✔
564
                    except IndexError:
×
565
                        data = None
×
566
                elif isinstance(data, dict):
2✔
567
                    data = data.get(part, None)
2✔
568
                else:
569
                    field_name = data._reverse_db_field_map.get(part, part)
2✔
570
                    data = getattr(data, field_name, None)
2✔
571

572
                if not isinstance(data, LazyReference) and hasattr(
2✔
573
                    data, "_changed_fields"
574
                ):
575
                    if getattr(data, "_is_document", False):
2✔
576
                        continue
2✔
577

578
                    data._changed_fields = []
2✔
579
                elif isinstance(data, (list, tuple, dict)):
2✔
580
                    if hasattr(data, "field") and isinstance(
2✔
581
                        data.field, (ReferenceField, GenericReferenceField)
582
                    ):
583
                        continue
×
584
                    BaseDocument._nestable_types_clear_changed_fields(data)
2✔
585

586
        self._changed_fields = []
2✔
587

588
    @staticmethod
2✔
589
    def _nestable_types_clear_changed_fields(data):
2✔
590
        """Inspect nested data for changed fields
591

592
        :param data: data to inspect for changes
593
        """
594
        Document = _import_class("Document")
2✔
595

596
        # Loop list / dict fields as they contain documents
597
        # Determine the iterator to use
598
        if not hasattr(data, "items"):
2✔
599
            iterator = enumerate(data)
2✔
600
        else:
601
            iterator = data.items()
2✔
602

603
        for _index_or_key, value in iterator:
2✔
604
            if hasattr(value, "_get_changed_fields") and not isinstance(
2✔
605
                value, Document
606
            ):  # don't follow references
607
                value._clear_changed_fields()
2✔
608
            elif isinstance(value, (list, tuple, dict)):
2✔
609
                BaseDocument._nestable_types_clear_changed_fields(value)
2✔
610

611
    @staticmethod
2✔
612
    def _nestable_types_changed_fields(changed_fields, base_key, data):
2✔
613
        """Inspect nested data for changed fields
614

615
        :param changed_fields: Previously collected changed fields
616
        :param base_key: The base key that must be used to prepend changes to this data
617
        :param data: data to inspect for changes
618
        """
619
        # Loop list / dict fields as they contain documents
620
        # Determine the iterator to use
621
        if not hasattr(data, "items"):
2✔
622
            iterator = enumerate(data)
2✔
623
        else:
624
            iterator = data.items()
2✔
625

626
        for index_or_key, value in iterator:
2✔
627
            item_key = f"{base_key}{index_or_key}."
2✔
628
            # don't check anything lower if this key is already marked
629
            # as changed.
630
            if item_key[:-1] in changed_fields:
2✔
631
                continue
2✔
632

633
            if hasattr(value, "_get_changed_fields"):
2✔
634
                changed = value._get_changed_fields()
2✔
635
                changed_fields += [f"{item_key}{k}" for k in changed if k]
2✔
636
            elif isinstance(value, (list, tuple, dict)):
2✔
637
                BaseDocument._nestable_types_changed_fields(
2✔
638
                    changed_fields, item_key, value
639
                )
640

641
    def _get_changed_fields(self):
2✔
642
        """Return a list of all fields that have explicitly been changed."""
643
        EmbeddedDocument = _import_class("EmbeddedDocument")
2✔
644
        LazyReferenceField = _import_class("LazyReferenceField")
2✔
645
        ReferenceField = _import_class("ReferenceField")
2✔
646
        GenericLazyReferenceField = _import_class("GenericLazyReferenceField")
2✔
647
        GenericReferenceField = _import_class("GenericReferenceField")
2✔
648
        SortedListField = _import_class("SortedListField")
2✔
649

650
        changed_fields = []
2✔
651
        changed_fields += getattr(self, "_changed_fields", [])
2✔
652

653
        for field_name in self._fields_ordered:
2✔
654
            db_field_name = self._db_field_map.get(field_name, field_name)
2✔
655
            key = "%s." % db_field_name
2✔
656
            data = self._data.get(field_name, None)
2✔
657
            field = self._fields.get(field_name)
2✔
658

659
            if db_field_name in changed_fields:
2✔
660
                # Whole field already marked as changed, no need to go further
661
                continue
2✔
662

663
            if isinstance(field, ReferenceField):  # Don't follow referenced documents
2✔
664
                continue
2✔
665

666
            if isinstance(data, EmbeddedDocument):
2✔
667
                # Find all embedded fields that have been changed
668
                changed = data._get_changed_fields()
2✔
669
                changed_fields += [f"{key}{k}" for k in changed if k]
2✔
670
            elif isinstance(data, (list, tuple, dict)):
2✔
671
                if hasattr(field, "field") and isinstance(
2✔
672
                    field.field,
673
                    (
674
                        LazyReferenceField,
675
                        ReferenceField,
676
                        GenericLazyReferenceField,
677
                        GenericReferenceField,
678
                    ),
679
                ):
680
                    continue
2✔
681
                elif isinstance(field, SortedListField) and field._ordering:
2✔
682
                    # if ordering is affected whole list is changed
683
                    if any(field._ordering in d._changed_fields for d in data):
2✔
684
                        changed_fields.append(db_field_name)
2✔
685
                        continue
2✔
686

687
                self._nestable_types_changed_fields(changed_fields, key, data)
2✔
688
        return changed_fields
2✔
689

690
    def _delta(self):
2✔
691
        """Returns the delta (set, unset) of the changes for a document.
692
        Gets any values that have been explicitly changed.
693
        """
694
        # Handles cases where not loaded from_son but has _id
695
        doc = self.to_mongo()
2✔
696

697
        set_fields = self._get_changed_fields()
2✔
698
        unset_data = {}
2✔
699
        if hasattr(self, "_changed_fields"):
2✔
700
            set_data = {}
2✔
701
            # Fetch each set item from its path
702
            for path in set_fields:
2✔
703
                parts = path.split(".")
2✔
704
                d = doc
2✔
705
                new_path = []
2✔
706
                for p in parts:
2✔
707
                    if isinstance(d, (ObjectId, DBRef)):
2✔
708
                        # Don't dig in the references
709
                        break
×
710
                    elif isinstance(d, list) and p.isdigit():
2✔
711
                        # An item of a list (identified by its index) is updated
712
                        d = d[int(p)]
2✔
713
                    elif hasattr(d, "get"):
2✔
714
                        # dict-like (dict, embedded document)
715
                        d = d.get(p)
2✔
716
                    new_path.append(p)
2✔
717
                path = ".".join(new_path)
2✔
718
                set_data[path] = d
2✔
719
        else:
720
            set_data = doc
2✔
721
            if "_id" in set_data:
2✔
722
                del set_data["_id"]
2✔
723

724
        # Determine if any changed items were actually unset.
725
        for path, value in list(set_data.items()):
2✔
726
            if value or isinstance(
2✔
727
                value, (numbers.Number, bool)
728
            ):  # Account for 0 and True that are truthy
729
                continue
2✔
730

731
            parts = path.split(".")
2✔
732

733
            if self._dynamic and len(parts) and parts[0] in self._dynamic_fields:
2✔
734
                del set_data[path]
2✔
735
                unset_data[path] = 1
2✔
736
                continue
2✔
737

738
            # If we've set a value that ain't the default value don't unset it.
739
            default = None
2✔
740
            if path in self._fields:
2✔
741
                default = self._fields[path].default
2✔
742
            else:  # Perform a full lookup for lists / embedded lookups
743
                d = self
2✔
744
                db_field_name = parts.pop()
2✔
745
                for p in parts:
2✔
746
                    if isinstance(d, list) and p.isdigit():
2✔
747
                        d = d[int(p)]
2✔
748
                    elif hasattr(d, "__getattribute__") and not isinstance(d, dict):
2✔
749
                        real_path = d._reverse_db_field_map.get(p, p)
2✔
750
                        d = getattr(d, real_path)
2✔
751
                    else:
752
                        d = d.get(p)
×
753

754
                if hasattr(d, "_fields"):
2✔
755
                    field_name = d._reverse_db_field_map.get(
2✔
756
                        db_field_name, db_field_name
757
                    )
758
                    if field_name in d._fields:
2✔
759
                        default = d._fields.get(field_name).default
2✔
760
                    else:
761
                        default = None
×
762

763
            if default is not None:
2✔
764
                default = default() if callable(default) else default
2✔
765

766
            if value != default:
2✔
767
                continue
2✔
768

769
            del set_data[path]
2✔
770
            unset_data[path] = 1
2✔
771
        return set_data, unset_data
2✔
772

773
    @classmethod
2✔
774
    def _get_collection_name(cls):
2✔
775
        """Return the collection name for this class. None for abstract
776
        class.
777
        """
778
        return cls._meta.get("collection", None)
2✔
779

780
    @classmethod
2✔
781
    def _from_son(cls, son, _auto_dereference=True, created=False):
2✔
782
        """Create an instance of a Document (subclass) from a PyMongo SON (dict)"""
783
        if son and not isinstance(son, dict):
2✔
784
            raise ValueError(
2✔
785
                "The source SON object needs to be of type 'dict' but a '%s' was found"
786
                % type(son)
787
            )
788

789
        # Get the class name from the document, falling back to the given
790
        # class if unavailable
791
        class_name = son.get("_cls", cls._class_name)
2✔
792

793
        # Convert SON to a data dict, making sure each key is a string and
794
        # corresponds to the right db field.
795
        # This is needed as _from_son is currently called both from BaseDocument.__init__
796
        # and from EmbeddedDocumentField.to_python
797
        data = {}
2✔
798
        for key, value in son.items():
2✔
799
            key = str(key)
2✔
800
            key = cls._db_field_map.get(key, key)
2✔
801
            data[key] = value
2✔
802

803
        # Return correct subclass for document type
804
        if class_name != cls._class_name:
2✔
805
            cls = _DocumentRegistry.get(class_name)
2✔
806

807
        errors_dict = {}
2✔
808

809
        fields = cls._fields
2✔
810
        if not _auto_dereference:
2✔
811
            # if auto_deref is turned off, we copy the fields so
812
            # we can mutate the auto_dereference of the fields
813
            fields = copy.deepcopy(fields)
2✔
814

815
        # Apply field-name / db-field conversion
816
        for field_name, field in fields.items():
2✔
817
            field.set_auto_dereferencing(
2✔
818
                _auto_dereference
819
            )  # align the field's auto-dereferencing with the document's
820
            if field.db_field in data:
2✔
821
                value = data[field.db_field]
2✔
822
                try:
2✔
823
                    data[field_name] = (
2✔
824
                        value if value is None else field.to_python(value)
825
                    )
826
                    if field_name != field.db_field:
2✔
827
                        del data[field.db_field]
2✔
828
                except (AttributeError, ValueError) as e:
2✔
829
                    errors_dict[field_name] = e
2✔
830

831
        if errors_dict:
2✔
832
            errors = "\n".join([f"Field '{k}' - {v}" for k, v in errors_dict.items()])
2✔
833
            msg = "Invalid data to create a `{}` instance.\n{}".format(
2✔
834
                cls._class_name,
835
                errors,
836
            )
837
            raise InvalidDocumentError(msg)
2✔
838

839
        # In STRICT documents, remove any keys that aren't in cls._fields
840
        if cls.STRICT:
2✔
841
            data = {k: v for k, v in data.items() if k in cls._fields}
×
842

843
        obj = cls(__auto_convert=False, _created=created, **data)
2✔
844
        obj._changed_fields = []
2✔
845
        if not _auto_dereference:
2✔
846
            obj._fields = fields
2✔
847

848
        return obj
2✔
849

850
    @classmethod
2✔
851
    def _build_index_specs(cls, meta_indexes):
2✔
852
        """Generate and merge the full index specs."""
853
        geo_indices = cls._geo_indices()
2✔
854
        unique_indices = cls._unique_with_indexes()
2✔
855
        index_specs = [cls._build_index_spec(spec) for spec in meta_indexes]
2✔
856

857
        def merge_index_specs(index_specs, indices):
2✔
858
            """Helper method for merging index specs."""
859
            if not indices:
2✔
860
                return index_specs
2✔
861

862
            # Create a map of index fields to index spec. We're converting
863
            # the fields from a list to a tuple so that it's hashable.
864
            spec_fields = {tuple(index["fields"]): index for index in index_specs}
2✔
865

866
            # For each new index, if there's an existing index with the same
867
            # fields list, update the existing spec with all data from the
868
            # new spec.
869
            for new_index in indices:
2✔
870
                candidate = spec_fields.get(tuple(new_index["fields"]))
2✔
871
                if candidate is None:
2✔
872
                    index_specs.append(new_index)
2✔
873
                else:
874
                    candidate.update(new_index)
2✔
875

876
            return index_specs
2✔
877

878
        # Merge geo indexes and unique_with indexes into the meta index specs.
879
        index_specs = merge_index_specs(index_specs, geo_indices)
2✔
880
        index_specs = merge_index_specs(index_specs, unique_indices)
2✔
881
        return index_specs
2✔
882

883
    @classmethod
2✔
884
    def _build_index_spec(cls, spec):
2✔
885
        """Build a PyMongo index spec from a MongoEngine index spec."""
886
        if isinstance(spec, str):
2✔
887
            spec = {"fields": [spec]}
2✔
888
        elif isinstance(spec, (list, tuple)):
2✔
889
            spec = {"fields": list(spec)}
2✔
890
        elif isinstance(spec, dict):
2✔
891
            spec = dict(spec)
2✔
892

893
        index_list = []
2✔
894
        direction = None
2✔
895

896
        # Check to see if we need to include _cls
897
        allow_inheritance = cls._meta.get("allow_inheritance")
2✔
898
        include_cls = (
2✔
899
            allow_inheritance
900
            and not spec.get("sparse", False)
901
            and spec.get("cls", True)
902
            and "_cls" not in spec["fields"]
903
        )
904

905
        # 733: don't include cls if index_cls is False unless there is an explicit cls with the index
906
        include_cls = include_cls and (
2✔
907
            spec.get("cls", False) or cls._meta.get("index_cls", True)
908
        )
909
        if "cls" in spec:
2✔
910
            spec.pop("cls")
2✔
911
        for key in spec["fields"]:
2✔
912
            # If inherited spec continue
913
            if isinstance(key, (list, tuple)):
2✔
914
                continue
2✔
915

916
            # ASCENDING from +
917
            # DESCENDING from -
918
            # TEXT from $
919
            # HASHED from #
920
            # GEOSPHERE from (
921
            # GEOHAYSTACK from )
922
            # GEO2D from *
923
            direction = pymongo.ASCENDING
2✔
924
            if key.startswith("-"):
2✔
925
                direction = pymongo.DESCENDING
2✔
926
            elif key.startswith("$"):
2✔
927
                direction = pymongo.TEXT
2✔
928
            elif key.startswith("#"):
2✔
929
                direction = pymongo.HASHED
2✔
930
            elif key.startswith("("):
2✔
931
                direction = pymongo.GEOSPHERE
2✔
932
            elif key.startswith(")"):
2✔
933
                try:
2✔
934
                    direction = pymongo.GEOHAYSTACK
2✔
935
                except AttributeError:
×
936
                    raise NotImplementedError
×
937
            elif key.startswith("*"):
2✔
938
                direction = pymongo.GEO2D
2✔
939
            if key.startswith(("+", "-", "*", "$", "#", "(", ")")):
2✔
940
                key = key[1:]
2✔
941

942
            # Use real field name, do it manually because we need field
943
            # objects for the next part (list field checking)
944
            parts = key.split(".")
2✔
945
            if parts in (["pk"], ["id"], ["_id"]):
2✔
946
                key = "_id"
2✔
947
            else:
948
                fields = cls._lookup_field(parts)
2✔
949
                parts = []
2✔
950
                for field in fields:
2✔
951
                    try:
2✔
952
                        if field != "_id":
2✔
953
                            field = field.db_field
2✔
954
                    except AttributeError:
×
955
                        pass
×
956
                    parts.append(field)
2✔
957
                key = ".".join(parts)
2✔
958
            index_list.append((key, direction))
2✔
959

960
        # Don't add cls to a geo index
961
        if (
2✔
962
            include_cls
963
            and direction not in (pymongo.GEO2D, pymongo.GEOSPHERE)
964
            and (GEOHAYSTACK is None or direction != GEOHAYSTACK)
965
        ):
966
            index_list.insert(0, ("_cls", 1))
2✔
967

968
        if index_list:
2✔
969
            spec["fields"] = index_list
2✔
970

971
        return spec
2✔
972

973
    @classmethod
2✔
974
    def _unique_with_indexes(cls, namespace=""):
2✔
975
        """Find unique indexes in the document schema and return them."""
976
        unique_indexes = []
2✔
977
        for field_name, field in cls._fields.items():
2✔
978
            sparse = field.sparse
2✔
979

980
            # Generate a list of indexes needed by uniqueness constraints
981
            if field.unique:
2✔
982
                unique_fields = [field.db_field]
2✔
983

984
                # Add any unique_with fields to the back of the index spec
985
                if field.unique_with:
2✔
986
                    if isinstance(field.unique_with, str):
2✔
987
                        field.unique_with = [field.unique_with]
2✔
988

989
                    # Convert unique_with field names to real field names
990
                    unique_with = []
2✔
991
                    for other_name in field.unique_with:
2✔
992
                        parts = other_name.split(".")
2✔
993

994
                        # Lookup real name
995
                        parts = cls._lookup_field(parts)
2✔
996
                        name_parts = [part.db_field for part in parts]
2✔
997
                        unique_with.append(".".join(name_parts))
2✔
998

999
                        # Unique field should be required
1000
                        parts[-1].required = True
2✔
1001
                        sparse = not sparse and parts[-1].name not in cls.__dict__
2✔
1002

1003
                    unique_fields += unique_with
2✔
1004

1005
                # Add the new index to the list
1006
                fields = [(f"{namespace}{f}", pymongo.ASCENDING) for f in unique_fields]
2✔
1007
                index = {"fields": fields, "unique": True, "sparse": sparse}
2✔
1008
                unique_indexes.append(index)
2✔
1009

1010
            if field.__class__.__name__ in {
2✔
1011
                "EmbeddedDocumentListField",
1012
                "ListField",
1013
                "SortedListField",
1014
            }:
1015
                field = field.field
2✔
1016

1017
            # Grab any embedded document field unique indexes
1018
            if (
2✔
1019
                field.__class__.__name__ == "EmbeddedDocumentField"
1020
                and field.document_type != cls
1021
            ):
1022
                field_namespace = "%s." % field_name
2✔
1023
                doc_cls = field.document_type
2✔
1024
                unique_indexes += doc_cls._unique_with_indexes(field_namespace)
2✔
1025

1026
        return unique_indexes
2✔
1027

1028
    @classmethod
2✔
1029
    def _geo_indices(cls, inspected=None, parent_field=None):
2✔
1030
        inspected = inspected or []
2✔
1031
        geo_indices = []
2✔
1032
        inspected.append(cls)
2✔
1033

1034
        geo_field_type_names = (
2✔
1035
            "EmbeddedDocumentField",
1036
            "GeoPointField",
1037
            "PointField",
1038
            "LineStringField",
1039
            "PolygonField",
1040
        )
1041

1042
        geo_field_types = tuple(_import_class(field) for field in geo_field_type_names)
2✔
1043

1044
        for field in cls._fields.values():
2✔
1045
            if not isinstance(field, geo_field_types):
2✔
1046
                continue
2✔
1047

1048
            if hasattr(field, "document_type"):
2✔
1049
                field_cls = field.document_type
2✔
1050
                if field_cls in inspected:
2✔
1051
                    continue
2✔
1052

1053
                if hasattr(field_cls, "_geo_indices"):
2✔
1054
                    geo_indices += field_cls._geo_indices(
2✔
1055
                        inspected, parent_field=field.db_field
1056
                    )
1057
            elif field._geo_index:
2✔
1058
                field_name = field.db_field
2✔
1059
                if parent_field:
2✔
1060
                    field_name = f"{parent_field}.{field_name}"
2✔
1061
                geo_indices.append({"fields": [(field_name, field._geo_index)]})
2✔
1062

1063
        return geo_indices
2✔
1064

1065
    @classmethod
2✔
1066
    def _lookup_field(cls, parts):
2✔
1067
        """Given the path to a given field, return a list containing
1068
        the Field object associated with that field and all of its parent
1069
        Field objects.
1070

1071
        Args:
1072
            parts (str, list, or tuple) - path to the field. Should be a
1073
            string for simple fields existing on this document or a list
1074
            of strings for a field that exists deeper in embedded documents.
1075

1076
        Returns:
1077
            A list of Field instances for fields that were found or
1078
            strings for sub-fields that weren't.
1079

1080
        Example:
1081
            >>> user._lookup_field('name')
1082
            [<mongoengine.fields.StringField at 0x1119bff50>]
1083

1084
            >>> user._lookup_field('roles')
1085
            [<mongoengine.fields.EmbeddedDocumentListField at 0x1119ec250>]
1086

1087
            >>> user._lookup_field(['roles', 'role'])
1088
            [<mongoengine.fields.EmbeddedDocumentListField at 0x1119ec250>,
1089
             <mongoengine.fields.StringField at 0x1119ec050>]
1090

1091
            >>> user._lookup_field('doesnt_exist')
1092
            raises LookUpError
1093

1094
            >>> user._lookup_field(['roles', 'doesnt_exist'])
1095
            [<mongoengine.fields.EmbeddedDocumentListField at 0x1119ec250>,
1096
             'doesnt_exist']
1097

1098
        """
1099
        # TODO this method is WAY too complicated. Simplify it.
1100
        # TODO don't think returning a string for embedded non-existent fields is desired
1101

1102
        ListField = _import_class("ListField")
2✔
1103
        DynamicField = _import_class("DynamicField")
2✔
1104

1105
        if not isinstance(parts, (list, tuple)):
2✔
1106
            parts = [parts]
×
1107

1108
        fields = []
2✔
1109
        field = None
2✔
1110

1111
        for field_name in parts:
2✔
1112
            # Handle ListField indexing:
1113
            if field_name.isdigit() and isinstance(field, ListField):
2✔
1114
                fields.append(field_name)
2✔
1115
                continue
2✔
1116

1117
            # Look up first field from the document
1118
            if field is None:
2✔
1119
                if field_name == "pk":
2✔
1120
                    # Deal with "primary key" alias
1121
                    field_name = cls._meta["id_field"]
2✔
1122

1123
                if field_name in cls._fields:
2✔
1124
                    field = cls._fields[field_name]
2✔
1125
                elif cls._dynamic:
2✔
1126
                    field = DynamicField(db_field=field_name)
2✔
1127
                elif cls._meta.get("allow_inheritance") or cls._meta.get(
2✔
1128
                    "abstract", False
1129
                ):
1130
                    # 744: in case the field is defined in a subclass
1131
                    for subcls in cls.__subclasses__():
2✔
1132
                        try:
2✔
1133
                            field = subcls._lookup_field([field_name])[0]
2✔
1134
                        except LookUpError:
2✔
1135
                            continue
2✔
1136

1137
                        if field is not None:
2✔
1138
                            break
2✔
1139
                    else:
1140
                        raise LookUpError('Cannot resolve field "%s"' % field_name)
2✔
1141
                else:
1142
                    raise LookUpError('Cannot resolve field "%s"' % field_name)
2✔
1143
            else:
1144
                ReferenceField = _import_class("ReferenceField")
2✔
1145
                GenericReferenceField = _import_class("GenericReferenceField")
2✔
1146

1147
                # If previous field was a reference, throw an error (we
1148
                # cannot look up fields that are on references).
1149
                if isinstance(field, (ReferenceField, GenericReferenceField)):
2✔
1150
                    raise LookUpError(
2✔
1151
                        "Cannot perform join in mongoDB: %s" % "__".join(parts)
1152
                    )
1153

1154
                # If the parent field has a "field" attribute which has a
1155
                # lookup_member method, call it to find the field
1156
                # corresponding to this iteration.
1157
                if hasattr(getattr(field, "field", None), "lookup_member"):
2✔
1158
                    new_field = field.field.lookup_member(field_name)
2✔
1159

1160
                # If the parent field is a DynamicField or if it's part of
1161
                # a DynamicDocument, mark current field as a DynamicField
1162
                # with db_name equal to the field name.
1163
                elif cls._dynamic and (
2✔
1164
                    isinstance(field, DynamicField)
1165
                    or getattr(getattr(field, "document_type", None), "_dynamic", None)
1166
                ):
1167
                    new_field = DynamicField(db_field=field_name)
2✔
1168

1169
                # Else, try to use the parent field's lookup_member method
1170
                # to find the subfield.
1171
                elif hasattr(field, "lookup_member"):
2✔
1172
                    new_field = field.lookup_member(field_name)
2✔
1173

1174
                # Raise a LookUpError if all the other conditions failed.
1175
                else:
1176
                    raise LookUpError(
2✔
1177
                        "Cannot resolve subfield or operator {} "
1178
                        "on the field {}".format(field_name, field.name)
1179
                    )
1180

1181
                # If current field still wasn't found and the parent field
1182
                # is a ComplexBaseField, add the name current field name and
1183
                # move on.
1184
                if not new_field and isinstance(field, ComplexBaseField):
2✔
1185
                    fields.append(field_name)
2✔
1186
                    continue
2✔
1187
                elif not new_field:
2✔
1188
                    raise LookUpError('Cannot resolve field "%s"' % field_name)
2✔
1189

1190
                field = new_field  # update field to the new field type
2✔
1191

1192
            fields.append(field)
2✔
1193

1194
        return fields
2✔
1195

1196
    @classmethod
2✔
1197
    def _translate_field_name(cls, field, sep="."):
2✔
1198
        """Translate a field attribute name to a database field name."""
1199
        parts = field.split(sep)
2✔
1200
        parts = [f.db_field for f in cls._lookup_field(parts)]
2✔
1201
        return ".".join(parts)
2✔
1202

1203
    def __set_field_display(self):
2✔
1204
        """For each field that specifies choices, create a
1205
        get_<field>_display method.
1206
        """
1207
        fields_with_choices = [(n, f) for n, f in self._fields.items() if f.choices]
2✔
1208
        for attr_name, field in fields_with_choices:
2✔
1209
            setattr(
2✔
1210
                self,
1211
                "get_%s_display" % attr_name,
1212
                partial(self.__get_field_display, field=field),
1213
            )
1214

1215
    def __get_field_display(self, field):
2✔
1216
        """Return the display value for a choice field"""
1217
        value = getattr(self, field.name)
2✔
1218
        if field.choices and isinstance(field.choices[0], (list, tuple)):
2✔
1219
            if value is None:
2✔
1220
                return None
2✔
1221
            sep = getattr(field, "display_sep", " ")
2✔
1222
            values = (
2✔
1223
                value
1224
                if field.__class__.__name__ in ("ListField", "SortedListField")
1225
                else [value]
1226
            )
1227
            return sep.join(
2✔
1228
                [str(dict(field.choices).get(val, val)) for val in values or []]
1229
            )
1230
        return value
2✔
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