Skip to content

Preserve None values in DictFields. #2278

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions mongoengine/base/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ def _delta(self):

set_fields = self._get_changed_fields()
unset_data = {}
deleted_sentinel = object()
if hasattr(self, "_changed_fields"):
set_data = {}
# Fetch each set item from its path
Expand All @@ -660,7 +661,7 @@ def _delta(self):
d = d[int(p)]
elif hasattr(d, "get"):
# dict-like (dict, embedded document)
d = d.get(p)
d = d.get(p, deleted_sentinel)
new_path.append(p)
path = ".".join(new_path)
set_data[path] = d
Expand All @@ -669,10 +670,14 @@ def _delta(self):
if "_id" in set_data:
del set_data["_id"]

DictField = _import_class("DictField")

# Determine if any changed items were actually unset.
for path, value in set_data.items():
if value or isinstance(
value, (numbers.Number, bool)
if (
value
and value != deleted_sentinel
or isinstance(value, (numbers.Number, bool))
): # Account for 0 and True that are truthy
continue

Expand All @@ -690,7 +695,15 @@ def _delta(self):
else: # Perform a full lookup for lists / embedded lookups
d = self
db_field_name = parts.pop()
preserve = False
for p in parts:
if value != deleted_sentinel and hasattr(d, "_fields"):
# Preserve None values in DictFields
field_name = d._reverse_db_field_map.get(p, p)
if isinstance(d._fields.get(field_name), DictField):
preserve = True
break

if isinstance(d, list) and p.isdigit():
d = d[int(p)]
elif hasattr(d, "__getattribute__") and not isinstance(d, dict):
Expand All @@ -699,6 +712,9 @@ def _delta(self):
else:
d = d.get(p)

if preserve:
continue

if hasattr(d, "_fields"):
field_name = d._reverse_db_field_map.get(
db_field_name, db_field_name
Expand All @@ -711,6 +727,9 @@ def _delta(self):
if default is not None:
default = default() if callable(default) else default

if value == deleted_sentinel:
value = None

if value != default:
continue

Expand Down
41 changes: 41 additions & 0 deletions tests/fields/test_dict_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,3 +354,44 @@ class Simple(Document):
assert isinstance(s.mapping7["someint"][0]["d"], Doc)
assert isinstance(s.mapping8["someint"][0]["d"][0], Doc)
assert isinstance(s.mapping9["someint"][0]["d"][0], Doc)

def test_dictfield_with_none_values(self):
class Doc(Document):
field = DictField()

Doc.drop_collection()
Doc(field={"key": "value", "key2": None}).save()
d = Doc.objects.first()
assert {"key": "value", "key2": None} == d.field

d.field["key"] = None
d.save()
d = Doc.objects.first()
assert {"key": None, "key2": None} == d.field

del d.field["key"]
d.save()
d = Doc.objects.first()
assert {"key2": None} == d.field

def test_embedded_dictfield_with_none_values(self):
class EmbeddedDoc(EmbeddedDocument):
field = DictField()

class Doc(Document):
field = ListField(EmbeddedDocumentField(EmbeddedDoc))

Doc.drop_collection()
Doc(field=[EmbeddedDoc(field={"key": "value", "key2": None})]).save()
d = Doc.objects.first()
assert {"key": "value", "key2": None} == d.field[0].field

d.field[0].field["key"] = None
d.save()
d = Doc.objects.first()
assert {"key": None, "key2": None} == d.field[0].field

del d.field[0].field["key"]
d.save()
d = Doc.objects.first()
assert {"key2": None} == d.field[0].field