|
16 | 16 | from bson.decimal128 import Decimal128, create_decimal128_context |
17 | 17 | from pymongo import ReturnDocument |
18 | 18 |
|
| 19 | +from mongoengine.base.datastructures import RawDict |
| 20 | + |
19 | 21 | try: |
20 | 22 | import dateutil |
21 | 23 | except ImportError: |
|
81 | 83 | "SortedListField", |
82 | 84 | "EmbeddedDocumentListField", |
83 | 85 | "DictField", |
| 86 | + "LazyDictField", |
84 | 87 | "MapField", |
85 | 88 | "ReferenceField", |
86 | 89 | "CachedReferenceField", |
@@ -1035,6 +1038,7 @@ def key_starts_with_dollar(d): |
1035 | 1038 | return True |
1036 | 1039 |
|
1037 | 1040 |
|
| 1041 | +# TODO: make a LazyDictField that lazily deferences on access |
1038 | 1042 | class DictField(ComplexBaseField): |
1039 | 1043 | """A dictionary field that wraps a standard Python dictionary. This is |
1040 | 1044 | similar to an embedded document, but the structure is not defined. |
@@ -1089,6 +1093,66 @@ def prepare_query_value(self, op, value): |
1089 | 1093 | return super().prepare_query_value(op, value) |
1090 | 1094 |
|
1091 | 1095 |
|
| 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 | + |
1092 | 1156 | class MapField(DictField): |
1093 | 1157 | """A field that maps a name to a specified field type. Similar to |
1094 | 1158 | a DictField, except the 'value' of each item must match the specified |
|
0 commit comments