Skip to content

Commit 2931801

Browse files
committed
WIP
1 parent e51ee40 commit 2931801

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

mongoengine/base/datastructures.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,60 @@ def __getattr__(self, name):
472472

473473
def __repr__(self):
474474
return f"<LazyReference({self.document_type}, {self.pk!r})>"
475+
476+
477+
class RawDict:
478+
def __init__(self, data, deserialize_method):
479+
self._data = data
480+
self.deserialize_method = deserialize_method
481+
482+
def deserialize(self):
483+
return self.deserialize_method(self._data)
484+
485+
def __setitem__(self, key, item):
486+
self._data[key] = item
487+
488+
def __getitem__(self, key):
489+
return self._data[key]
490+
491+
def __repr__(self):
492+
return repr(self._data)
493+
494+
def __len__(self):
495+
return len(self._data)
496+
497+
def __delitem__(self, key):
498+
del self._data[key]
499+
500+
def clear(self):
501+
return self._data.clear()
502+
503+
def copy(self):
504+
return self._data.copy()
505+
506+
def has_key(self, k):
507+
return k in self._data
508+
509+
def update(self, *args, **kwargs):
510+
return self._data.update(*args, **kwargs)
511+
512+
def keys(self):
513+
return self._data.keys()
514+
515+
def values(self):
516+
return self._data.values()
517+
518+
def items(self):
519+
return self._data.items()
520+
521+
def pop(self, *args):
522+
return self._data.pop(*args)
523+
524+
def __cmp__(self, dict_):
525+
return self.__cmp__(self._data, dict_)
526+
527+
def __contains__(self, item):
528+
return item in self._data
529+
530+
def __iter__(self):
531+
return iter(self._data)

mongoengine/fields.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from bson.decimal128 import Decimal128, create_decimal128_context
1717
from pymongo import ReturnDocument
1818

19+
from mongoengine.base.datastructures import RawDict
20+
1921
try:
2022
import dateutil
2123
except ImportError:
@@ -81,6 +83,7 @@
8183
"SortedListField",
8284
"EmbeddedDocumentListField",
8385
"DictField",
86+
"LazyDictField",
8487
"MapField",
8588
"ReferenceField",
8689
"CachedReferenceField",
@@ -1035,6 +1038,7 @@ def key_starts_with_dollar(d):
10351038
return True
10361039

10371040

1041+
# TODO: make a LazyDictField that lazily deferences on access
10381042
class DictField(ComplexBaseField):
10391043
"""A dictionary field that wraps a standard Python dictionary. This is
10401044
similar to an embedded document, but the structure is not defined.
@@ -1089,6 +1093,66 @@ def prepare_query_value(self, op, value):
10891093
return super().prepare_query_value(op, value)
10901094

10911095

1096+
class LazyDictField(ComplexBaseField):
1097+
"""A lazy dictionary field that wraps a standard Python dictionary.
1098+
Unlike the :class:`~mongoengine.fields.DictField`, it will
1099+
**not** be automatically deserialized. Manual deserialization must be triggered
1100+
using the ``deserialize()`` method.
1101+
1102+
.. note::
1103+
Required means it cannot be empty - as the default for DictFields is {}
1104+
"""
1105+
1106+
def __init__(self, field=None, *args, **kwargs):
1107+
kwargs.setdefault("default", dict)
1108+
super().__init__(*args, field=field, **kwargs)
1109+
self.set_auto_dereferencing(False)
1110+
1111+
def validate(self, value):
1112+
"""Make sure that a list of valid fields is being used."""
1113+
if not isinstance(value, dict):
1114+
self.error("Only dictionaries may be used in a DictField")
1115+
1116+
if key_not_string(value):
1117+
msg = "Invalid dictionary key - documents must have only string keys"
1118+
self.error(msg)
1119+
1120+
# Following condition applies to MongoDB >= 3.6
1121+
# older Mongo has stricter constraints but
1122+
# it will be rejected upon insertion anyway
1123+
# Having a validation that depends on the MongoDB version
1124+
# is not straightforward as the field isn't aware of the connected Mongo
1125+
if key_starts_with_dollar(value):
1126+
self.error(
1127+
'Invalid dictionary key name - keys may not startswith "$" characters'
1128+
)
1129+
super().validate(value)
1130+
1131+
def lookup_member(self, member_name):
1132+
return DictField(db_field=member_name)
1133+
1134+
def prepare_query_value(self, op, value):
1135+
match_operators = [*STRING_OPERATORS]
1136+
1137+
if op in match_operators and isinstance(value, str):
1138+
return StringField().prepare_query_value(op, value)
1139+
1140+
if hasattr(
1141+
self.field, "field"
1142+
): # Used for instance when using DictField(ListField(IntField()))
1143+
if op in ("set", "unset") and isinstance(value, dict):
1144+
return {
1145+
k: self.field.prepare_query_value(op, v) for k, v in value.items()
1146+
}
1147+
return self.field.prepare_query_value(op, value)
1148+
1149+
return super().prepare_query_value(op, value)
1150+
1151+
def to_python(self, value):
1152+
self._data = RawDict(value, super().to_python)
1153+
return self._data
1154+
1155+
10921156
class MapField(DictField):
10931157
"""A field that maps a name to a specified field type. Similar to
10941158
a DictField, except the 'value' of each item must match the specified

0 commit comments

Comments
 (0)