• 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

92.93
/mongoengine/fields.py
1
import datetime
2✔
2
import decimal
2✔
3
import inspect
2✔
4
import itertools
2✔
5
import re
2✔
6
import socket
2✔
7
import time
2✔
8
import uuid
2✔
9
from inspect import isclass
2✔
10
from io import BytesIO
2✔
11
from operator import itemgetter
2✔
12

13
import gridfs
2✔
14
import pymongo
2✔
15
from bson import SON, Binary, DBRef, ObjectId
2✔
16
from bson.decimal128 import Decimal128, create_decimal128_context
2✔
17
from pymongo import ReturnDocument
2✔
18

19
try:
2✔
20
    import dateutil
2✔
21
except ImportError:
2✔
22
    dateutil = None
2✔
23
else:
24
    import dateutil.parser
×
25

26
from mongoengine.base import (
2✔
27
    BaseDocument,
28
    BaseField,
29
    ComplexBaseField,
30
    GeoJsonBaseField,
31
    LazyReference,
32
    ObjectIdField,
33
    _DocumentRegistry,
34
)
35
from mongoengine.base.utils import LazyRegexCompiler
2✔
36
from mongoengine.common import _import_class
2✔
37
from mongoengine.connection import (
2✔
38
    DEFAULT_CONNECTION_NAME,
39
    _get_session,
40
    get_db,
41
)
42
from mongoengine.document import Document, EmbeddedDocument
2✔
43
from mongoengine.errors import (
2✔
44
    DoesNotExist,
45
    InvalidQueryError,
46
    ValidationError,
47
)
48
from mongoengine.queryset import DO_NOTHING
2✔
49
from mongoengine.queryset.base import BaseQuerySet
2✔
50
from mongoengine.queryset.transform import STRING_OPERATORS
2✔
51

52
try:
2✔
53
    from PIL import Image, ImageOps
2✔
54

55
    if hasattr(Image, "Resampling"):
2✔
56
        LANCZOS = Image.Resampling.LANCZOS
2✔
57
    else:
58
        LANCZOS = Image.LANCZOS
×
59
except ImportError:
×
60
    # pillow is optional so may not be installed
61
    Image = None
×
62
    ImageOps = None
×
63

64

65
__all__ = (
2✔
66
    "StringField",
67
    "URLField",
68
    "EmailField",
69
    "IntField",
70
    "FloatField",
71
    "DecimalField",
72
    "BooleanField",
73
    "DateTimeField",
74
    "DateField",
75
    "ComplexDateTimeField",
76
    "EmbeddedDocumentField",
77
    "ObjectIdField",
78
    "GenericEmbeddedDocumentField",
79
    "DynamicField",
80
    "ListField",
81
    "SortedListField",
82
    "EmbeddedDocumentListField",
83
    "DictField",
84
    "MapField",
85
    "ReferenceField",
86
    "CachedReferenceField",
87
    "LazyReferenceField",
88
    "GenericLazyReferenceField",
89
    "GenericReferenceField",
90
    "BinaryField",
91
    "GridFSError",
92
    "GridFSProxy",
93
    "FileField",
94
    "ImageGridFsProxy",
95
    "ImproperlyConfigured",
96
    "ImageField",
97
    "GeoPointField",
98
    "PointField",
99
    "LineStringField",
100
    "PolygonField",
101
    "SequenceField",
102
    "UUIDField",
103
    "EnumField",
104
    "MultiPointField",
105
    "MultiLineStringField",
106
    "MultiPolygonField",
107
    "GeoJsonBaseField",
108
    "Decimal128Field",
109
)
110

111
RECURSIVE_REFERENCE_CONSTANT = "self"
2✔
112

113

114
def _unsaved_object_error(document):
2✔
115
    return (
2✔
116
        f"The instance of the document '{document}' you are "
117
        "trying to reference has an empty 'id'. You can only reference "
118
        "documents once they have been saved to the database"
119
    )
120

121

122
class StringField(BaseField):
2✔
123
    """A unicode string field."""
124

125
    def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
2✔
126
        """
127
        :param regex: (optional) A string pattern that will be applied during validation
128
        :param max_length: (optional) A max length that will be applied during validation
129
        :param min_length: (optional) A min length that will be applied during validation
130
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
131
        """
132
        self.regex = re.compile(regex) if regex else None
2✔
133
        self.max_length = max_length
2✔
134
        self.min_length = min_length
2✔
135
        super().__init__(**kwargs)
2✔
136

137
    def to_python(self, value):
2✔
138
        if isinstance(value, str):
2✔
139
            return value
2✔
140
        try:
2✔
141
            value = value.decode("utf-8")
2✔
142
        except Exception:
2✔
143
            pass
2✔
144
        return value
2✔
145

146
    def validate(self, value):
2✔
147
        if not isinstance(value, str):
2✔
148
            self.error("StringField only accepts string values")
2✔
149

150
        if self.max_length is not None and len(value) > self.max_length:
2✔
151
            self.error("String value is too long")
2✔
152

153
        if self.min_length is not None and len(value) < self.min_length:
2✔
154
            self.error("String value is too short")
2✔
155

156
        if self.regex is not None and self.regex.match(value) is None:
2✔
157
            self.error("String value did not match validation regex")
2✔
158

159
    def lookup_member(self, member_name):
2✔
160
        return None
2✔
161

162
    def prepare_query_value(self, op, value):
2✔
163
        if not isinstance(op, str):
2✔
164
            return value
2✔
165

166
        if op in STRING_OPERATORS:
2✔
167
            case_insensitive = op.startswith("i")
2✔
168
            op = op.lstrip("i")
2✔
169

170
            flags = re.IGNORECASE if case_insensitive else 0
2✔
171

172
            regex = r"%s"
2✔
173
            if op == "startswith":
2✔
174
                regex = r"^%s"
2✔
175
            elif op == "endswith":
2✔
176
                regex = r"%s$"
2✔
177
            elif op == "exact":
2✔
178
                regex = r"^%s$"
2✔
179
            elif op == "wholeword":
2✔
180
                regex = r"\b%s\b"
2✔
181
            elif op == "regex":
2✔
182
                regex = value
2✔
183

184
            if op == "regex":
2✔
185
                value = re.compile(regex, flags)
2✔
186
            else:
187
                # escape unsafe characters which could lead to a re.error
188
                value = re.escape(value)
2✔
189
                value = re.compile(regex % value, flags)
2✔
190
        return super().prepare_query_value(op, value)
2✔
191

192

193
class URLField(StringField):
2✔
194
    """A field that validates input as an URL."""
195

196
    _URL_REGEX = LazyRegexCompiler(
2✔
197
        r"^(?:[a-z0-9\.\-]*)://"  # scheme is validated separately
198
        r"(?:(?:[A-Z0-9](?:[A-Z0-9-_]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(?<!-)\.?)|"  # domain...
199
        r"localhost|"  # localhost...
200
        r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|"  # ...or ipv4
201
        r"\[?[A-F0-9]*:[A-F0-9:]+\]?)"  # ...or ipv6
202
        r"(?::\d+)?"  # optional port
203
        r"(?:/?|[/?]\S+)$",
204
        re.IGNORECASE,
205
    )
206
    _URL_SCHEMES = ["http", "https", "ftp", "ftps"]
2✔
207

208
    def __init__(self, url_regex=None, schemes=None, **kwargs):
2✔
209
        """
210
        :param url_regex: (optional) Overwrite the default regex used for validation
211
        :param schemes: (optional) Overwrite the default URL schemes that are allowed
212
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField`
213
        """
214
        self.url_regex = url_regex or self._URL_REGEX
2✔
215
        self.schemes = schemes or self._URL_SCHEMES
2✔
216
        super().__init__(**kwargs)
2✔
217

218
    def validate(self, value):
2✔
219
        # Check first if the scheme is valid
220
        scheme = value.split("://")[0].lower()
2✔
221
        if scheme not in self.schemes:
2✔
222
            self.error(f"Invalid scheme {scheme} in URL: {value}")
2✔
223

224
        # Then check full URL
225
        if not self.url_regex.match(value):
2✔
226
            self.error(f"Invalid URL: {value}")
2✔
227

228

229
class EmailField(StringField):
2✔
230
    """A field that validates input as an email address."""
231

232
    USER_REGEX = LazyRegexCompiler(
2✔
233
        # `dot-atom` defined in RFC 5322 Section 3.2.3.
234
        r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"
235
        # `quoted-string` defined in RFC 5322 Section 3.2.4.
236
        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)',
237
        re.IGNORECASE,
238
    )
239

240
    UTF8_USER_REGEX = LazyRegexCompiler(
2✔
241
        (
242
            # RFC 6531 Section 3.3 extends `atext` (used by dot-atom) to
243
            # include `UTF8-non-ascii`.
244
            r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z\u0080-\U0010FFFF]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z\u0080-\U0010FFFF]+)*\Z"
245
            # `quoted-string`
246
            r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)'
247
        ),
248
        re.IGNORECASE | re.UNICODE,
249
    )
250

251
    DOMAIN_REGEX = LazyRegexCompiler(
2✔
252
        r"((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z",
253
        re.IGNORECASE,
254
    )
255

256
    error_msg = "Invalid email address: %s"
2✔
257

258
    def __init__(
2✔
259
        self,
260
        domain_whitelist=None,
261
        allow_utf8_user=False,
262
        allow_ip_domain=False,
263
        *args,
264
        **kwargs,
265
    ):
266
        """
267
        :param domain_whitelist: (optional) list of valid domain names applied during validation
268
        :param allow_utf8_user: Allow user part of the email to contain utf8 char
269
        :param allow_ip_domain: Allow domain part of the email to be an IPv4 or IPv6 address
270
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField`
271
        """
272
        self.domain_whitelist = domain_whitelist or []
2✔
273
        self.allow_utf8_user = allow_utf8_user
2✔
274
        self.allow_ip_domain = allow_ip_domain
2✔
275
        super().__init__(*args, **kwargs)
2✔
276

277
    def validate_user_part(self, user_part):
2✔
278
        """Validate the user part of the email address. Return True if
279
        valid and False otherwise.
280
        """
281
        if self.allow_utf8_user:
2✔
282
            return self.UTF8_USER_REGEX.match(user_part)
2✔
283
        return self.USER_REGEX.match(user_part)
2✔
284

285
    def validate_domain_part(self, domain_part):
2✔
286
        """Validate the domain part of the email address. Return True if
287
        valid and False otherwise.
288
        """
289
        # Skip domain validation if it's in the whitelist.
290
        if domain_part in self.domain_whitelist:
2✔
291
            return True
2✔
292

293
        if self.DOMAIN_REGEX.match(domain_part):
2✔
294
            return True
2✔
295

296
        # Validate IPv4/IPv6, e.g. user@[192.168.0.1]
297
        if self.allow_ip_domain and domain_part[0] == "[" and domain_part[-1] == "]":
2✔
298
            for addr_family in (socket.AF_INET, socket.AF_INET6):
2✔
299
                try:
2✔
300
                    socket.inet_pton(addr_family, domain_part[1:-1])
2✔
301
                    return True
2✔
302
                except (OSError, UnicodeEncodeError):
2✔
303
                    pass
2✔
304

305
        return False
2✔
306

307
    def validate(self, value):
2✔
308
        super().validate(value)
2✔
309

310
        if "@" not in value:
2✔
311
            self.error(self.error_msg % value)
2✔
312

313
        user_part, domain_part = value.rsplit("@", 1)
2✔
314

315
        # Validate the user part.
316
        if not self.validate_user_part(user_part):
2✔
317
            self.error(self.error_msg % value)
2✔
318

319
        # Validate the domain and, if invalid, see if it's IDN-encoded.
320
        if not self.validate_domain_part(domain_part):
2✔
321
            try:
2✔
322
                domain_part = domain_part.encode("idna").decode("ascii")
2✔
323
            except UnicodeError:
2✔
324
                self.error(
2✔
325
                    "{} {}".format(
326
                        self.error_msg % value, "(domain failed IDN encoding)"
327
                    )
328
                )
329
            else:
330
                if not self.validate_domain_part(domain_part):
2✔
331
                    self.error(
2✔
332
                        "{} {}".format(
333
                            self.error_msg % value, "(domain validation failed)"
334
                        )
335
                    )
336

337

338
class IntField(BaseField):
2✔
339
    """32-bit integer field."""
340

341
    def __init__(self, min_value=None, max_value=None, **kwargs):
2✔
342
        """
343
        :param min_value: (optional) A min value that will be applied during validation
344
        :param max_value: (optional) A max value that will be applied during validation
345
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
346
        """
347
        self.min_value, self.max_value = min_value, max_value
2✔
348
        super().__init__(**kwargs)
2✔
349

350
    def to_python(self, value):
2✔
351
        try:
2✔
352
            value = int(value)
2✔
353
        except (TypeError, ValueError):
2✔
354
            pass
2✔
355
        return value
2✔
356

357
    def validate(self, value):
2✔
358
        try:
2✔
359
            value = int(value)
2✔
360
        except (TypeError, ValueError):
2✔
361
            self.error("%s could not be converted to int" % value)
2✔
362

363
        if self.min_value is not None and value < self.min_value:
2✔
364
            self.error("Integer value is too small")
2✔
365

366
        if self.max_value is not None and value > self.max_value:
2✔
367
            self.error("Integer value is too large")
2✔
368

369
    def prepare_query_value(self, op, value):
2✔
370
        if value is None:
2✔
371
            return value
2✔
372

373
        return super().prepare_query_value(op, int(value))
2✔
374

375

376
class FloatField(BaseField):
2✔
377
    """Floating point number field."""
378

379
    def __init__(self, min_value=None, max_value=None, **kwargs):
2✔
380
        """
381
        :param min_value: (optional) A min value that will be applied during validation
382
        :param max_value: (optional) A max value that will be applied during validation
383
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
384
        """
385
        self.min_value, self.max_value = min_value, max_value
2✔
386
        super().__init__(**kwargs)
2✔
387

388
    def to_python(self, value):
2✔
389
        try:
2✔
390
            value = float(value)
2✔
391
        except ValueError:
2✔
392
            pass
2✔
393
        return value
2✔
394

395
    def validate(self, value):
2✔
396
        if isinstance(value, int):
2✔
397
            try:
2✔
398
                value = float(value)
2✔
399
            except OverflowError:
2✔
400
                self.error("The value is too large to be converted to float")
2✔
401

402
        if not isinstance(value, float):
2✔
403
            self.error("FloatField only accepts float and integer values")
2✔
404

405
        if self.min_value is not None and value < self.min_value:
2✔
406
            self.error("Float value is too small")
2✔
407

408
        if self.max_value is not None and value > self.max_value:
2✔
409
            self.error("Float value is too large")
2✔
410

411
    def prepare_query_value(self, op, value):
2✔
412
        if value is None:
2✔
413
            return value
2✔
414

415
        return super().prepare_query_value(op, float(value))
2✔
416

417

418
class DecimalField(BaseField):
2✔
419
    """Disclaimer: This field is kept for historical reason but since it converts the values to float, it
420
    is not suitable for true decimal storage. Consider using :class:`~mongoengine.fields.Decimal128Field`.
421

422
    Fixed-point decimal number field. Stores the value as a float by default unless `force_string` is used.
423
    If using floats, beware of Decimal to float conversion (potential precision loss)
424
    """
425

426
    def __init__(
2✔
427
        self,
428
        min_value=None,
429
        max_value=None,
430
        force_string=False,
431
        precision=2,
432
        rounding=decimal.ROUND_HALF_UP,
433
        **kwargs,
434
    ):
435
        """
436
        :param min_value: (optional) A min value that will be applied during validation
437
        :param max_value: (optional) A max value that will be applied during validation
438
        :param force_string: Store the value as a string (instead of a float).
439
         Be aware that this affects query sorting and operation like lte, gte (as string comparison is applied)
440
         and some query operator won't work (e.g. inc, dec)
441
        :param precision: Number of decimal places to store.
442
        :param rounding: The rounding rule from the python decimal library:
443

444
            - decimal.ROUND_CEILING (towards Infinity)
445
            - decimal.ROUND_DOWN (towards zero)
446
            - decimal.ROUND_FLOOR (towards -Infinity)
447
            - decimal.ROUND_HALF_DOWN (to nearest with ties going towards zero)
448
            - decimal.ROUND_HALF_EVEN (to nearest with ties going to nearest even integer)
449
            - decimal.ROUND_HALF_UP (to nearest with ties going away from zero)
450
            - decimal.ROUND_UP (away from zero)
451
            - decimal.ROUND_05UP (away from zero if last digit after rounding towards zero would have been 0 or 5; otherwise towards zero)
452

453
            Defaults to: ``decimal.ROUND_HALF_UP``
454
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
455
        """
456
        self.min_value = min_value
2✔
457
        self.max_value = max_value
2✔
458
        self.force_string = force_string
2✔
459

460
        if precision < 0 or not isinstance(precision, int):
2✔
461
            self.error("precision must be a positive integer")
2✔
462

463
        self.precision = precision
2✔
464
        self.rounding = rounding
2✔
465

466
        super().__init__(**kwargs)
2✔
467

468
    def to_python(self, value):
2✔
469
        # Convert to string for python 2.6 before casting to Decimal
470
        try:
2✔
471
            value = decimal.Decimal("%s" % value)
2✔
472
        except (TypeError, ValueError, decimal.InvalidOperation):
2✔
473
            return value
2✔
474
        if self.precision > 0:
2✔
475
            return value.quantize(
2✔
476
                decimal.Decimal(".%s" % ("0" * self.precision)), rounding=self.rounding
477
            )
478
        else:
479
            return value.quantize(decimal.Decimal(), rounding=self.rounding)
2✔
480

481
    def to_mongo(self, value):
2✔
482
        if self.force_string:
2✔
483
            return str(self.to_python(value))
2✔
484
        return float(self.to_python(value))
2✔
485

486
    def validate(self, value):
2✔
487
        if not isinstance(value, decimal.Decimal):
2✔
488
            if not isinstance(value, str):
2✔
489
                value = str(value)
2✔
490
            try:
2✔
491
                value = decimal.Decimal(value)
2✔
492
            except (TypeError, ValueError, decimal.InvalidOperation) as exc:
2✔
493
                self.error("Could not convert value to decimal: %s" % exc)
2✔
494

495
        if self.min_value is not None and value < self.min_value:
2✔
496
            self.error("Decimal value is too small")
2✔
497

498
        if self.max_value is not None and value > self.max_value:
2✔
499
            self.error("Decimal value is too large")
2✔
500

501
    def prepare_query_value(self, op, value):
2✔
502
        if value is None:
2✔
503
            return value
2✔
504
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
505

506

507
class BooleanField(BaseField):
2✔
508
    """Boolean field type."""
509

510
    def to_python(self, value):
2✔
511
        try:
2✔
512
            value = bool(value)
2✔
513
        except (ValueError, TypeError):
2✔
514
            pass
2✔
515
        return value
2✔
516

517
    def validate(self, value):
2✔
518
        if not isinstance(value, bool):
2✔
519
            self.error("BooleanField only accepts boolean values")
2✔
520

521

522
class DateTimeField(BaseField):
2✔
523
    """Datetime field.
524

525
    Uses the python-dateutil library if available alternatively use time.strptime
526
    to parse the dates.  Note: python-dateutil's parser is fully featured and when
527
    installed you can utilise it to convert varying types of date formats into valid
528
    python datetime objects.
529

530
    Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow)
531

532
    Note: Microseconds are rounded to the nearest millisecond.
533
      Pre UTC microsecond support is effectively broken.
534
      Use :class:`~mongoengine.fields.ComplexDateTimeField` if you
535
      need accurate microsecond support.
536
    """
537

538
    def validate(self, value):
2✔
539
        new_value = self.to_mongo(value)
2✔
540
        if not isinstance(new_value, (datetime.datetime, datetime.date)):
2✔
541
            self.error('cannot parse date "%s"' % value)
2✔
542

543
    def to_mongo(self, value):
2✔
544
        if value is None:
2✔
545
            return value
2✔
546
        if isinstance(value, datetime.datetime):
2✔
547
            return value
2✔
548
        if isinstance(value, datetime.date):
2✔
549
            return datetime.datetime(value.year, value.month, value.day)
2✔
550
        if callable(value):
2✔
551
            return value()
2✔
552

553
        if isinstance(value, str):
2✔
554
            return self._parse_datetime(value)
2✔
555
        else:
556
            return None
2✔
557

558
    @staticmethod
2✔
559
    def _parse_datetime(value):
2✔
560
        # Attempt to parse a datetime from a string
561
        value = value.strip()
2✔
562
        if not value:
2✔
563
            return None
2✔
564

565
        if dateutil:
2✔
566
            try:
×
567
                return dateutil.parser.parse(value)
×
568
            except (TypeError, ValueError, OverflowError):
×
569
                return None
×
570

571
        # split usecs, because they are not recognized by strptime.
572
        if "." in value:
2✔
573
            try:
2✔
574
                value, usecs = value.split(".")
2✔
575
                usecs = int(usecs)
2✔
576
            except ValueError:
2✔
577
                return None
2✔
578
        else:
579
            usecs = 0
2✔
580
        kwargs = {"microsecond": usecs}
2✔
581
        try:  # Seconds are optional, so try converting seconds first.
2✔
582
            return datetime.datetime(
2✔
583
                *time.strptime(value, "%Y-%m-%d %H:%M:%S")[:6], **kwargs
584
            )
585
        except ValueError:
2✔
586
            try:  # Try without seconds.
2✔
587
                return datetime.datetime(
2✔
588
                    *time.strptime(value, "%Y-%m-%d %H:%M")[:5], **kwargs
589
                )
590
            except ValueError:  # Try without hour/minutes/seconds.
2✔
591
                try:
2✔
592
                    return datetime.datetime(
2✔
593
                        *time.strptime(value, "%Y-%m-%d")[:3], **kwargs
594
                    )
595
                except ValueError:
2✔
596
                    return None
2✔
597

598
    def prepare_query_value(self, op, value):
2✔
599
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
600

601

602
class DateField(DateTimeField):
2✔
603
    def to_mongo(self, value):
2✔
604
        value = super().to_mongo(value)
2✔
605
        # drop hours, minutes, seconds
606
        if isinstance(value, datetime.datetime):
2✔
607
            value = datetime.datetime(value.year, value.month, value.day)
2✔
608
        return value
2✔
609

610
    def to_python(self, value):
2✔
611
        value = super().to_python(value)
2✔
612
        # convert datetime to date
613
        if isinstance(value, datetime.datetime):
2✔
614
            value = datetime.date(value.year, value.month, value.day)
2✔
615
        return value
2✔
616

617

618
class ComplexDateTimeField(StringField):
2✔
619
    """
620
    ComplexDateTimeField handles microseconds exactly instead of rounding
621
    like DateTimeField does.
622

623
    Derives from a StringField so you can do `gte` and `lte` filtering by
624
    using lexicographical comparison when filtering / sorting strings.
625

626
    The stored string has the following format:
627

628
        YYYY,MM,DD,HH,MM,SS,NNNNNN
629

630
    Where NNNNNN is the number of microseconds of the represented `datetime`.
631
    The `,` as the separator can be easily modified by passing the `separator`
632
    keyword when initializing the field.
633

634
    Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow)
635
    """
636

637
    def __init__(self, separator=",", **kwargs):
2✔
638
        """
639
        :param separator: Allows to customize the separator used for storage (default ``,``)
640
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.StringField`
641
        """
642
        self.separator = separator
2✔
643
        self.format = separator.join(["%Y", "%m", "%d", "%H", "%M", "%S", "%f"])
2✔
644
        super().__init__(**kwargs)
2✔
645

646
    def _convert_from_datetime(self, val):
2✔
647
        """
648
        Convert a `datetime` object to a string representation (which will be
649
        stored in MongoDB). This is the reverse function of
650
        `_convert_from_string`.
651

652
        >>> a = datetime(2011, 6, 8, 20, 26, 24, 92284)
653
        >>> ComplexDateTimeField()._convert_from_datetime(a)
654
        '2011,06,08,20,26,24,092284'
655
        """
656
        return val.strftime(self.format)
2✔
657

658
    def _convert_from_string(self, data):
2✔
659
        """
660
        Convert a string representation to a `datetime` object (the object you
661
        will manipulate). This is the reverse function of
662
        `_convert_from_datetime`.
663

664
        >>> a = '2011,06,08,20,26,24,092284'
665
        >>> ComplexDateTimeField()._convert_from_string(a)
666
        datetime.datetime(2011, 6, 8, 20, 26, 24, 92284)
667
        """
668
        values = [int(d) for d in data.split(self.separator)]
2✔
669
        return datetime.datetime(*values)
2✔
670

671
    def __get__(self, instance, owner):
2✔
672
        if instance is None:
2✔
673
            return self
×
674

675
        data = super().__get__(instance, owner)
2✔
676

677
        if isinstance(data, datetime.datetime) or data is None:
2✔
678
            return data
2✔
679
        return self._convert_from_string(data)
2✔
680

681
    def __set__(self, instance, value):
2✔
682
        super().__set__(instance, value)
2✔
683
        value = instance._data[self.name]
2✔
684
        if value is not None:
2✔
685
            if isinstance(value, datetime.datetime):
2✔
686
                instance._data[self.name] = self._convert_from_datetime(value)
2✔
687
            else:
688
                instance._data[self.name] = value
2✔
689

690
    def validate(self, value):
2✔
691
        value = self.to_python(value)
2✔
692
        if not isinstance(value, datetime.datetime):
2✔
693
            self.error("Only datetime objects may used in a ComplexDateTimeField")
2✔
694

695
    def to_python(self, value):
2✔
696
        original_value = value
2✔
697
        try:
2✔
698
            return self._convert_from_string(value)
2✔
699
        except Exception:
2✔
700
            return original_value
2✔
701

702
    def to_mongo(self, value):
2✔
703
        value = self.to_python(value)
2✔
704
        return self._convert_from_datetime(value)
2✔
705

706
    def prepare_query_value(self, op, value):
2✔
707
        if value is None:
2✔
708
            return value
2✔
709
        return super().prepare_query_value(op, self._convert_from_datetime(value))
2✔
710

711

712
class EmbeddedDocumentField(BaseField):
2✔
713
    """An embedded document field - with a declared document_type.
714
    Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
715
    """
716

717
    def __init__(self, document_type, **kwargs):
2✔
718
        if not (
2✔
719
            isinstance(document_type, str)
720
            or issubclass(document_type, EmbeddedDocument)
721
        ):
722
            self.error(
2✔
723
                "Invalid embedded document class provided to an "
724
                "EmbeddedDocumentField"
725
            )
726

727
        self.document_type_obj = document_type
2✔
728
        super().__init__(**kwargs)
2✔
729

730
    @property
2✔
731
    def document_type(self):
2✔
732
        if isinstance(self.document_type_obj, str):
2✔
733
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
734
                resolved_document_type = self.owner_document
2✔
735
            else:
736
                resolved_document_type = _DocumentRegistry.get(self.document_type_obj)
2✔
737

738
            if not issubclass(resolved_document_type, EmbeddedDocument):
2✔
739
                # Due to the late resolution of the document_type
740
                # There is a chance that it won't be an EmbeddedDocument (#1661)
741
                self.error(
2✔
742
                    "Invalid embedded document class provided to an "
743
                    "EmbeddedDocumentField"
744
                )
745
            self.document_type_obj = resolved_document_type
2✔
746

747
        return self.document_type_obj
2✔
748

749
    def to_python(self, value):
2✔
750
        if not isinstance(value, self.document_type):
2✔
751
            return self.document_type._from_son(
2✔
752
                value, _auto_dereference=self._auto_dereference
753
            )
754
        return value
2✔
755

756
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
757
        if not isinstance(value, self.document_type):
2✔
758
            return value
2✔
759
        return self.document_type.to_mongo(value, use_db_field, fields)
2✔
760

761
    def validate(self, value, clean=True):
2✔
762
        """Make sure that the document instance is an instance of the
763
        EmbeddedDocument subclass provided when the document was defined.
764
        """
765
        # Using isinstance also works for subclasses of self.document
766
        if not isinstance(value, self.document_type):
2✔
767
            self.error(
2✔
768
                "Invalid embedded document instance provided to an "
769
                "EmbeddedDocumentField"
770
            )
771
        value.validate(clean=clean)
2✔
772

773
    def lookup_member(self, member_name):
2✔
774
        doc_and_subclasses = [self.document_type] + self.document_type.__subclasses__()
2✔
775
        for doc_type in doc_and_subclasses:
2✔
776
            field = doc_type._fields.get(member_name)
2✔
777
            if field:
2✔
778
                return field
2✔
779

780
    def prepare_query_value(self, op, value):
2✔
781
        if value is not None and not isinstance(value, self.document_type):
2✔
782
            # Short circuit for special operators, returning them as is
783
            if isinstance(value, dict) and all(k.startswith("$") for k in value.keys()):
2✔
784
                return value
2✔
785
            try:
2✔
786
                value = self.document_type._from_son(value)
2✔
787
            except ValueError:
2✔
788
                raise InvalidQueryError(
2✔
789
                    "Querying the embedded document '%s' failed, due to an invalid query value"
790
                    % (self.document_type._class_name,)
791
                )
792
        super().prepare_query_value(op, value)
2✔
793
        return self.to_mongo(value)
2✔
794

795

796
class GenericEmbeddedDocumentField(BaseField):
2✔
797
    """A generic embedded document field - allows any
798
    :class:`~mongoengine.EmbeddedDocument` to be stored.
799

800
    Only valid values are subclasses of :class:`~mongoengine.EmbeddedDocument`.
801

802
    .. note ::
803
        You can use the choices param to limit the acceptable
804
        EmbeddedDocument types
805
    """
806

807
    def prepare_query_value(self, op, value):
2✔
808
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
809

810
    def to_python(self, value):
2✔
811
        if isinstance(value, dict):
2✔
812
            doc_cls = _DocumentRegistry.get(value["_cls"])
2✔
813
            value = doc_cls._from_son(value)
2✔
814

815
        return value
2✔
816

817
    def validate(self, value, clean=True):
2✔
818
        if self.choices and isinstance(value, SON):
2✔
819
            for choice in self.choices:
2✔
820
                if value["_cls"] == choice._class_name:
2✔
821
                    return True
2✔
822

823
        if not isinstance(value, EmbeddedDocument):
2✔
824
            self.error(
×
825
                "Invalid embedded document instance provided to an "
826
                "GenericEmbeddedDocumentField"
827
            )
828

829
        value.validate(clean=clean)
2✔
830

831
    def lookup_member(self, member_name):
2✔
832
        document_choices = self.choices or []
2✔
833
        for document_choice in document_choices:
2✔
834
            doc_and_subclasses = [document_choice] + document_choice.__subclasses__()
2✔
835
            for doc_type in doc_and_subclasses:
2✔
836
                field = doc_type._fields.get(member_name)
2✔
837
                if field:
2✔
838
                    return field
2✔
839

840
    def to_mongo(self, document, use_db_field=True, fields=None):
2✔
841
        if document is None:
2✔
842
            return None
×
843
        data = document.to_mongo(use_db_field, fields)
2✔
844
        if "_cls" not in data:
2✔
845
            data["_cls"] = document._class_name
2✔
846
        return data
2✔
847

848

849
class DynamicField(BaseField):
2✔
850
    """A truly dynamic field type capable of handling different and varying
851
    types of data.
852

853
    Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
854

855
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
856
        """Convert a Python type to a MongoDB compatible type."""
857

858
        if isinstance(value, str):
2✔
859
            return value
2✔
860

861
        if hasattr(value, "to_mongo"):
2✔
862
            cls = value.__class__
2✔
863
            val = value.to_mongo(use_db_field, fields)
2✔
864
            # If we its a document thats not inherited add _cls
865
            if isinstance(value, Document):
2✔
866
                val = {"_ref": value.to_dbref(), "_cls": cls.__name__}
2✔
867
            if isinstance(value, EmbeddedDocument):
2✔
868
                val["_cls"] = cls.__name__
2✔
869
            return val
2✔
870

871
        if not isinstance(value, (dict, list, tuple)):
2✔
872
            return value
2✔
873

874
        is_list = False
2✔
875
        if not hasattr(value, "items"):
2✔
876
            is_list = True
2✔
877
            value = {k: v for k, v in enumerate(value)}
2✔
878

879
        data = {}
2✔
880
        for k, v in value.items():
2✔
881
            data[k] = self.to_mongo(v, use_db_field, fields)
2✔
882

883
        value = data
2✔
884
        if is_list:  # Convert back to a list
2✔
885
            value = [v for k, v in sorted(data.items(), key=itemgetter(0))]
2✔
886
        return value
2✔
887

888
    def to_python(self, value):
2✔
889
        if isinstance(value, dict) and "_cls" in value:
2✔
890
            doc_cls = _DocumentRegistry.get(value["_cls"])
2✔
891
            if "_ref" in value:
2✔
892
                value = doc_cls._get_db().dereference(
2✔
893
                    value["_ref"], session=_get_session()
894
                )
895
            return doc_cls._from_son(value)
2✔
896

897
        return super().to_python(value)
2✔
898

899
    def lookup_member(self, member_name):
2✔
900
        return member_name
×
901

902
    def prepare_query_value(self, op, value):
2✔
903
        if isinstance(value, str):
2✔
904
            return StringField().prepare_query_value(op, value)
2✔
905
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
906

907
    def validate(self, value, clean=True):
2✔
908
        if hasattr(value, "validate"):
2✔
909
            value.validate(clean=clean)
2✔
910

911

912
class ListField(ComplexBaseField):
2✔
913
    """A list field that wraps a standard field, allowing multiple instances
914
    of the field to be used as a list in the database.
915

916
    If using with ReferenceFields see: :ref:`many-to-many-with-listfields`
917

918
    .. note::
919
        Required means it cannot be empty - as the default for ListFields is []
920
    """
921

922
    def __init__(self, field=None, *, max_length=None, **kwargs):
2✔
923
        self.max_length = max_length
2✔
924
        kwargs.setdefault("default", list)
2✔
925
        super().__init__(field=field, **kwargs)
2✔
926

927
    def __get__(self, instance, owner):
2✔
928
        if instance is None:
2✔
929
            # Document class being used rather than a document object
930
            return self
2✔
931
        value = instance._data.get(self.name)
2✔
932
        LazyReferenceField = _import_class("LazyReferenceField")
2✔
933
        GenericLazyReferenceField = _import_class("GenericLazyReferenceField")
2✔
934
        if (
2✔
935
            isinstance(self.field, (LazyReferenceField, GenericLazyReferenceField))
936
            and value
937
        ):
938
            instance._data[self.name] = [self.field.build_lazyref(x) for x in value]
2✔
939
        return super().__get__(instance, owner)
2✔
940

941
    def validate(self, value):
2✔
942
        """Make sure that a list of valid fields is being used."""
943
        if not isinstance(value, (list, tuple, BaseQuerySet)):
2✔
944
            self.error("Only lists and tuples may be used in a list field")
2✔
945

946
        # Validate that max_length is not exceeded.
947
        # NOTE It's still possible to bypass this enforcement by using $push.
948
        # However, if the document is reloaded after $push and then re-saved,
949
        # the validation error will be raised.
950
        if self.max_length is not None and len(value) > self.max_length:
2✔
951
            self.error("List is too long")
2✔
952

953
        super().validate(value)
2✔
954

955
    def prepare_query_value(self, op, value):
2✔
956
        # Validate that the `set` operator doesn't contain more items than `max_length`.
957
        if op == "set" and self.max_length is not None and len(value) > self.max_length:
2✔
958
            self.error("List is too long")
2✔
959

960
        if self.field:
2✔
961
            # If the value is iterable and it's not a string nor a
962
            # BaseDocument, call prepare_query_value for each of its items.
963
            is_iter = hasattr(value, "__iter__")
2✔
964
            eligible_iter = is_iter and not isinstance(value, (str, BaseDocument))
2✔
965
            if (
2✔
966
                op in ("set", "unset", "gt", "gte", "lt", "lte", "ne", None)
967
                and eligible_iter
968
            ):
969
                return [self.field.prepare_query_value(op, v) for v in value]
2✔
970

971
            return self.field.prepare_query_value(op, value)
2✔
972

973
        return super().prepare_query_value(op, value)
2✔
974

975

976
class EmbeddedDocumentListField(ListField):
2✔
977
    """A :class:`~mongoengine.ListField` designed specially to hold a list of
978
    embedded documents to provide additional query helpers.
979

980
    .. note::
981
        The only valid list values are subclasses of
982
        :class:`~mongoengine.EmbeddedDocument`.
983
    """
984

985
    def __init__(self, document_type, **kwargs):
2✔
986
        """
987
        :param document_type: The type of
988
         :class:`~mongoengine.EmbeddedDocument` the list will hold.
989
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.ListField`
990
        """
991
        super().__init__(field=EmbeddedDocumentField(document_type), **kwargs)
2✔
992

993

994
class SortedListField(ListField):
2✔
995
    """A ListField that sorts the contents of its list before writing to
996
    the database in order to ensure that a sorted list is always
997
    retrieved.
998

999
    .. warning::
1000
        There is a potential race condition when handling lists.  If you set /
1001
        save the whole list then other processes trying to save the whole list
1002
        as well could overwrite changes.  The safest way to append to a list is
1003
        to perform a push operation.
1004
    """
1005

1006
    def __init__(self, field, **kwargs):
2✔
1007
        self._ordering = kwargs.pop("ordering", None)
2✔
1008
        self._order_reverse = kwargs.pop("reverse", False)
2✔
1009
        super().__init__(field, **kwargs)
2✔
1010

1011
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
1012
        value = super().to_mongo(value, use_db_field, fields)
2✔
1013
        if self._ordering is not None:
2✔
1014
            return sorted(
2✔
1015
                value, key=itemgetter(self._ordering), reverse=self._order_reverse
1016
            )
1017
        return sorted(value, reverse=self._order_reverse)
2✔
1018

1019

1020
def key_not_string(d):
2✔
1021
    """Helper function to recursively determine if any key in a
1022
    dictionary is not a string.
1023
    """
1024
    for k, v in d.items():
2✔
1025
        if not isinstance(k, str) or (isinstance(v, dict) and key_not_string(v)):
2✔
1026
            return True
2✔
1027

1028

1029
def key_starts_with_dollar(d):
2✔
1030
    """Helper function to recursively determine if any key in a
1031
    dictionary starts with a dollar
1032
    """
1033
    for k, v in d.items():
2✔
1034
        if (k.startswith("$")) or (isinstance(v, dict) and key_starts_with_dollar(v)):
2✔
1035
            return True
2✔
1036

1037

1038
class DictField(ComplexBaseField):
2✔
1039
    """A dictionary field that wraps a standard Python dictionary. This is
1040
    similar to an embedded document, but the structure is not defined.
1041

1042
    .. note::
1043
        Required means it cannot be empty - as the default for DictFields is {}
1044
    """
1045

1046
    def __init__(self, field=None, *args, **kwargs):
2✔
1047
        kwargs.setdefault("default", dict)
2✔
1048
        super().__init__(*args, field=field, **kwargs)
2✔
1049
        self.set_auto_dereferencing(False)
2✔
1050

1051
    def validate(self, value):
2✔
1052
        """Make sure that a list of valid fields is being used."""
1053
        if not isinstance(value, dict):
2✔
1054
            self.error("Only dictionaries may be used in a DictField")
2✔
1055

1056
        if key_not_string(value):
2✔
1057
            msg = "Invalid dictionary key - documents must have only string keys"
2✔
1058
            self.error(msg)
2✔
1059

1060
        # Following condition applies to MongoDB >= 3.6
1061
        # older Mongo has stricter constraints but
1062
        # it will be rejected upon insertion anyway
1063
        # Having a validation that depends on the MongoDB version
1064
        # is not straightforward as the field isn't aware of the connected Mongo
1065
        if key_starts_with_dollar(value):
2✔
1066
            self.error(
2✔
1067
                'Invalid dictionary key name - keys may not startswith "$" characters'
1068
            )
1069
        super().validate(value)
2✔
1070

1071
    def lookup_member(self, member_name):
2✔
1072
        return DictField(db_field=member_name)
2✔
1073

1074
    def prepare_query_value(self, op, value):
2✔
1075
        match_operators = [*STRING_OPERATORS]
2✔
1076

1077
        if op in match_operators and isinstance(value, str):
2✔
1078
            return StringField().prepare_query_value(op, value)
2✔
1079

1080
        if hasattr(
2✔
1081
            self.field, "field"
1082
        ):  # Used for instance when using DictField(ListField(IntField()))
1083
            if op in ("set", "unset") and isinstance(value, dict):
2✔
1084
                return {
2✔
1085
                    k: self.field.prepare_query_value(op, v) for k, v in value.items()
1086
                }
1087
            return self.field.prepare_query_value(op, value)
2✔
1088

1089
        return super().prepare_query_value(op, value)
2✔
1090

1091

1092
class MapField(DictField):
2✔
1093
    """A field that maps a name to a specified field type. Similar to
1094
    a DictField, except the 'value' of each item must match the specified
1095
    field type.
1096
    """
1097

1098
    def __init__(self, field=None, *args, **kwargs):
2✔
1099
        # XXX ValidationError raised outside the "validate" method.
1100
        if not isinstance(field, BaseField):
2✔
1101
            self.error("Argument to MapField constructor must be a valid field")
2✔
1102
        super().__init__(field=field, *args, **kwargs)
2✔
1103

1104

1105
class ReferenceField(BaseField):
2✔
1106
    """A reference to a document that will be automatically dereferenced on
1107
    access (lazily).
1108

1109
    Note this means you will get a database I/O access everytime you access
1110
    this field. This is necessary because the field returns a :class:`~mongoengine.Document`
1111
    which precise type can depend of the value of the `_cls` field present in the
1112
    document in database.
1113
    In short, using this type of field can lead to poor performances (especially
1114
    if you access this field only to retrieve it `pk` field which is already
1115
    known before dereference). To solve this you should consider using the
1116
    :class:`~mongoengine.fields.LazyReferenceField`.
1117

1118
    Use the `reverse_delete_rule` to handle what should happen if the document
1119
    the field is referencing is deleted.  EmbeddedDocuments, DictFields and
1120
    MapFields does not support reverse_delete_rule and an `InvalidDocumentError`
1121
    will be raised if trying to set on one of these Document / Field types.
1122

1123
    The options are:
1124

1125
      * DO_NOTHING (0)  - don't do anything (default).
1126
      * NULLIFY    (1)  - Updates the reference to null.
1127
      * CASCADE    (2)  - Deletes the documents associated with the reference.
1128
      * DENY       (3)  - Prevent the deletion of the reference object.
1129
      * PULL       (4)  - Pull the reference from a :class:`~mongoengine.fields.ListField` of references
1130

1131
    Alternative syntax for registering delete rules (useful when implementing
1132
    bi-directional delete rules)
1133

1134
    .. code-block:: python
1135

1136
        class Org(Document):
1137
            owner = ReferenceField('User')
1138

1139
        class User(Document):
1140
            org = ReferenceField('Org', reverse_delete_rule=CASCADE)
1141

1142
        User.register_delete_rule(Org, 'owner', DENY)
1143
    """
1144

1145
    def __init__(
2✔
1146
        self, document_type, dbref=False, reverse_delete_rule=DO_NOTHING, **kwargs
1147
    ):
1148
        """Initialises the Reference Field.
1149

1150
        :param document_type: The type of Document that will be referenced
1151
        :param dbref:  Store the reference as :class:`~pymongo.dbref.DBRef`
1152
          or as the :class:`~pymongo.objectid.ObjectId`.
1153
        :param reverse_delete_rule: Determines what to do when the referring
1154
          object is deleted
1155
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
1156

1157
        .. note ::
1158
            A reference to an abstract document type is always stored as a
1159
            :class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`.
1160
        """
1161
        # XXX ValidationError raised outside of the "validate" method.
1162
        if not (
2✔
1163
            isinstance(document_type, str)
1164
            or (isclass(document_type) and issubclass(document_type, Document))
1165
        ):
1166
            self.error(
2✔
1167
                "Argument to ReferenceField constructor must be a "
1168
                "document class or a string"
1169
            )
1170

1171
        self.dbref = dbref
2✔
1172
        self.document_type_obj = document_type
2✔
1173
        self.reverse_delete_rule = reverse_delete_rule
2✔
1174
        super().__init__(**kwargs)
2✔
1175

1176
    @property
2✔
1177
    def document_type(self):
2✔
1178
        if isinstance(self.document_type_obj, str):
2✔
1179
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
1180
                self.document_type_obj = self.owner_document
2✔
1181
            else:
1182
                self.document_type_obj = _DocumentRegistry.get(self.document_type_obj)
2✔
1183
        return self.document_type_obj
2✔
1184

1185
    @staticmethod
2✔
1186
    def _lazy_load_ref(ref_cls, dbref):
2✔
1187
        dereferenced_son = ref_cls._get_db().dereference(dbref, session=_get_session())
2✔
1188
        if dereferenced_son is None:
2✔
1189
            raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
2✔
1190

1191
        return ref_cls._from_son(dereferenced_son)
2✔
1192

1193
    def __get__(self, instance, owner):
2✔
1194
        """Descriptor to allow lazy dereferencing."""
1195
        if instance is None:
2✔
1196
            # Document class being used rather than a document object
1197
            return self
2✔
1198

1199
        # Get value from document instance if available
1200
        ref_value = instance._data.get(self.name)
2✔
1201
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
1202
        # Dereference DBRefs
1203
        if auto_dereference and isinstance(ref_value, DBRef):
2✔
1204
            if hasattr(ref_value, "cls"):
2✔
1205
                # Dereference using the class type specified in the reference
1206
                cls = _DocumentRegistry.get(ref_value.cls)
2✔
1207
            else:
1208
                cls = self.document_type
2✔
1209

1210
            instance._data[self.name] = self._lazy_load_ref(cls, ref_value)
2✔
1211

1212
        return super().__get__(instance, owner)
2✔
1213

1214
    def to_mongo(self, document):
2✔
1215
        if isinstance(document, DBRef):
2✔
1216
            if not self.dbref:
2✔
1217
                return document.id
2✔
1218
            return document
2✔
1219

1220
        if isinstance(document, Document):
2✔
1221
            # We need the id from the saved object to create the DBRef
1222
            id_ = document.pk
2✔
1223

1224
            # XXX ValidationError raised outside of the "validate" method.
1225
            if id_ is None:
2✔
1226
                self.error(_unsaved_object_error(document.__class__.__name__))
×
1227

1228
            # Use the attributes from the document instance, so that they
1229
            # override the attributes of this field's document type
1230
            cls = document
2✔
1231
        else:
1232
            id_ = document
2✔
1233
            cls = self.document_type
2✔
1234

1235
        id_field_name = cls._meta["id_field"]
2✔
1236
        id_field = cls._fields[id_field_name]
2✔
1237

1238
        id_ = id_field.to_mongo(id_)
2✔
1239
        if self.document_type._meta.get("abstract"):
2✔
1240
            collection = cls._get_collection_name()
2✔
1241
            return DBRef(collection, id_, cls=cls._class_name)
2✔
1242
        elif self.dbref:
2✔
1243
            collection = cls._get_collection_name()
2✔
1244
            return DBRef(collection, id_)
2✔
1245

1246
        return id_
2✔
1247

1248
    def to_python(self, value):
2✔
1249
        """Convert a MongoDB-compatible type to a Python type."""
1250
        if not self.dbref and not isinstance(
2✔
1251
            value, (DBRef, Document, EmbeddedDocument)
1252
        ):
1253
            collection = self.document_type._get_collection_name()
2✔
1254
            value = DBRef(collection, self.document_type.id.to_python(value))
2✔
1255
        return value
2✔
1256

1257
    def prepare_query_value(self, op, value):
2✔
1258
        if value is None:
2✔
1259
            return None
2✔
1260
        super().prepare_query_value(op, value)
2✔
1261
        return self.to_mongo(value)
2✔
1262

1263
    def validate(self, value):
2✔
1264
        if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)):
2✔
1265
            self.error(
2✔
1266
                "A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents"
1267
            )
1268

1269
        if isinstance(value, Document) and value.id is None:
2✔
1270
            self.error(_unsaved_object_error(value.__class__.__name__))
2✔
1271

1272
    def lookup_member(self, member_name):
2✔
1273
        return self.document_type._fields.get(member_name)
×
1274

1275

1276
class CachedReferenceField(BaseField):
2✔
1277
    """A referencefield with cache fields to purpose pseudo-joins"""
1278

1279
    def __init__(self, document_type, fields=None, auto_sync=True, **kwargs):
2✔
1280
        """Initialises the Cached Reference Field.
1281

1282
        :param document_type: The type of Document that will be referenced
1283
        :param fields:  A list of fields to be cached in document
1284
        :param auto_sync: if True documents are auto updated
1285
        :param kwargs: Keyword arguments passed into the parent :class:`~mongoengine.BaseField`
1286
        """
1287
        if fields is None:
2✔
1288
            fields = []
2✔
1289

1290
        # XXX ValidationError raised outside of the "validate" method.
1291
        if not isinstance(document_type, str) and not (
2✔
1292
            inspect.isclass(document_type) and issubclass(document_type, Document)
1293
        ):
1294
            self.error(
2✔
1295
                "Argument to CachedReferenceField constructor must be a"
1296
                " document class or a string"
1297
            )
1298

1299
        self.auto_sync = auto_sync
2✔
1300
        self.document_type_obj = document_type
2✔
1301
        self.fields = fields
2✔
1302
        super().__init__(**kwargs)
2✔
1303

1304
    def start_listener(self):
2✔
1305
        from mongoengine import signals
2✔
1306

1307
        signals.post_save.connect(self.on_document_pre_save, sender=self.document_type)
2✔
1308

1309
    def on_document_pre_save(self, sender, document, created, **kwargs):
2✔
1310
        if created:
2✔
1311
            return None
2✔
1312

1313
        update_kwargs = {
2✔
1314
            f"set__{self.name}__{key}": val
1315
            for key, val in document._delta()[0].items()
1316
            if key in self.fields
1317
        }
1318
        if update_kwargs:
2✔
1319
            filter_kwargs = {}
2✔
1320
            filter_kwargs[self.name] = document
2✔
1321

1322
            self.owner_document.objects(**filter_kwargs).update(**update_kwargs)
2✔
1323

1324
    def to_python(self, value):
2✔
1325
        if isinstance(value, dict):
2✔
1326
            collection = self.document_type._get_collection_name()
2✔
1327
            value = DBRef(collection, self.document_type.id.to_python(value["_id"]))
2✔
1328
            return self.document_type._from_son(
2✔
1329
                self.document_type._get_db().dereference(value, session=_get_session())
1330
            )
1331

1332
        return value
2✔
1333

1334
    @property
2✔
1335
    def document_type(self):
2✔
1336
        if isinstance(self.document_type_obj, str):
2✔
1337
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
1338
                self.document_type_obj = self.owner_document
2✔
1339
            else:
1340
                self.document_type_obj = _DocumentRegistry.get(self.document_type_obj)
×
1341
        return self.document_type_obj
2✔
1342

1343
    @staticmethod
2✔
1344
    def _lazy_load_ref(ref_cls, dbref):
2✔
1345
        dereferenced_son = ref_cls._get_db().dereference(dbref, session=_get_session())
×
1346
        if dereferenced_son is None:
×
1347
            raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
×
1348

1349
        return ref_cls._from_son(dereferenced_son)
×
1350

1351
    def __get__(self, instance, owner):
2✔
1352
        if instance is None:
2✔
1353
            # Document class being used rather than a document object
1354
            return self
2✔
1355

1356
        # Get value from document instance if available
1357
        value = instance._data.get(self.name)
2✔
1358
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
1359

1360
        # Dereference DBRefs
1361
        if auto_dereference and isinstance(value, DBRef):
2✔
1362
            instance._data[self.name] = self._lazy_load_ref(self.document_type, value)
×
1363

1364
        return super().__get__(instance, owner)
2✔
1365

1366
    def to_mongo(self, document, use_db_field=True, fields=None):
2✔
1367
        id_field_name = self.document_type._meta["id_field"]
2✔
1368
        id_field = self.document_type._fields[id_field_name]
2✔
1369

1370
        # XXX ValidationError raised outside of the "validate" method.
1371
        if isinstance(document, Document):
2✔
1372
            # We need the id from the saved object to create the DBRef
1373
            id_ = document.pk
2✔
1374
            if id_ is None:
2✔
1375
                self.error(_unsaved_object_error(document.__class__.__name__))
×
1376
        else:
1377
            self.error("Only accept a document object")
×
1378

1379
        value = SON((("_id", id_field.to_mongo(id_)),))
2✔
1380

1381
        if fields:
2✔
1382
            new_fields = [f for f in self.fields if f in fields]
×
1383
        else:
1384
            new_fields = self.fields
2✔
1385

1386
        value.update(dict(document.to_mongo(use_db_field, fields=new_fields)))
2✔
1387
        return value
2✔
1388

1389
    def prepare_query_value(self, op, value):
2✔
1390
        if value is None:
2✔
1391
            return None
2✔
1392

1393
        # XXX ValidationError raised outside of the "validate" method.
1394
        if isinstance(value, Document):
2✔
1395
            if value.pk is None:
2✔
1396
                self.error(_unsaved_object_error(value.__class__.__name__))
×
1397
            value_dict = {"_id": value.pk}
2✔
1398
            for field in self.fields:
2✔
1399
                value_dict.update({field: value[field]})
2✔
1400

1401
            return value_dict
2✔
1402

1403
        raise NotImplementedError
×
1404

1405
    def validate(self, value):
2✔
1406
        if not isinstance(value, self.document_type):
2✔
1407
            self.error("A CachedReferenceField only accepts documents")
×
1408

1409
        if isinstance(value, Document) and value.id is None:
2✔
1410
            self.error(_unsaved_object_error(value.__class__.__name__))
×
1411

1412
    def lookup_member(self, member_name):
2✔
1413
        return self.document_type._fields.get(member_name)
2✔
1414

1415
    def sync_all(self):
2✔
1416
        """
1417
        Sync all cached fields on demand.
1418
        Caution: this operation may be slower.
1419
        """
1420
        update_key = "set__%s" % self.name
2✔
1421

1422
        for doc in self.document_type.objects:
2✔
1423
            filter_kwargs = {}
2✔
1424
            filter_kwargs[self.name] = doc
2✔
1425

1426
            update_kwargs = {}
2✔
1427
            update_kwargs[update_key] = doc
2✔
1428

1429
            self.owner_document.objects(**filter_kwargs).update(**update_kwargs)
2✔
1430

1431

1432
class GenericReferenceField(BaseField):
2✔
1433
    """A reference to *any* :class:`~mongoengine.document.Document` subclass
1434
    that will be automatically dereferenced on access (lazily).
1435

1436
    Note this field works the same way as :class:`~mongoengine.document.ReferenceField`,
1437
    doing database I/O access the first time it is accessed (even if it's to access
1438
    it ``pk`` or ``id`` field).
1439
    To solve this you should consider using the
1440
    :class:`~mongoengine.fields.GenericLazyReferenceField`.
1441

1442
    .. note ::
1443
        * Any documents used as a generic reference must be registered in the
1444
          document registry.  Importing the model will automatically register
1445
          it.
1446

1447
        * You can use the choices param to limit the acceptable Document types
1448
    """
1449

1450
    def __init__(self, *args, **kwargs):
2✔
1451
        choices = kwargs.pop("choices", None)
2✔
1452
        super().__init__(*args, **kwargs)
2✔
1453
        self.choices = []
2✔
1454
        # Keep the choices as a list of allowed Document class names
1455
        if choices:
2✔
1456
            for choice in choices:
2✔
1457
                if isinstance(choice, str):
2✔
1458
                    self.choices.append(choice)
2✔
1459
                elif isinstance(choice, type) and issubclass(choice, Document):
2✔
1460
                    self.choices.append(choice._class_name)
2✔
1461
                else:
1462
                    # XXX ValidationError raised outside of the "validate"
1463
                    # method.
1464
                    self.error(
×
1465
                        "Invalid choices provided: must be a list of"
1466
                        "Document subclasses and/or str"
1467
                    )
1468

1469
    def _validate_choices(self, value):
2✔
1470
        if isinstance(value, dict):
2✔
1471
            # If the field has not been dereferenced, it is still a dict
1472
            # of class and DBRef
1473
            value = value.get("_cls")
2✔
1474
        elif isinstance(value, Document):
2✔
1475
            value = value._class_name
2✔
1476
        super()._validate_choices(value)
2✔
1477

1478
    @staticmethod
2✔
1479
    def _lazy_load_ref(ref_cls, dbref):
2✔
1480
        dereferenced_son = ref_cls._get_db().dereference(dbref, session=_get_session())
2✔
1481
        if dereferenced_son is None:
2✔
1482
            raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
2✔
1483

1484
        return ref_cls._from_son(dereferenced_son)
2✔
1485

1486
    def __get__(self, instance, owner):
2✔
1487
        if instance is None:
2✔
1488
            return self
×
1489

1490
        value = instance._data.get(self.name)
2✔
1491

1492
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
1493
        if auto_dereference and isinstance(value, dict):
2✔
1494
            doc_cls = _DocumentRegistry.get(value["_cls"])
2✔
1495
            instance._data[self.name] = self._lazy_load_ref(doc_cls, value["_ref"])
2✔
1496

1497
        return super().__get__(instance, owner)
2✔
1498

1499
    def validate(self, value):
2✔
1500
        if not isinstance(value, (Document, DBRef, dict, SON)):
2✔
1501
            self.error("GenericReferences can only contain documents")
2✔
1502

1503
        if isinstance(value, (dict, SON)):
2✔
1504
            if "_ref" not in value or "_cls" not in value:
2✔
1505
                self.error("GenericReferences can only contain documents")
×
1506

1507
        # We need the id from the saved object to create the DBRef
1508
        elif isinstance(value, Document) and value.id is None:
2✔
1509
            self.error(_unsaved_object_error(value.__class__.__name__))
2✔
1510

1511
    def to_mongo(self, document):
2✔
1512
        if document is None:
2✔
1513
            return None
×
1514

1515
        if isinstance(document, (dict, SON, ObjectId, DBRef)):
2✔
1516
            return document
2✔
1517

1518
        id_field_name = document.__class__._meta["id_field"]
2✔
1519
        id_field = document.__class__._fields[id_field_name]
2✔
1520

1521
        if isinstance(document, Document):
2✔
1522
            # We need the id from the saved object to create the DBRef
1523
            id_ = document.id
2✔
1524
            if id_ is None:
2✔
1525
                # XXX ValidationError raised outside of the "validate" method.
1526
                self.error(_unsaved_object_error(document.__class__.__name__))
×
1527
        else:
1528
            id_ = document
×
1529

1530
        id_ = id_field.to_mongo(id_)
2✔
1531
        collection = document._get_collection_name()
2✔
1532
        ref = DBRef(collection, id_)
2✔
1533
        return SON((("_cls", document._class_name), ("_ref", ref)))
2✔
1534

1535
    def prepare_query_value(self, op, value):
2✔
1536
        if value is None:
2✔
1537
            return None
2✔
1538

1539
        return self.to_mongo(value)
2✔
1540

1541

1542
class BinaryField(BaseField):
2✔
1543
    """A binary data field."""
1544

1545
    def __init__(self, max_bytes=None, **kwargs):
2✔
1546
        self.max_bytes = max_bytes
2✔
1547
        super().__init__(**kwargs)
2✔
1548

1549
    def __set__(self, instance, value):
2✔
1550
        """Handle bytearrays in python 3.1"""
1551
        if isinstance(value, bytearray):
2✔
1552
            value = bytes(value)
2✔
1553
        return super().__set__(instance, value)
2✔
1554

1555
    def to_mongo(self, value):
2✔
1556
        return Binary(value)
2✔
1557

1558
    def validate(self, value):
2✔
1559
        if not isinstance(value, (bytes, Binary)):
2✔
1560
            self.error(
2✔
1561
                "BinaryField only accepts instances of "
1562
                "(%s, %s, Binary)" % (bytes.__name__, Binary.__name__)
1563
            )
1564

1565
        if self.max_bytes is not None and len(value) > self.max_bytes:
2✔
1566
            self.error("Binary value is too long")
2✔
1567

1568
    def prepare_query_value(self, op, value):
2✔
1569
        if value is None:
2✔
1570
            return value
×
1571
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
1572

1573

1574
class EnumField(BaseField):
2✔
1575
    """Enumeration Field. Values are stored underneath as is,
1576
    so it will only work with simple types (str, int, etc) that
1577
    are bson encodable
1578

1579
    Example usage:
1580

1581
    .. code-block:: python
1582

1583
        class Status(Enum):
1584
            NEW = 'new'
1585
            ONGOING = 'ongoing'
1586
            DONE = 'done'
1587

1588
        class ModelWithEnum(Document):
1589
            status = EnumField(Status, default=Status.NEW)
1590

1591
        ModelWithEnum(status='done')
1592
        ModelWithEnum(status=Status.DONE)
1593

1594
    Enum fields can be searched using enum or its value:
1595

1596
    .. code-block:: python
1597

1598
        ModelWithEnum.objects(status='new').count()
1599
        ModelWithEnum.objects(status=Status.NEW).count()
1600

1601
    The values can be restricted to a subset of the enum by using the ``choices`` parameter:
1602

1603
    .. code-block:: python
1604

1605
        class ModelWithEnum(Document):
1606
            status = EnumField(Status, choices=[Status.NEW, Status.DONE])
1607
    """
1608

1609
    def __init__(self, enum, **kwargs):
2✔
1610
        self._enum_cls = enum
2✔
1611
        if kwargs.get("choices"):
2✔
1612
            invalid_choices = []
2✔
1613
            for choice in kwargs["choices"]:
2✔
1614
                if not isinstance(choice, enum):
2✔
1615
                    invalid_choices.append(choice)
2✔
1616
            if invalid_choices:
2✔
1617
                raise ValueError("Invalid choices: %r" % invalid_choices)
2✔
1618
        else:
1619
            kwargs["choices"] = list(self._enum_cls)  # Implicit validator
2✔
1620
        super().__init__(**kwargs)
2✔
1621

1622
    def validate(self, value):
2✔
1623
        if isinstance(value, self._enum_cls):
2✔
1624
            return super().validate(value)
2✔
1625
        try:
×
1626
            self._enum_cls(value)
×
1627
        except ValueError:
×
1628
            self.error(f"{value} is not a valid {self._enum_cls}")
×
1629

1630
    def to_python(self, value):
2✔
1631
        value = super().to_python(value)
2✔
1632
        if not isinstance(value, self._enum_cls):
2✔
1633
            try:
2✔
1634
                return self._enum_cls(value)
2✔
1635
            except ValueError:
2✔
1636
                return value
2✔
1637
        return value
2✔
1638

1639
    def __set__(self, instance, value):
2✔
1640
        return super().__set__(instance, self.to_python(value))
2✔
1641

1642
    def to_mongo(self, value):
2✔
1643
        if isinstance(value, self._enum_cls):
2✔
1644
            return value.value
2✔
1645
        return value
2✔
1646

1647
    def prepare_query_value(self, op, value):
2✔
1648
        if value is None:
2✔
1649
            return value
2✔
1650
        return super().prepare_query_value(op, self.to_mongo(value))
2✔
1651

1652

1653
class GridFSError(Exception):
2✔
1654
    pass
2✔
1655

1656

1657
class GridFSProxy:
2✔
1658
    """Proxy object to handle writing and reading of files to and from GridFS"""
1659

1660
    _fs = None
2✔
1661

1662
    def __init__(
2✔
1663
        self,
1664
        grid_id=None,
1665
        key=None,
1666
        instance=None,
1667
        db_alias=DEFAULT_CONNECTION_NAME,
1668
        collection_name="fs",
1669
    ):
1670
        self.grid_id = grid_id  # Store GridFS id for file
2✔
1671
        self.key = key
2✔
1672
        self.instance = instance
2✔
1673
        self.db_alias = db_alias
2✔
1674
        self.collection_name = collection_name
2✔
1675
        self.newfile = None  # Used for partial writes
2✔
1676
        self.gridout = None
2✔
1677

1678
    def __getattr__(self, name):
2✔
1679
        attrs = (
2✔
1680
            "_fs",
1681
            "grid_id",
1682
            "key",
1683
            "instance",
1684
            "db_alias",
1685
            "collection_name",
1686
            "newfile",
1687
            "gridout",
1688
        )
1689
        if name in attrs:
2✔
1690
            return self.__getattribute__(name)
×
1691
        obj = self.get()
2✔
1692
        if hasattr(obj, name):
2✔
1693
            return getattr(obj, name)
2✔
1694
        raise AttributeError
2✔
1695

1696
    def __get__(self, instance, value):
2✔
1697
        return self
×
1698

1699
    def __bool__(self):
2✔
1700
        return bool(self.grid_id)
2✔
1701

1702
    def __getstate__(self):
2✔
1703
        self_dict = self.__dict__
×
1704
        self_dict["_fs"] = None
×
1705
        return self_dict
×
1706

1707
    def __copy__(self):
2✔
1708
        copied = GridFSProxy()
×
1709
        copied.__dict__.update(self.__getstate__())
×
1710
        return copied
×
1711

1712
    def __deepcopy__(self, memo):
2✔
1713
        return self.__copy__()
×
1714

1715
    def __repr__(self):
2✔
1716
        return f"<{self.__class__.__name__}: {self.grid_id}>"
×
1717

1718
    def __str__(self):
2✔
1719
        gridout = self.get()
2✔
1720
        filename = gridout.filename if gridout else "<no file>"
2✔
1721
        return f"<{self.__class__.__name__}: {filename} ({self.grid_id})>"
2✔
1722

1723
    def __eq__(self, other):
2✔
1724
        if isinstance(other, GridFSProxy):
2✔
1725
            return (
2✔
1726
                (self.grid_id == other.grid_id)
1727
                and (self.collection_name == other.collection_name)
1728
                and (self.db_alias == other.db_alias)
1729
            )
1730
        else:
1731
            return False
2✔
1732

1733
    def __ne__(self, other):
2✔
1734
        return not self == other
×
1735

1736
    @property
2✔
1737
    def fs(self):
2✔
1738
        if not self._fs:
2✔
1739
            self._fs = gridfs.GridFS(get_db(self.db_alias), self.collection_name)
2✔
1740
        return self._fs
2✔
1741

1742
    def get(self, grid_id=None):
2✔
1743
        if grid_id:
2✔
1744
            self.grid_id = grid_id
×
1745

1746
        if self.grid_id is None:
2✔
1747
            return None
2✔
1748

1749
        try:
2✔
1750
            if self.gridout is None:
2✔
1751
                self.gridout = self.fs.get(self.grid_id, session=_get_session())
2✔
1752
            return self.gridout
2✔
1753
        except Exception:
×
1754
            # File has been deleted
1755
            return None
×
1756

1757
    def new_file(self, **kwargs):
2✔
1758
        self.newfile = self.fs.new_file(**kwargs)
2✔
1759
        self.grid_id = self.newfile._id
2✔
1760
        self._mark_as_changed()
2✔
1761

1762
    def put(self, file_obj, **kwargs):
2✔
1763
        if self.grid_id:
2✔
1764
            raise GridFSError(
×
1765
                "This document already has a file. Either delete "
1766
                "it or call replace to overwrite it"
1767
            )
1768
        self.grid_id = self.fs.put(file_obj, **kwargs)
2✔
1769
        self._mark_as_changed()
2✔
1770

1771
    def write(self, string):
2✔
1772
        if self.grid_id:
2✔
1773
            if not self.newfile:
2✔
1774
                raise GridFSError(
×
1775
                    "This document already has a file. Either "
1776
                    "delete it or call replace to overwrite it"
1777
                )
1778
        else:
1779
            self.new_file()
×
1780
        self.newfile.write(string)
2✔
1781

1782
    def writelines(self, lines):
2✔
1783
        if not self.newfile:
×
1784
            self.new_file()
×
1785
            self.grid_id = self.newfile._id
×
1786
        self.newfile.writelines(lines)
×
1787

1788
    def read(self, size=-1):
2✔
1789
        gridout = self.get()
2✔
1790
        if gridout is None:
2✔
1791
            return None
2✔
1792
        else:
1793
            try:
2✔
1794
                return gridout.read(size)
2✔
1795
            except Exception:
×
1796
                return ""
×
1797

1798
    def delete(self):
2✔
1799
        # Delete file from GridFS, FileField still remains
1800
        self.fs.delete(self.grid_id, session=_get_session())
2✔
1801
        self.grid_id = None
2✔
1802
        self.gridout = None
2✔
1803
        self._mark_as_changed()
2✔
1804

1805
    def replace(self, file_obj, **kwargs):
2✔
1806
        self.delete()
2✔
1807
        self.put(file_obj, **kwargs)
2✔
1808

1809
    def close(self):
2✔
1810
        if self.newfile:
2✔
1811
            self.newfile.close()
2✔
1812

1813
    def _mark_as_changed(self):
2✔
1814
        """Inform the instance that `self.key` has been changed"""
1815
        if self.instance:
2✔
1816
            self.instance._mark_as_changed(self.key)
2✔
1817

1818

1819
class FileField(BaseField):
2✔
1820
    """A GridFS storage field."""
1821

1822
    proxy_class = GridFSProxy
2✔
1823

1824
    def __init__(
2✔
1825
        self, db_alias=DEFAULT_CONNECTION_NAME, collection_name="fs", **kwargs
1826
    ):
1827
        super().__init__(**kwargs)
2✔
1828
        self.collection_name = collection_name
2✔
1829
        self.db_alias = db_alias
2✔
1830

1831
    def __get__(self, instance, owner):
2✔
1832
        if instance is None:
2✔
1833
            return self
×
1834

1835
        # Check if a file already exists for this model
1836
        grid_file = instance._data.get(self.name)
2✔
1837
        if not isinstance(grid_file, self.proxy_class):
2✔
1838
            grid_file = self.get_proxy_obj(key=self.name, instance=instance)
2✔
1839
            instance._data[self.name] = grid_file
2✔
1840

1841
        if not grid_file.key:
2✔
1842
            grid_file.key = self.name
2✔
1843
            grid_file.instance = instance
2✔
1844
        return grid_file
2✔
1845

1846
    def __set__(self, instance, value):
2✔
1847
        key = self.name
2✔
1848
        if (
2✔
1849
            hasattr(value, "read") and not isinstance(value, GridFSProxy)
1850
        ) or isinstance(value, (bytes, str)):
1851
            # using "FileField() = file/string" notation
1852
            grid_file = instance._data.get(self.name)
2✔
1853
            # If a file already exists, delete it
1854
            if grid_file:
2✔
1855
                try:
2✔
1856
                    grid_file.delete()
2✔
1857
                except Exception:
×
1858
                    pass
×
1859

1860
            # Create a new proxy object as we don't already have one
1861
            instance._data[key] = self.get_proxy_obj(key=key, instance=instance)
2✔
1862
            instance._data[key].put(value)
2✔
1863
        else:
1864
            instance._data[key] = value
2✔
1865

1866
        instance._mark_as_changed(key)
2✔
1867

1868
    def get_proxy_obj(self, key, instance, db_alias=None, collection_name=None):
2✔
1869
        if db_alias is None:
2✔
1870
            db_alias = self.db_alias
2✔
1871
        if collection_name is None:
2✔
1872
            collection_name = self.collection_name
2✔
1873

1874
        return self.proxy_class(
2✔
1875
            key=key,
1876
            instance=instance,
1877
            db_alias=db_alias,
1878
            collection_name=collection_name,
1879
        )
1880

1881
    def to_mongo(self, value):
2✔
1882
        # Store the GridFS file id in MongoDB
1883
        if isinstance(value, self.proxy_class) and value.grid_id is not None:
2✔
1884
            return value.grid_id
2✔
1885
        return None
2✔
1886

1887
    def to_python(self, value):
2✔
1888
        if value is not None:
2✔
1889
            return self.proxy_class(
2✔
1890
                value, collection_name=self.collection_name, db_alias=self.db_alias
1891
            )
1892

1893
    def validate(self, value):
2✔
1894
        if value.grid_id is not None:
2✔
1895
            if not isinstance(value, self.proxy_class):
2✔
1896
                self.error("FileField only accepts GridFSProxy values")
×
1897
            if not isinstance(value.grid_id, ObjectId):
2✔
1898
                self.error("Invalid GridFSProxy value")
×
1899

1900

1901
class ImageGridFsProxy(GridFSProxy):
2✔
1902
    """Proxy for ImageField"""
1903

1904
    def put(self, file_obj, **kwargs):
2✔
1905
        """
1906
        Insert a image in database
1907
        applying field properties (size, thumbnail_size)
1908
        """
1909
        field = self.instance._fields[self.key]
2✔
1910
        # Handle nested fields
1911
        if hasattr(field, "field") and isinstance(field.field, FileField):
2✔
1912
            field = field.field
×
1913

1914
        try:
2✔
1915
            img = Image.open(file_obj)
2✔
1916
            img_format = img.format
2✔
1917
        except Exception as e:
2✔
1918
            raise ValidationError("Invalid image: %s" % e)
2✔
1919

1920
        # Progressive JPEG
1921
        # TODO: fixme, at least unused, at worst bad implementation
1922
        progressive = img.info.get("progressive") or False
2✔
1923

1924
        if (
2✔
1925
            kwargs.get("progressive")
1926
            and isinstance(kwargs.get("progressive"), bool)
1927
            and img_format == "JPEG"
1928
        ):
1929
            progressive = True
×
1930
        else:
1931
            progressive = False
2✔
1932

1933
        if field.size and (
2✔
1934
            img.size[0] > field.size["width"] or img.size[1] > field.size["height"]
1935
        ):
1936
            size = field.size
2✔
1937

1938
            if size["force"]:
2✔
1939
                img = ImageOps.fit(img, (size["width"], size["height"]), LANCZOS)
2✔
1940
            else:
1941
                img.thumbnail((size["width"], size["height"]), LANCZOS)
×
1942

1943
        thumbnail = None
2✔
1944
        if field.thumbnail_size:
2✔
1945
            size = field.thumbnail_size
2✔
1946

1947
            if size["force"]:
2✔
1948
                thumbnail = ImageOps.fit(img, (size["width"], size["height"]), LANCZOS)
2✔
1949
            else:
1950
                thumbnail = img.copy()
×
1951
                thumbnail.thumbnail((size["width"], size["height"]), LANCZOS)
×
1952

1953
        if thumbnail:
2✔
1954
            thumb_id = self._put_thumbnail(thumbnail, img_format, progressive)
2✔
1955
        else:
1956
            thumb_id = None
2✔
1957

1958
        w, h = img.size
2✔
1959

1960
        io = BytesIO()
2✔
1961
        img.save(io, img_format, progressive=progressive)
2✔
1962
        io.seek(0)
2✔
1963

1964
        return super().put(
2✔
1965
            io, width=w, height=h, format=img_format, thumbnail_id=thumb_id, **kwargs
1966
        )
1967

1968
    def delete(self, *args, **kwargs):
2✔
1969
        # deletes thumbnail
1970
        out = self.get()
2✔
1971
        if out and out.thumbnail_id:
2✔
1972
            self.fs.delete(out.thumbnail_id, session=_get_session())
2✔
1973

1974
        return super().delete()
2✔
1975

1976
    def _put_thumbnail(self, thumbnail, format, progressive, **kwargs):
2✔
1977
        w, h = thumbnail.size
2✔
1978

1979
        io = BytesIO()
2✔
1980
        thumbnail.save(io, format, progressive=progressive)
2✔
1981
        io.seek(0)
2✔
1982

1983
        return self.fs.put(io, width=w, height=h, format=format, **kwargs)
2✔
1984

1985
    @property
2✔
1986
    def size(self):
2✔
1987
        """
1988
        return a width, height of image
1989
        """
1990
        out = self.get()
2✔
1991
        if out:
2✔
1992
            return out.width, out.height
2✔
1993

1994
    @property
2✔
1995
    def format(self):
2✔
1996
        """
1997
        return format of image
1998
        ex: PNG, JPEG, GIF, etc
1999
        """
2000
        out = self.get()
2✔
2001
        if out:
2✔
2002
            return out.format
2✔
2003

2004
    @property
2✔
2005
    def thumbnail(self):
2✔
2006
        """
2007
        return a gridfs.grid_file.GridOut
2008
        representing a thumbnail of Image
2009
        """
2010
        out = self.get()
2✔
2011
        if out and out.thumbnail_id:
2✔
2012
            return self.fs.get(out.thumbnail_id, session=_get_session())
2✔
2013

2014
    def write(self, *args, **kwargs):
2✔
2015
        raise RuntimeError('Please use "put" method instead')
×
2016

2017
    def writelines(self, *args, **kwargs):
2✔
2018
        raise RuntimeError('Please use "put" method instead')
×
2019

2020

2021
class ImproperlyConfigured(Exception):
2✔
2022
    pass
2✔
2023

2024

2025
class ImageField(FileField):
2✔
2026
    """
2027
    A Image File storage field.
2028

2029
    :param size: max size to store images, provided as (width, height, force)
2030
        if larger, it will be automatically resized (ex: size=(800, 600, True))
2031
    :param thumbnail_size: size to generate a thumbnail, provided as (width, height, force)
2032
    """
2033

2034
    proxy_class = ImageGridFsProxy
2✔
2035

2036
    def __init__(
2✔
2037
        self, size=None, thumbnail_size=None, collection_name="images", **kwargs
2038
    ):
2039
        if not Image:
2✔
2040
            raise ImproperlyConfigured("PIL library was not found")
×
2041

2042
        params_size = ("width", "height", "force")
2✔
2043
        extra_args = {"size": size, "thumbnail_size": thumbnail_size}
2✔
2044
        for att_name, att in extra_args.items():
2✔
2045
            value = None
2✔
2046
            if isinstance(att, (tuple, list)):
2✔
2047
                value = dict(itertools.zip_longest(params_size, att, fillvalue=None))
2✔
2048

2049
            setattr(self, att_name, value)
2✔
2050

2051
        super().__init__(collection_name=collection_name, **kwargs)
2✔
2052

2053

2054
class SequenceField(BaseField):
2✔
2055
    """Provides a sequential counter see:
2056
     https://www.mongodb.com/docs/manual/reference/method/ObjectId/#ObjectIDs-SequenceNumbers
2057

2058
    .. note::
2059

2060
             Although traditional databases often use increasing sequence
2061
             numbers for primary keys. In MongoDB, the preferred approach is to
2062
             use Object IDs instead.  The concept is that in a very large
2063
             cluster of machines, it is easier to create an object ID than have
2064
             global, uniformly increasing sequence numbers.
2065

2066
    :param collection_name:  Name of the counter collection (default 'mongoengine.counters')
2067
    :param sequence_name: Name of the sequence in the collection (default 'ClassName.counter')
2068
    :param value_decorator: Any callable to use as a counter (default int)
2069

2070
    Use any callable as `value_decorator` to transform calculated counter into
2071
    any value suitable for your needs, e.g. string or hexadecimal
2072
    representation of the default integer counter value.
2073

2074
    .. note::
2075

2076
        In case the counter is defined in the abstract document, it will be
2077
        common to all inherited documents and the default sequence name will
2078
        be the class name of the abstract document.
2079
    """
2080

2081
    _auto_gen = True
2✔
2082
    COLLECTION_NAME = "mongoengine.counters"
2✔
2083
    VALUE_DECORATOR = int
2✔
2084

2085
    def __init__(
2✔
2086
        self,
2087
        collection_name=None,
2088
        db_alias=None,
2089
        sequence_name=None,
2090
        value_decorator=None,
2091
        *args,
2092
        **kwargs,
2093
    ):
2094
        self.collection_name = collection_name or self.COLLECTION_NAME
2✔
2095
        self.db_alias = db_alias or DEFAULT_CONNECTION_NAME
2✔
2096
        self.sequence_name = sequence_name
2✔
2097
        self.value_decorator = (
2✔
2098
            value_decorator if callable(value_decorator) else self.VALUE_DECORATOR
2099
        )
2100
        super().__init__(*args, **kwargs)
2✔
2101

2102
    def generate(self):
2✔
2103
        """
2104
        Generate and Increment the counter
2105
        """
2106
        sequence_name = self.get_sequence_name()
2✔
2107
        sequence_id = f"{sequence_name}.{self.name}"
2✔
2108
        collection = get_db(alias=self.db_alias)[self.collection_name]
2✔
2109

2110
        counter = collection.find_one_and_update(
2✔
2111
            filter={"_id": sequence_id},
2112
            update={"$inc": {"next": 1}},
2113
            return_document=ReturnDocument.AFTER,
2114
            upsert=True,
2115
            session=_get_session(),
2116
        )
2117
        return self.value_decorator(counter["next"])
2✔
2118

2119
    def set_next_value(self, value):
2✔
2120
        """Helper method to set the next sequence value"""
2121
        sequence_name = self.get_sequence_name()
2✔
2122
        sequence_id = f"{sequence_name}.{self.name}"
2✔
2123
        collection = get_db(alias=self.db_alias)[self.collection_name]
2✔
2124
        counter = collection.find_one_and_update(
2✔
2125
            filter={"_id": sequence_id},
2126
            update={"$set": {"next": value}},
2127
            return_document=ReturnDocument.AFTER,
2128
            upsert=True,
2129
            session=_get_session(),
2130
        )
2131
        return self.value_decorator(counter["next"])
2✔
2132

2133
    def get_next_value(self):
2✔
2134
        """Helper method to get the next value for previewing.
2135

2136
        .. warning:: There is no guarantee this will be the next value
2137
        as it is only fixed on set.
2138
        """
2139
        sequence_name = self.get_sequence_name()
2✔
2140
        sequence_id = f"{sequence_name}.{self.name}"
2✔
2141
        collection = get_db(alias=self.db_alias)[self.collection_name]
2✔
2142
        data = collection.find_one({"_id": sequence_id}, session=_get_session())
2✔
2143

2144
        if data:
2✔
2145
            return self.value_decorator(data["next"] + 1)
2✔
2146

2147
        return self.value_decorator(1)
2✔
2148

2149
    def get_sequence_name(self):
2✔
2150
        if self.sequence_name:
2✔
2151
            return self.sequence_name
2✔
2152
        owner = self.owner_document
2✔
2153
        if issubclass(owner, Document) and not owner._meta.get("abstract"):
2✔
2154
            return owner._get_collection_name()
2✔
2155
        else:
2156
            return (
2✔
2157
                "".join("_%s" % c if c.isupper() else c for c in owner._class_name)
2158
                .strip("_")
2159
                .lower()
2160
            )
2161

2162
    def __get__(self, instance, owner):
2✔
2163
        value = super().__get__(instance, owner)
2✔
2164
        if value is None and instance._initialised:
2✔
2165
            value = self.generate()
2✔
2166
            instance._data[self.name] = value
2✔
2167
            instance._mark_as_changed(self.name)
2✔
2168

2169
        return value
2✔
2170

2171
    def __set__(self, instance, value):
2✔
2172
        if value is None and instance._initialised:
2✔
2173
            value = self.generate()
2✔
2174

2175
        return super().__set__(instance, value)
2✔
2176

2177
    def prepare_query_value(self, op, value):
2✔
2178
        """
2179
        This method is overridden in order to convert the query value into to required
2180
        type. We need to do this in order to be able to successfully compare query
2181
        values passed as string, the base implementation returns the value as is.
2182
        """
2183
        return self.value_decorator(value)
2✔
2184

2185
    def to_python(self, value):
2✔
2186
        if value is None:
2✔
2187
            value = self.generate()
×
2188
        return value
2✔
2189

2190

2191
class UUIDField(BaseField):
2✔
2192
    """A UUID field."""
2193

2194
    _binary = None
2✔
2195

2196
    def __init__(self, binary=True, **kwargs):
2✔
2197
        """
2198
        Store UUID data in the database
2199

2200
        :param binary: if False store as a string.
2201
        """
2202
        self._binary = binary
2✔
2203
        super().__init__(**kwargs)
2✔
2204

2205
    def to_python(self, value):
2✔
2206
        if not self._binary:
2✔
2207
            original_value = value
2✔
2208
            try:
2✔
2209
                if not isinstance(value, str):
2✔
2210
                    value = str(value)
2✔
2211
                return uuid.UUID(value)
2✔
2212
            except (ValueError, TypeError, AttributeError):
×
2213
                return original_value
×
2214
        return value
2✔
2215

2216
    def to_mongo(self, value):
2✔
2217
        if not self._binary:
2✔
2218
            return str(value)
2✔
2219
        elif isinstance(value, str):
2✔
2220
            return uuid.UUID(value)
×
2221
        return value
2✔
2222

2223
    def prepare_query_value(self, op, value):
2✔
2224
        if value is None:
2✔
2225
            return None
×
2226
        return self.to_mongo(value)
2✔
2227

2228
    def validate(self, value):
2✔
2229
        if not isinstance(value, uuid.UUID):
2✔
2230
            if not isinstance(value, str):
2✔
2231
                value = str(value)
×
2232
            try:
2✔
2233
                uuid.UUID(value)
2✔
2234
            except (ValueError, TypeError, AttributeError) as exc:
2✔
2235
                self.error("Could not convert to UUID: %s" % exc)
2✔
2236

2237

2238
class GeoPointField(BaseField):
2✔
2239
    """A list storing a longitude and latitude coordinate.
2240

2241
    .. note:: this represents a generic point in a 2D plane and a legacy way of
2242
        representing a geo point. It admits 2d indexes but not "2dsphere" indexes
2243
        in MongoDB > 2.4 which are more natural for modeling geospatial points.
2244
        See :ref:`geospatial-indexes`
2245
    """
2246

2247
    _geo_index = pymongo.GEO2D
2✔
2248

2249
    def validate(self, value):
2✔
2250
        """Make sure that a geo-value is of type (x, y)"""
2251
        if not isinstance(value, (list, tuple)):
2✔
2252
            self.error("GeoPointField can only accept tuples or lists of (x, y)")
2✔
2253

2254
        if not len(value) == 2:
2✔
2255
            self.error("Value (%s) must be a two-dimensional point" % repr(value))
2✔
2256
        elif not isinstance(value[0], (float, int)) or not isinstance(
2✔
2257
            value[1], (float, int)
2258
        ):
2259
            self.error("Both values (%s) in point must be float or int" % repr(value))
2✔
2260

2261

2262
class PointField(GeoJsonBaseField):
2✔
2263
    """A GeoJSON field storing a longitude and latitude coordinate.
2264

2265
    The data is represented as:
2266

2267
    .. code-block:: js
2268

2269
        {'type' : 'Point' ,
2270
         'coordinates' : [x, y]}
2271

2272
    You can either pass a dict with the full information or a list
2273
    to set the value.
2274

2275
    Requires mongodb >= 2.4
2276
    """
2277

2278
    _type = "Point"
2✔
2279

2280

2281
class LineStringField(GeoJsonBaseField):
2✔
2282
    """A GeoJSON field storing a line of longitude and latitude coordinates.
2283

2284
    The data is represented as:
2285

2286
    .. code-block:: js
2287

2288
        {'type' : 'LineString' ,
2289
         'coordinates' : [[x1, y1], [x2, y2] ... [xn, yn]]}
2290

2291
    You can either pass a dict with the full information or a list of points.
2292

2293
    Requires mongodb >= 2.4
2294
    """
2295

2296
    _type = "LineString"
2✔
2297

2298

2299
class PolygonField(GeoJsonBaseField):
2✔
2300
    """A GeoJSON field storing a polygon of longitude and latitude coordinates.
2301

2302
    The data is represented as:
2303

2304
    .. code-block:: js
2305

2306
        {'type' : 'Polygon' ,
2307
         'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]],
2308
                          [[x1, y1], [x1, y1] ... [xn, yn]]}
2309

2310
    You can either pass a dict with the full information or a list
2311
    of LineStrings. The first LineString being the outside and the rest being
2312
    holes.
2313

2314
    Requires mongodb >= 2.4
2315
    """
2316

2317
    _type = "Polygon"
2✔
2318

2319

2320
class MultiPointField(GeoJsonBaseField):
2✔
2321
    """A GeoJSON field storing a list of Points.
2322

2323
    The data is represented as:
2324

2325
    .. code-block:: js
2326

2327
        {'type' : 'MultiPoint' ,
2328
         'coordinates' : [[x1, y1], [x2, y2]]}
2329

2330
    You can either pass a dict with the full information or a list
2331
    to set the value.
2332

2333
    Requires mongodb >= 2.6
2334
    """
2335

2336
    _type = "MultiPoint"
2✔
2337

2338

2339
class MultiLineStringField(GeoJsonBaseField):
2✔
2340
    """A GeoJSON field storing a list of LineStrings.
2341

2342
    The data is represented as:
2343

2344
    .. code-block:: js
2345

2346
        {'type' : 'MultiLineString' ,
2347
         'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]],
2348
                          [[x1, y1], [x1, y1] ... [xn, yn]]]}
2349

2350
    You can either pass a dict with the full information or a list of points.
2351

2352
    Requires mongodb >= 2.6
2353
    """
2354

2355
    _type = "MultiLineString"
2✔
2356

2357

2358
class MultiPolygonField(GeoJsonBaseField):
2✔
2359
    """A GeoJSON field storing  list of Polygons.
2360

2361
    The data is represented as:
2362

2363
    .. code-block:: js
2364

2365
        {'type' : 'MultiPolygon' ,
2366
         'coordinates' : [[
2367
               [[x1, y1], [x1, y1] ... [xn, yn]],
2368
               [[x1, y1], [x1, y1] ... [xn, yn]]
2369
           ], [
2370
               [[x1, y1], [x1, y1] ... [xn, yn]],
2371
               [[x1, y1], [x1, y1] ... [xn, yn]]
2372
           ]
2373
        }
2374

2375
    You can either pass a dict with the full information or a list
2376
    of Polygons.
2377

2378
    Requires mongodb >= 2.6
2379
    """
2380

2381
    _type = "MultiPolygon"
2✔
2382

2383

2384
class LazyReferenceField(BaseField):
2✔
2385
    """A really lazy reference to a document.
2386
    Unlike the :class:`~mongoengine.fields.ReferenceField` it will
2387
    **not** be automatically (lazily) dereferenced on access.
2388
    Instead, access will return a :class:`~mongoengine.base.LazyReference` class
2389
    instance, allowing access to `pk` or manual dereference by using
2390
    ``fetch()`` method.
2391
    """
2392

2393
    def __init__(
2✔
2394
        self,
2395
        document_type,
2396
        passthrough=False,
2397
        dbref=False,
2398
        reverse_delete_rule=DO_NOTHING,
2399
        **kwargs,
2400
    ):
2401
        """Initialises the Reference Field.
2402

2403
        :param dbref:  Store the reference as :class:`~pymongo.dbref.DBRef`
2404
          or as the :class:`~pymongo.objectid.ObjectId`.id .
2405
        :param reverse_delete_rule: Determines what to do when the referring
2406
          object is deleted
2407
        :param passthrough: When trying to access unknown fields, the
2408
          :class:`~mongoengine.base.datastructure.LazyReference` instance will
2409
          automatically call `fetch()` and try to retrieve the field on the fetched
2410
          document. Note this only work getting field (not setting or deleting).
2411
        """
2412
        # XXX ValidationError raised outside of the "validate" method.
2413
        if not isinstance(document_type, str) and not issubclass(
2✔
2414
            document_type, Document
2415
        ):
2416
            self.error(
2✔
2417
                "Argument to LazyReferenceField constructor must be a "
2418
                "document class or a string"
2419
            )
2420

2421
        self.dbref = dbref
2✔
2422
        self.passthrough = passthrough
2✔
2423
        self.document_type_obj = document_type
2✔
2424
        self.reverse_delete_rule = reverse_delete_rule
2✔
2425
        super().__init__(**kwargs)
2✔
2426

2427
    @property
2✔
2428
    def document_type(self):
2✔
2429
        if isinstance(self.document_type_obj, str):
2✔
2430
            if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
2✔
2431
                self.document_type_obj = self.owner_document
×
2432
            else:
2433
                self.document_type_obj = _DocumentRegistry.get(self.document_type_obj)
2✔
2434
        return self.document_type_obj
2✔
2435

2436
    def build_lazyref(self, value):
2✔
2437
        if isinstance(value, LazyReference):
2✔
2438
            if value.passthrough != self.passthrough:
2✔
2439
                value = LazyReference(
×
2440
                    value.document_type, value.pk, passthrough=self.passthrough
2441
                )
2442
        elif value is not None:
2✔
2443
            if isinstance(value, self.document_type):
2✔
2444
                value = LazyReference(
2✔
2445
                    self.document_type, value.pk, passthrough=self.passthrough
2446
                )
2447
            elif isinstance(value, DBRef):
2✔
2448
                value = LazyReference(
2✔
2449
                    self.document_type, value.id, passthrough=self.passthrough
2450
                )
2451
            else:
2452
                # value is the primary key of the referenced document
2453
                value = LazyReference(
2✔
2454
                    self.document_type, value, passthrough=self.passthrough
2455
                )
2456
        return value
2✔
2457

2458
    def __get__(self, instance, owner):
2✔
2459
        """Descriptor to allow lazy dereferencing."""
2460
        if instance is None:
2✔
2461
            # Document class being used rather than a document object
2462
            return self
×
2463

2464
        value = self.build_lazyref(instance._data.get(self.name))
2✔
2465
        if value:
2✔
2466
            instance._data[self.name] = value
2✔
2467

2468
        return super().__get__(instance, owner)
2✔
2469

2470
    def to_mongo(self, value):
2✔
2471
        if isinstance(value, LazyReference):
2✔
2472
            pk = value.pk
2✔
2473
        elif isinstance(value, self.document_type):
2✔
2474
            pk = value.pk
2✔
2475
        elif isinstance(value, DBRef):
2✔
2476
            pk = value.id
2✔
2477
        else:
2478
            # value is the primary key of the referenced document
2479
            pk = value
×
2480
        id_field_name = self.document_type._meta["id_field"]
2✔
2481
        id_field = self.document_type._fields[id_field_name]
2✔
2482
        pk = id_field.to_mongo(pk)
2✔
2483
        if self.dbref:
2✔
2484
            return DBRef(self.document_type._get_collection_name(), pk)
2✔
2485
        else:
2486
            return pk
2✔
2487

2488
    def to_python(self, value):
2✔
2489
        """Convert a MongoDB-compatible type to a Python type."""
2490
        if not isinstance(value, (DBRef, Document, EmbeddedDocument)):
2✔
2491
            collection = self.document_type._get_collection_name()
2✔
2492
            value = DBRef(collection, self.document_type.id.to_python(value))
2✔
2493
            value = self.build_lazyref(value)
2✔
2494
        return value
2✔
2495

2496
    def validate(self, value):
2✔
2497
        if isinstance(value, LazyReference):
2✔
2498
            if value.collection != self.document_type._get_collection_name():
2✔
2499
                self.error("Reference must be on a `%s` document." % self.document_type)
2✔
2500
            pk = value.pk
2✔
2501
        elif isinstance(value, self.document_type):
2✔
2502
            pk = value.pk
2✔
2503
        elif isinstance(value, DBRef):
2✔
2504
            # TODO: check collection ?
2505
            collection = self.document_type._get_collection_name()
2✔
2506
            if value.collection != collection:
2✔
2507
                self.error("DBRef on bad collection (must be on `%s`)" % collection)
2✔
2508
            pk = value.id
2✔
2509
        else:
2510
            # value is the primary key of the referenced document
2511
            id_field_name = self.document_type._meta["id_field"]
2✔
2512
            id_field = getattr(self.document_type, id_field_name)
2✔
2513
            pk = value
2✔
2514
            try:
2✔
2515
                id_field.validate(pk)
2✔
2516
            except ValidationError:
2✔
2517
                self.error(
2✔
2518
                    "value should be `{0}` document, LazyReference or DBRef on `{0}` "
2519
                    "or `{0}`'s primary key (i.e. `{1}`)".format(
2520
                        self.document_type.__name__, type(id_field).__name__
2521
                    )
2522
                )
2523

2524
        if pk is None:
2✔
2525
            self.error(_unsaved_object_error(self.document_type.__name__))
2✔
2526

2527
    def prepare_query_value(self, op, value):
2✔
2528
        if value is None:
2✔
2529
            return None
×
2530
        super().prepare_query_value(op, value)
2✔
2531
        return self.to_mongo(value)
2✔
2532

2533
    def lookup_member(self, member_name):
2✔
2534
        return self.document_type._fields.get(member_name)
×
2535

2536

2537
class GenericLazyReferenceField(GenericReferenceField):
2✔
2538
    """A reference to *any* :class:`~mongoengine.document.Document` subclass.
2539
    Unlike the :class:`~mongoengine.fields.GenericReferenceField` it will
2540
    **not** be automatically (lazily) dereferenced on access.
2541
    Instead, access will return a :class:`~mongoengine.base.LazyReference` class
2542
    instance, allowing access to `pk` or manual dereference by using
2543
    ``fetch()`` method.
2544

2545
    .. note ::
2546
        * Any documents used as a generic reference must be registered in the
2547
          document registry.  Importing the model will automatically register
2548
          it.
2549

2550
        * You can use the choices param to limit the acceptable Document types
2551
    """
2552

2553
    def __init__(self, *args, **kwargs):
2✔
2554
        self.passthrough = kwargs.pop("passthrough", False)
2✔
2555
        super().__init__(*args, **kwargs)
2✔
2556

2557
    def _validate_choices(self, value):
2✔
2558
        if isinstance(value, LazyReference):
2✔
2559
            value = value.document_type._class_name
2✔
2560
        super()._validate_choices(value)
2✔
2561

2562
    def build_lazyref(self, value):
2✔
2563
        if isinstance(value, LazyReference):
2✔
2564
            if value.passthrough != self.passthrough:
2✔
2565
                value = LazyReference(
×
2566
                    value.document_type, value.pk, passthrough=self.passthrough
2567
                )
2568
        elif value is not None:
2✔
2569
            if isinstance(value, (dict, SON)):
2✔
2570
                value = LazyReference(
2✔
2571
                    _DocumentRegistry.get(value["_cls"]),
2572
                    value["_ref"].id,
2573
                    passthrough=self.passthrough,
2574
                )
2575
            elif isinstance(value, Document):
2✔
2576
                value = LazyReference(
2✔
2577
                    type(value), value.pk, passthrough=self.passthrough
2578
                )
2579
        return value
2✔
2580

2581
    def __get__(self, instance, owner):
2✔
2582
        if instance is None:
2✔
2583
            return self
×
2584

2585
        value = self.build_lazyref(instance._data.get(self.name))
2✔
2586
        if value:
2✔
2587
            instance._data[self.name] = value
2✔
2588

2589
        return super().__get__(instance, owner)
2✔
2590

2591
    def validate(self, value):
2✔
2592
        if isinstance(value, LazyReference) and value.pk is None:
2✔
2593
            self.error(
×
2594
                _unsaved_object_error(
2595
                    self.__class__.__name__
2596
                )  # Actual class is difficult to predict here
2597
            )
2598
        return super().validate(value)
2✔
2599

2600
    def to_mongo(self, document):
2✔
2601
        if document is None:
2✔
2602
            return None
×
2603

2604
        if isinstance(document, LazyReference):
2✔
2605
            return SON(
2✔
2606
                (
2607
                    ("_cls", document.document_type._class_name),
2608
                    (
2609
                        "_ref",
2610
                        DBRef(
2611
                            document.document_type._get_collection_name(), document.pk
2612
                        ),
2613
                    ),
2614
                )
2615
            )
2616
        else:
2617
            return super().to_mongo(document)
2✔
2618

2619

2620
class Decimal128Field(BaseField):
2✔
2621
    """
2622
    128-bit decimal-based floating-point field capable of emulating decimal
2623
    rounding with exact precision. This field will expose decimal.Decimal but stores the value as a
2624
    `bson.Decimal128` behind the scene, this field is intended for monetary data, scientific computations, etc.
2625
    """
2626

2627
    DECIMAL_CONTEXT = create_decimal128_context()
2✔
2628

2629
    def __init__(self, min_value=None, max_value=None, **kwargs):
2✔
2630
        self.min_value = min_value
2✔
2631
        self.max_value = max_value
2✔
2632
        super().__init__(**kwargs)
2✔
2633

2634
    def to_mongo(self, value):
2✔
2635
        if value is None:
2✔
2636
            return None
2✔
2637
        if isinstance(value, Decimal128):
2✔
2638
            return value
2✔
2639
        if not isinstance(value, decimal.Decimal):
2✔
2640
            with decimal.localcontext(self.DECIMAL_CONTEXT) as ctx:
2✔
2641
                value = ctx.create_decimal(value)
2✔
2642
        return Decimal128(value)
2✔
2643

2644
    def to_python(self, value):
2✔
2645
        if value is None:
2✔
2646
            return None
×
2647
        return self.to_mongo(value).to_decimal()
2✔
2648

2649
    def validate(self, value):
2✔
2650
        if not isinstance(value, Decimal128):
2✔
2651
            try:
2✔
2652
                value = Decimal128(value)
2✔
2653
            except (TypeError, ValueError, decimal.InvalidOperation) as exc:
2✔
2654
                self.error("Could not convert value to Decimal128: %s" % exc)
2✔
2655

2656
        if self.min_value is not None and value.to_decimal() < self.min_value:
2✔
2657
            self.error("Decimal value is too small")
2✔
2658

2659
        if self.max_value is not None and value.to_decimal() > self.max_value:
2✔
2660
            self.error("Decimal value is too large")
2✔
2661

2662
    def prepare_query_value(self, op, value):
2✔
2663
        return super().prepare_query_value(op, self.to_mongo(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