• 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

95.19
/mongoengine/base/fields.py
1
import contextlib
2✔
2
import operator
2✔
3
import threading
2✔
4
import weakref
2✔
5

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

9
from mongoengine.base.common import UPDATE_OPERATORS
2✔
10
from mongoengine.base.datastructures import (
2✔
11
    BaseDict,
12
    BaseList,
13
    EmbeddedDocumentList,
14
)
15
from mongoengine.common import _import_class
2✔
16
from mongoengine.errors import DeprecatedError, ValidationError
2✔
17

18
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField")
2✔
19

20

21
@contextlib.contextmanager
2✔
22
def _no_dereference_for_fields(*fields):
2✔
23
    """Context manager for temporarily disabling a Field's auto-dereferencing
24
    (meant to be used from no_dereference context manager)"""
25
    try:
2✔
26
        for field in fields:
2✔
27
            field._incr_no_dereference_context()
2✔
28
        yield None
2✔
29
    finally:
30
        for field in fields:
2✔
31
            field._decr_no_dereference_context()
2✔
32

33

34
class BaseField:
2✔
35
    """A base class for fields in a MongoDB document. Instances of this class
36
    may be added to subclasses of `Document` to define a document's schema.
37
    """
38

39
    name = None  # set in TopLevelDocumentMetaclass
2✔
40
    _geo_index = False
2✔
41
    _auto_gen = False  # Call `generate` to generate a value
2✔
42
    _thread_local_storage = threading.local()
2✔
43

44
    # These track each time a Field instance is created. Used to retain order.
45
    # The auto_creation_counter is used for fields that MongoEngine implicitly
46
    # creates, creation_counter is used for all user-specified fields.
47
    creation_counter = 0
2✔
48
    auto_creation_counter = -1
2✔
49

50
    def __init__(
2✔
51
        self,
52
        db_field=None,
53
        required=False,
54
        default=None,
55
        unique=False,
56
        unique_with=None,
57
        primary_key=False,
58
        validation=None,
59
        choices=None,
60
        null=False,
61
        sparse=False,
62
        **kwargs,
63
    ):
64
        """
65
        :param db_field: The database field to store this field in
66
            (defaults to the name of the field)
67
        :param required: If the field is required. Whether it has to have a
68
            value or not. Defaults to False.
69
        :param default: (optional) The default value for this field if no value
70
            has been set, if the value is set to None or has been unset. It can be a
71
            callable.
72
        :param unique: Is the field value unique or not (Creates an index).  Defaults to False.
73
        :param unique_with: (optional) The other field this field should be
74
            unique with (Creates an index).
75
        :param primary_key: Mark this field as the primary key ((Creates an index)). Defaults to False.
76
        :param validation: (optional) A callable to validate the value of the
77
            field. The callable takes the value as parameter and should raise
78
            a ValidationError if validation fails
79
        :param choices: (optional) The valid choices
80
        :param null: (optional) If the field value can be null when a default exists. If not set, the default value
81
            will be used in case a field with a default value is set to None. Defaults to False.
82
        :param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
83
            means that uniqueness won't be enforced for `None` values (Creates an index). Defaults to False.
84
        :param **kwargs: (optional) Arbitrary indirection-free metadata for
85
            this field can be supplied as additional keyword arguments and
86
            accessed as attributes of the field. Must not conflict with any
87
            existing attributes. Common metadata includes `verbose_name` and
88
            `help_text`.
89
        """
90
        self.db_field = db_field if not primary_key else "_id"
2✔
91

92
        self.required = required or primary_key
2✔
93
        self.default = default
2✔
94
        self.unique = bool(unique or unique_with)
2✔
95
        self.unique_with = unique_with
2✔
96
        self.primary_key = primary_key
2✔
97
        self.validation = validation
2✔
98
        self.choices = choices
2✔
99
        self.null = null
2✔
100
        self.sparse = sparse
2✔
101
        self._owner_document = None
2✔
102

103
        self.__auto_dereference = True
2✔
104

105
        # Make sure db_field is a string (if it's explicitly defined).
106
        if self.db_field is not None and not isinstance(self.db_field, str):
2✔
107
            raise TypeError("db_field should be a string.")
×
108

109
        # Make sure db_field doesn't contain any forbidden characters.
110
        if isinstance(self.db_field, str) and (
2✔
111
            "." in self.db_field
112
            or "\0" in self.db_field
113
            or self.db_field.startswith("$")
114
        ):
115
            raise ValueError(
2✔
116
                'field names cannot contain dots (".") or null characters '
117
                '("\\0"), and they must not start with a dollar sign ("$").'
118
            )
119

120
        # Detect and report conflicts between metadata and base properties.
121
        conflicts = set(dir(self)) & set(kwargs)
2✔
122
        if conflicts:
2✔
123
            raise TypeError(
×
124
                "%s already has attribute(s): %s"
125
                % (self.__class__.__name__, ", ".join(conflicts))
126
            )
127

128
        # Assign metadata to the instance
129
        # This efficient method is available because no __slots__ are defined.
130
        self.__dict__.update(kwargs)
2✔
131

132
        # Adjust the appropriate creation counter, and save our local copy.
133
        if self.db_field == "_id":
2✔
134
            self.creation_counter = BaseField.auto_creation_counter
2✔
135
            BaseField.auto_creation_counter -= 1
2✔
136
        else:
137
            self.creation_counter = BaseField.creation_counter
2✔
138
            BaseField.creation_counter += 1
2✔
139

140
    def set_auto_dereferencing(self, value):
2✔
141
        self.__auto_dereference = value
2✔
142

143
    @property
2✔
144
    def _no_dereference_context_local(self):
2✔
145
        if not hasattr(self._thread_local_storage, "no_dereference_context"):
2✔
146
            self._thread_local_storage.no_dereference_context = 0
2✔
147
        return self._thread_local_storage.no_dereference_context
2✔
148

149
    @property
2✔
150
    def _no_dereference_context_is_set(self):
2✔
151
        return self._no_dereference_context_local > 0
2✔
152

153
    def _incr_no_dereference_context(self):
2✔
154
        self._thread_local_storage.no_dereference_context = (
2✔
155
            self._no_dereference_context_local + 1
156
        )
157

158
    def _decr_no_dereference_context(self):
2✔
159
        self._thread_local_storage.no_dereference_context = (
2✔
160
            self._no_dereference_context_local - 1
161
        )
162

163
    @property
2✔
164
    def _auto_dereference(self):
2✔
165
        return self.__auto_dereference and not self._no_dereference_context_is_set
2✔
166

167
    def __get__(self, instance, owner):
2✔
168
        """Descriptor for retrieving a value from a field in a document."""
169
        if instance is None:
2✔
170
            # Document class being used rather than a document object
171
            return self
2✔
172

173
        # Get value from document instance if available
174
        return instance._data.get(self.name)
2✔
175

176
    def __set__(self, instance, value):
2✔
177
        """Descriptor for assigning a value to a field in a document."""
178
        # If setting to None and there is a default value provided for this
179
        # field, then set the value to the default value.
180
        if value is None:
2✔
181
            if self.null:
2✔
182
                value = None
2✔
183
            elif self.default is not None:
2✔
184
                value = self.default
2✔
185
                if callable(value):
2✔
186
                    value = value()
2✔
187

188
        if instance._initialised:
2✔
189
            try:
2✔
190
                value_has_changed = (
2✔
191
                    self.name not in instance._data
192
                    or instance._data[self.name] != value
193
                )
194
                if value_has_changed:
2✔
195
                    instance._mark_as_changed(self.name)
2✔
196
            except Exception:
×
197
                # Some values can't be compared and throw an error when we
198
                # attempt to do so (e.g. tz-naive and tz-aware datetimes).
199
                # Mark the field as changed in such cases.
200
                instance._mark_as_changed(self.name)
×
201

202
        EmbeddedDocument = _import_class("EmbeddedDocument")
2✔
203
        if isinstance(value, EmbeddedDocument):
2✔
204
            value._instance = weakref.proxy(instance)
2✔
205
        elif isinstance(value, (list, tuple)):
2✔
206
            for v in value:
2✔
207
                if isinstance(v, EmbeddedDocument):
2✔
208
                    v._instance = weakref.proxy(instance)
2✔
209

210
        instance._data[self.name] = value
2✔
211

212
    def error(self, message="", errors=None, field_name=None):
2✔
213
        """Raise a ValidationError."""
214
        field_name = field_name if field_name else self.name
2✔
215
        raise ValidationError(message, errors=errors, field_name=field_name)
2✔
216

217
    def to_python(self, value):
2✔
218
        """Convert a MongoDB-compatible type to a Python type."""
219
        return value
2✔
220

221
    def to_mongo(self, value):
2✔
222
        """Convert a Python type to a MongoDB-compatible type."""
223
        return self.to_python(value)
2✔
224

225
    def _to_mongo_safe_call(self, value, use_db_field=True, fields=None):
2✔
226
        """Helper method to call to_mongo with proper inputs."""
227
        f_inputs = self.to_mongo.__code__.co_varnames
2✔
228
        ex_vars = {}
2✔
229
        if "fields" in f_inputs:
2✔
230
            ex_vars["fields"] = fields
2✔
231

232
        if "use_db_field" in f_inputs:
2✔
233
            ex_vars["use_db_field"] = use_db_field
2✔
234

235
        return self.to_mongo(value, **ex_vars)
2✔
236

237
    def prepare_query_value(self, op, value):
2✔
238
        """Prepare a value that is being used in a query for PyMongo."""
239
        if op in UPDATE_OPERATORS:
2✔
240
            self.validate(value)
2✔
241
        return value
2✔
242

243
    def validate(self, value, clean=True):
2✔
244
        """Perform validation on a value."""
245
        pass
2✔
246

247
    def _validate_choices(self, value):
2✔
248
        Document = _import_class("Document")
2✔
249
        EmbeddedDocument = _import_class("EmbeddedDocument")
2✔
250

251
        choice_list = self.choices
2✔
252
        if isinstance(next(iter(choice_list)), (list, tuple)):
2✔
253
            # next(iter) is useful for sets
254
            choice_list = [k for k, _ in choice_list]
2✔
255

256
        # Choices which are other types of Documents
257
        if isinstance(value, (Document, EmbeddedDocument)):
2✔
258
            if not any(isinstance(value, c) for c in choice_list):
2✔
259
                self.error("Value must be an instance of %s" % (choice_list))
2✔
260
        # Choices which are types other than Documents
261
        else:
262
            values = value if isinstance(value, (list, tuple)) else [value]
2✔
263
            if len(set(values) - set(choice_list)):
2✔
264
                self.error("Value must be one of %s" % str(choice_list))
2✔
265

266
    def _validate(self, value, **kwargs):
2✔
267
        # Check the Choices Constraint
268
        if self.choices:
2✔
269
            self._validate_choices(value)
2✔
270

271
        # check validation argument
272
        if self.validation is not None:
2✔
273
            if callable(self.validation):
2✔
274
                try:
2✔
275
                    # breaking change of 0.18
276
                    # Get rid of True/False-type return for the validation method
277
                    # in favor of having validation raising a ValidationError
278
                    ret = self.validation(value)
2✔
279
                    if ret is not None:
2✔
280
                        raise DeprecatedError(
2✔
281
                            "validation argument for `%s` must not return anything, "
282
                            "it should raise a ValidationError if validation fails"
283
                            % self.name
284
                        )
285
                except ValidationError as ex:
2✔
286
                    self.error(str(ex))
2✔
287
            else:
288
                raise ValueError(
×
289
                    'validation argument for `"%s"` must be a ' "callable." % self.name
290
                )
291

292
        self.validate(value, **kwargs)
2✔
293

294
    @property
2✔
295
    def owner_document(self):
2✔
296
        return self._owner_document
2✔
297

298
    def _set_owner_document(self, owner_document):
2✔
299
        self._owner_document = owner_document
2✔
300

301
    @owner_document.setter
2✔
302
    def owner_document(self, owner_document):
2✔
303
        self._set_owner_document(owner_document)
2✔
304

305

306
class ComplexBaseField(BaseField):
2✔
307
    """Handles complex fields, such as lists / dictionaries.
308

309
    Allows for nesting of embedded documents inside complex types.
310
    Handles the lazy dereferencing of a queryset by lazily dereferencing all
311
    items in a list / dict rather than one at a time.
312
    """
313

314
    def __init__(self, field=None, **kwargs):
2✔
315
        if field is not None and not isinstance(field, BaseField):
2✔
316
            raise TypeError(
2✔
317
                f"field argument must be a Field instance (e.g {self.__class__.__name__}(StringField()))"
318
            )
319
        self.field = field
2✔
320
        super().__init__(**kwargs)
2✔
321

322
    @staticmethod
2✔
323
    def _lazy_load_refs(instance, name, ref_values, *, max_depth):
2✔
324
        _dereference = _import_class("DeReference")()
2✔
325
        documents = _dereference(
2✔
326
            ref_values,
327
            max_depth=max_depth,
328
            instance=instance,
329
            name=name,
330
        )
331
        return documents
2✔
332

333
    def __set__(self, instance, value):
2✔
334
        # Some fields e.g EnumField are converted upon __set__
335
        # So it is fair to mimic the same behavior when using e.g ListField(EnumField)
336
        EnumField = _import_class("EnumField")
2✔
337
        if self.field and isinstance(self.field, EnumField):
2✔
338
            if isinstance(value, (list, tuple)):
2✔
339
                value = [self.field.to_python(sub_val) for sub_val in value]
2✔
340
            elif isinstance(value, dict):
2✔
341
                value = {key: self.field.to_python(sub) for key, sub in value.items()}
2✔
342

343
        return super().__set__(instance, value)
2✔
344

345
    def __get__(self, instance, owner):
2✔
346
        """Descriptor to automatically dereference references."""
347
        if instance is None:
2✔
348
            # Document class being used rather than a document object
349
            return self
2✔
350

351
        ReferenceField = _import_class("ReferenceField")
2✔
352
        GenericReferenceField = _import_class("GenericReferenceField")
2✔
353
        EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField")
2✔
354

355
        auto_dereference = instance._fields[self.name]._auto_dereference
2✔
356

357
        dereference = auto_dereference and (
2✔
358
            self.field is None
359
            or isinstance(self.field, (GenericReferenceField, ReferenceField))
360
        )
361

362
        if (
2✔
363
            instance._initialised
364
            and dereference
365
            and instance._data.get(self.name)
366
            and not getattr(instance._data[self.name], "_dereferenced", False)
367
        ):
368
            ref_values = instance._data.get(self.name)
2✔
369
            instance._data[self.name] = self._lazy_load_refs(
2✔
370
                ref_values=ref_values, instance=instance, name=self.name, max_depth=1
371
            )
372
            if hasattr(instance._data[self.name], "_dereferenced"):
2✔
373
                instance._data[self.name]._dereferenced = True
2✔
374

375
        value = super().__get__(instance, owner)
2✔
376

377
        # Convert lists / values so we can watch for any changes on them
378
        if isinstance(value, (list, tuple)):
2✔
379
            if issubclass(type(self), EmbeddedDocumentListField) and not isinstance(
2✔
380
                value, EmbeddedDocumentList
381
            ):
382
                value = EmbeddedDocumentList(value, instance, self.name)
2✔
383
            elif not isinstance(value, BaseList):
2✔
384
                value = BaseList(value, instance, self.name)
2✔
385
            instance._data[self.name] = value
2✔
386
        elif isinstance(value, dict) and not isinstance(value, BaseDict):
2✔
387
            value = BaseDict(value, instance, self.name)
2✔
388
            instance._data[self.name] = value
2✔
389

390
        if (
2✔
391
            auto_dereference
392
            and instance._initialised
393
            and isinstance(value, (BaseList, BaseDict))
394
            and not value._dereferenced
395
        ):
396
            value = self._lazy_load_refs(
2✔
397
                ref_values=value, instance=instance, name=self.name, max_depth=1
398
            )
399
            value._dereferenced = True
2✔
400
            instance._data[self.name] = value
2✔
401

402
        return value
2✔
403

404
    def to_python(self, value):
2✔
405
        """Convert a MongoDB-compatible type to a Python type."""
406
        if isinstance(value, str):
2✔
407
            return value
2✔
408

409
        if hasattr(value, "to_python"):
2✔
410
            return value.to_python()
×
411

412
        BaseDocument = _import_class("BaseDocument")
2✔
413
        if isinstance(value, BaseDocument):
2✔
414
            # Something is wrong, return the value as it is
415
            return value
2✔
416

417
        is_list = False
2✔
418
        if not hasattr(value, "items"):
2✔
419
            try:
2✔
420
                is_list = True
2✔
421
                value = {idx: v for idx, v in enumerate(value)}
2✔
422
            except TypeError:  # Not iterable return the value
2✔
423
                return value
2✔
424

425
        if self.field:
2✔
426
            self.field.set_auto_dereferencing(self._auto_dereference)
2✔
427
            value_dict = {
2✔
428
                key: self.field.to_python(item) for key, item in value.items()
429
            }
430
        else:
431
            Document = _import_class("Document")
2✔
432
            value_dict = {}
2✔
433
            for k, v in value.items():
2✔
434
                if isinstance(v, Document):
2✔
435
                    # We need the id from the saved object to create the DBRef
436
                    if v.pk is None:
2✔
437
                        self.error(
×
438
                            "You can only reference documents once they"
439
                            " have been saved to the database"
440
                        )
441
                    collection = v._get_collection_name()
2✔
442
                    value_dict[k] = DBRef(collection, v.pk)
2✔
443
                elif hasattr(v, "to_python"):
2✔
444
                    value_dict[k] = v.to_python()
×
445
                else:
446
                    value_dict[k] = self.to_python(v)
2✔
447

448
        if is_list:  # Convert back to a list
2✔
449
            return [
2✔
450
                v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0))
451
            ]
452
        return value_dict
2✔
453

454
    def to_mongo(self, value, use_db_field=True, fields=None):
2✔
455
        """Convert a Python type to a MongoDB-compatible type."""
456
        Document = _import_class("Document")
2✔
457
        EmbeddedDocument = _import_class("EmbeddedDocument")
2✔
458
        GenericReferenceField = _import_class("GenericReferenceField")
2✔
459

460
        if isinstance(value, str):
2✔
461
            return value
2✔
462

463
        if hasattr(value, "to_mongo"):
2✔
464
            if isinstance(value, Document):
2✔
465
                return GenericReferenceField().to_mongo(value)
2✔
466
            cls = value.__class__
2✔
467
            val = value.to_mongo(use_db_field, fields)
2✔
468
            # If it's a document that is not inherited add _cls
469
            if isinstance(value, EmbeddedDocument):
2✔
470
                val["_cls"] = cls.__name__
2✔
471
            return val
2✔
472

473
        is_list = False
2✔
474
        if not hasattr(value, "items"):
2✔
475
            try:
2✔
476
                is_list = True
2✔
477
                value = {k: v for k, v in enumerate(value)}
2✔
478
            except TypeError:  # Not iterable return the value
2✔
479
                return value
2✔
480

481
        if self.field:
2✔
482
            value_dict = {
2✔
483
                key: self.field._to_mongo_safe_call(item, use_db_field, fields)
484
                for key, item in value.items()
485
            }
486
        else:
487
            value_dict = {}
2✔
488
            for k, v in value.items():
2✔
489
                if isinstance(v, Document):
2✔
490
                    # We need the id from the saved object to create the DBRef
491
                    if v.pk is None:
2✔
492
                        self.error(
×
493
                            "You can only reference documents once they"
494
                            " have been saved to the database"
495
                        )
496

497
                    # If it's a document that is not inheritable it won't have
498
                    # any _cls data so make it a generic reference allows
499
                    # us to dereference
500
                    meta = getattr(v, "_meta", {})
2✔
501
                    allow_inheritance = meta.get("allow_inheritance")
2✔
502
                    if not allow_inheritance:
2✔
503
                        value_dict[k] = GenericReferenceField().to_mongo(v)
2✔
504
                    else:
505
                        collection = v._get_collection_name()
×
506
                        value_dict[k] = DBRef(collection, v.pk)
×
507
                elif hasattr(v, "to_mongo"):
2✔
508
                    cls = v.__class__
2✔
509
                    val = v.to_mongo(use_db_field, fields)
2✔
510
                    # If it's a document that is not inherited add _cls
511
                    if isinstance(v, (Document, EmbeddedDocument)):
2✔
512
                        val["_cls"] = cls.__name__
2✔
513
                    value_dict[k] = val
2✔
514
                else:
515
                    value_dict[k] = self.to_mongo(v, use_db_field, fields)
2✔
516

517
        if is_list:  # Convert back to a list
2✔
518
            return [
2✔
519
                v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0))
520
            ]
521
        return value_dict
2✔
522

523
    def validate(self, value):
2✔
524
        """If field is provided ensure the value is valid."""
525
        errors = {}
2✔
526
        if self.field:
2✔
527
            if hasattr(value, "items"):
2✔
528
                sequence = value.items()
2✔
529
            else:
530
                sequence = enumerate(value)
2✔
531
            for k, v in sequence:
2✔
532
                try:
2✔
533
                    self.field._validate(v)
2✔
534
                except ValidationError as error:
2✔
535
                    errors[k] = error.errors or error
2✔
536
                except (ValueError, AssertionError) as error:
2✔
537
                    errors[k] = error
×
538

539
            if errors:
2✔
540
                field_class = self.field.__class__.__name__
2✔
541
                self.error(f"Invalid {field_class} item ({value})", errors=errors)
2✔
542
        # Don't allow empty values if required
543
        if self.required and not value:
2✔
544
            self.error("Field is required and cannot be empty")
2✔
545

546
    def prepare_query_value(self, op, value):
2✔
547
        return self.to_mongo(value)
2✔
548

549
    def lookup_member(self, member_name):
2✔
550
        if self.field:
2✔
551
            return self.field.lookup_member(member_name)
2✔
552
        return None
2✔
553

554
    def _set_owner_document(self, owner_document):
2✔
555
        if self.field:
2✔
556
            self.field.owner_document = owner_document
2✔
557
        self._owner_document = owner_document
2✔
558

559

560
class ObjectIdField(BaseField):
2✔
561
    """A field wrapper around MongoDB's ObjectIds."""
562

563
    def to_python(self, value):
2✔
564
        try:
2✔
565
            if not isinstance(value, ObjectId):
2✔
566
                value = ObjectId(value)
2✔
567
        except Exception:
2✔
568
            pass
2✔
569
        return value
2✔
570

571
    def to_mongo(self, value):
2✔
572
        if isinstance(value, ObjectId):
2✔
573
            return value
2✔
574

575
        try:
2✔
576
            return ObjectId(str(value))
2✔
577
        except Exception as e:
2✔
578
            self.error(str(e))
2✔
579

580
    def prepare_query_value(self, op, value):
2✔
581
        if value is None:
2✔
582
            return value
2✔
583
        return self.to_mongo(value)
2✔
584

585
    def validate(self, value):
2✔
586
        try:
2✔
587
            ObjectId(str(value))
2✔
588
        except Exception:
2✔
589
            self.error("Invalid ObjectID")
2✔
590

591

592
class GeoJsonBaseField(BaseField):
2✔
593
    """A geo json field storing a geojson style object."""
594

595
    _geo_index = pymongo.GEOSPHERE
2✔
596
    _type = "GeoBase"
2✔
597

598
    def __init__(self, auto_index=True, *args, **kwargs):
2✔
599
        """
600
        :param bool auto_index: Automatically create a '2dsphere' index.\
601
            Defaults to `True`.
602
        """
603
        self._name = "%sField" % self._type
2✔
604
        if not auto_index:
2✔
605
            self._geo_index = False
2✔
606
        super().__init__(*args, **kwargs)
2✔
607

608
    def validate(self, value):
2✔
609
        """Validate the GeoJson object based on its type."""
610
        if isinstance(value, dict):
2✔
611
            if set(value.keys()) == {"type", "coordinates"}:
2✔
612
                if value["type"] != self._type:
2✔
613
                    self.error(f'{self._name} type must be "{self._type}"')
2✔
614
                return self.validate(value["coordinates"])
2✔
615
            else:
616
                self.error(
2✔
617
                    "%s can only accept a valid GeoJson dictionary"
618
                    " or lists of (x, y)" % self._name
619
                )
620
                return
×
621
        elif not isinstance(value, (list, tuple)):
2✔
622
            self.error("%s can only accept lists of [x, y]" % self._name)
2✔
623
            return
×
624

625
        validate = getattr(self, "_validate_%s" % self._type.lower())
2✔
626
        error = validate(value)
2✔
627
        if error:
2✔
628
            self.error(error)
2✔
629

630
    def _validate_polygon(self, value, top_level=True):
2✔
631
        if not isinstance(value, (list, tuple)):
2✔
632
            return "Polygons must contain list of linestrings"
×
633

634
        # Quick and dirty validator
635
        try:
2✔
636
            value[0][0][0]
2✔
637
        except (TypeError, IndexError):
2✔
638
            return "Invalid Polygon must contain at least one valid linestring"
2✔
639

640
        errors = []
2✔
641
        for val in value:
2✔
642
            error = self._validate_linestring(val, False)
2✔
643
            if not error and val[0] != val[-1]:
2✔
644
                error = "LineStrings must start and end at the same point"
2✔
645
            if error and error not in errors:
2✔
646
                errors.append(error)
2✔
647
        if errors:
2✔
648
            if top_level:
2✔
649
                return "Invalid Polygon:\n%s" % ", ".join(errors)
2✔
650
            else:
651
                return "%s" % ", ".join(errors)
2✔
652

653
    def _validate_linestring(self, value, top_level=True):
2✔
654
        """Validate a linestring."""
655
        if not isinstance(value, (list, tuple)):
2✔
656
            return "LineStrings must contain list of coordinate pairs"
×
657

658
        # Quick and dirty validator
659
        try:
2✔
660
            value[0][0]
2✔
661
        except (TypeError, IndexError):
2✔
662
            return "Invalid LineString must contain at least one valid point"
2✔
663

664
        errors = []
2✔
665
        for val in value:
2✔
666
            error = self._validate_point(val)
2✔
667
            if error and error not in errors:
2✔
668
                errors.append(error)
2✔
669
        if errors:
2✔
670
            if top_level:
2✔
671
                return "Invalid LineString:\n%s" % ", ".join(errors)
2✔
672
            else:
673
                return "%s" % ", ".join(errors)
2✔
674

675
    def _validate_point(self, value):
2✔
676
        """Validate each set of coords"""
677
        if not isinstance(value, (list, tuple)):
2✔
678
            return "Points must be a list of coordinate pairs"
×
679
        elif not len(value) == 2:
2✔
680
            return "Value (%s) must be a two-dimensional point" % repr(value)
2✔
681
        elif not isinstance(value[0], (float, int)) or not isinstance(
2✔
682
            value[1], (float, int)
683
        ):
684
            return "Both values (%s) in point must be float or int" % repr(value)
2✔
685

686
    def _validate_multipoint(self, value):
2✔
687
        if not isinstance(value, (list, tuple)):
2✔
688
            return "MultiPoint must be a list of Point"
×
689

690
        # Quick and dirty validator
691
        try:
2✔
692
            value[0][0]
2✔
693
        except (TypeError, IndexError):
2✔
694
            return "Invalid MultiPoint must contain at least one valid point"
2✔
695

696
        errors = []
2✔
697
        for point in value:
2✔
698
            error = self._validate_point(point)
2✔
699
            if error and error not in errors:
2✔
700
                errors.append(error)
2✔
701

702
        if errors:
2✔
703
            return "%s" % ", ".join(errors)
2✔
704

705
    def _validate_multilinestring(self, value, top_level=True):
2✔
706
        if not isinstance(value, (list, tuple)):
2✔
707
            return "MultiLineString must be a list of LineString"
×
708

709
        # Quick and dirty validator
710
        try:
2✔
711
            value[0][0][0]
2✔
712
        except (TypeError, IndexError):
2✔
713
            return "Invalid MultiLineString must contain at least one valid linestring"
2✔
714

715
        errors = []
2✔
716
        for linestring in value:
2✔
717
            error = self._validate_linestring(linestring, False)
2✔
718
            if error and error not in errors:
2✔
719
                errors.append(error)
2✔
720

721
        if errors:
2✔
722
            if top_level:
2✔
723
                return "Invalid MultiLineString:\n%s" % ", ".join(errors)
2✔
724
            else:
725
                return "%s" % ", ".join(errors)
×
726

727
    def _validate_multipolygon(self, value):
2✔
728
        if not isinstance(value, (list, tuple)):
2✔
729
            return "MultiPolygon must be a list of Polygon"
×
730

731
        # Quick and dirty validator
732
        try:
2✔
733
            value[0][0][0][0]
2✔
734
        except (TypeError, IndexError):
2✔
735
            return "Invalid MultiPolygon must contain at least one valid Polygon"
2✔
736

737
        errors = []
2✔
738
        for polygon in value:
2✔
739
            error = self._validate_polygon(polygon, False)
2✔
740
            if error and error not in errors:
2✔
741
                errors.append(error)
2✔
742

743
        if errors:
2✔
744
            return "Invalid MultiPolygon:\n%s" % ", ".join(errors)
2✔
745

746
    def to_mongo(self, value):
2✔
747
        if isinstance(value, dict):
2✔
748
            return value
2✔
749
        return SON([("type", self._type), ("coordinates", 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