# -*- coding: utf-8 -*- from mongoengine import * from mongoengine.base import BaseDict from tests.utils import MongoDBTestCase, get_as_pymongo import pytest class TestDictField(MongoDBTestCase): def test_storage(self): class BlogPost(Document): info = DictField() BlogPost.drop_collection() info = {"testkey": "testvalue"} post = BlogPost(info=info).save() assert get_as_pymongo(post) == {"_id": post.id, "info": info} def test_general_things(self): """Ensure that dict types work as expected.""" class BlogPost(Document): info = DictField() BlogPost.drop_collection() post = BlogPost() post.info = "my post" with pytest.raises(ValidationError): post.validate() post.info = ["test", "test"] with pytest.raises(ValidationError): post.validate() post.info = {"$title": "test"} with pytest.raises(ValidationError): post.validate() post.info = {"nested": {"$title": "test"}} with pytest.raises(ValidationError): post.validate() post.info = {"the.title": "test"} with pytest.raises(ValidationError): post.validate() post.info = {"nested": {"the.title": "test"}} with pytest.raises(ValidationError): post.validate() post.info = {1: "test"} with pytest.raises(ValidationError): post.validate() post.info = {"title": "test"} post.save() post = BlogPost() post.info = {"title": "dollar_sign", "details": {"te$t": "test"}} post.save() post = BlogPost() post.info = {"details": {"test": "test"}} post.save() post = BlogPost() post.info = {"details": {"test": 3}} post.save() assert BlogPost.objects.count() == 4 assert BlogPost.objects.filter(info__title__exact="test").count() == 1 assert BlogPost.objects.filter(info__details__test__exact="test").count() == 1 post = BlogPost.objects.filter(info__title__exact="dollar_sign").first() assert "te$t" in post["info"]["details"] # Confirm handles non strings or non existing keys assert BlogPost.objects.filter(info__details__test__exact=5).count() == 0 assert BlogPost.objects.filter(info__made_up__test__exact="test").count() == 0 post = BlogPost.objects.create(info={"title": "original"}) post.info.update({"title": "updated"}) post.save() post.reload() assert "updated" == post.info["title"] post.info.setdefault("authors", []) post.save() post.reload() assert [] == post.info["authors"] def test_dictfield_dump_document(self): """Ensure a DictField can handle another document's dump.""" class Doc(Document): field = DictField() class ToEmbed(Document): id = IntField(primary_key=True, default=1) recursive = DictField() class ToEmbedParent(Document): id = IntField(primary_key=True, default=1) recursive = DictField() meta = {"allow_inheritance": True} class ToEmbedChild(ToEmbedParent): pass to_embed_recursive = ToEmbed(id=1).save() to_embed = ToEmbed( id=2, recursive=to_embed_recursive.to_mongo().to_dict() ).save() doc = Doc(field=to_embed.to_mongo().to_dict()) doc.save() assert isinstance(doc.field, dict) assert doc.field == {"_id": 2, "recursive": {"_id": 1, "recursive": {}}} # Same thing with a Document with a _cls field to_embed_recursive = ToEmbedChild(id=1).save() to_embed_child = ToEmbedChild( id=2, recursive=to_embed_recursive.to_mongo().to_dict() ).save() doc = Doc(field=to_embed_child.to_mongo().to_dict()) doc.save() assert isinstance(doc.field, dict) expected = { "_id": 2, "_cls": "ToEmbedParent.ToEmbedChild", "recursive": { "_id": 1, "_cls": "ToEmbedParent.ToEmbedChild", "recursive": {}, }, } assert doc.field == expected def test_dictfield_strict(self): """Ensure that dict field handles validation if provided a strict field type.""" class Simple(Document): mapping = DictField(field=IntField()) Simple.drop_collection() e = Simple() e.mapping["someint"] = 1 e.save() # try creating an invalid mapping with pytest.raises(ValidationError): e.mapping["somestring"] = "abc" e.save() def test_dictfield_complex(self): """Ensure that the dict field can handle the complex types.""" class SettingBase(EmbeddedDocument): meta = {"allow_inheritance": True} class StringSetting(SettingBase): value = StringField() class IntegerSetting(SettingBase): value = IntField() class Simple(Document): mapping = DictField() Simple.drop_collection() e = Simple() e.mapping["somestring"] = StringSetting(value="foo") e.mapping["someint"] = IntegerSetting(value=42) e.mapping["nested_dict"] = { "number": 1, "string": "Hi!", "float": 1.001, "complex": IntegerSetting(value=42), "list": [IntegerSetting(value=42), StringSetting(value="foo")], } e.save() e2 = Simple.objects.get(id=e.id) assert isinstance(e2.mapping["somestring"], StringSetting) assert isinstance(e2.mapping["someint"], IntegerSetting) # Test querying assert Simple.objects.filter(mapping__someint__value=42).count() == 1 assert Simple.objects.filter(mapping__nested_dict__number=1).count() == 1 assert ( Simple.objects.filter(mapping__nested_dict__complex__value=42).count() == 1 ) assert ( Simple.objects.filter(mapping__nested_dict__list__0__value=42).count() == 1 ) assert ( Simple.objects.filter(mapping__nested_dict__list__1__value="foo").count() == 1 ) # Confirm can update Simple.objects().update(set__mapping={"someint": IntegerSetting(value=10)}) Simple.objects().update( set__mapping__nested_dict__list__1=StringSetting(value="Boo") ) assert ( Simple.objects.filter(mapping__nested_dict__list__1__value="foo").count() == 0 ) assert ( Simple.objects.filter(mapping__nested_dict__list__1__value="Boo").count() == 1 ) def test_push_dict(self): class MyModel(Document): events = ListField(DictField()) doc = MyModel(events=[{"a": 1}]).save() raw_doc = get_as_pymongo(doc) expected_raw_doc = {"_id": doc.id, "events": [{"a": 1}]} assert raw_doc == expected_raw_doc MyModel.objects(id=doc.id).update(push__events={}) raw_doc = get_as_pymongo(doc) expected_raw_doc = {"_id": doc.id, "events": [{"a": 1}, {}]} assert raw_doc == expected_raw_doc def test_ensure_unique_default_instances(self): """Ensure that every field has it's own unique default instance.""" class D(Document): data = DictField() data2 = DictField(default=lambda: {}) d1 = D() d1.data["foo"] = "bar" d1.data2["foo"] = "bar" d2 = D() assert d2.data == {} assert d2.data2 == {} def test_dict_field_invalid_dict_value(self): class DictFieldTest(Document): dictionary = DictField(required=True) DictFieldTest.drop_collection() test = DictFieldTest(dictionary=None) test.dictionary # Just access to test getter with pytest.raises(ValidationError): test.validate() test = DictFieldTest(dictionary=False) test.dictionary # Just access to test getter with pytest.raises(ValidationError): test.validate() def test_dict_field_raises_validation_error_if_wrongly_assign_embedded_doc(self): class DictFieldTest(Document): dictionary = DictField(required=True) DictFieldTest.drop_collection() class Embedded(EmbeddedDocument): name = StringField() embed = Embedded(name="garbage") doc = DictFieldTest(dictionary=embed) with pytest.raises(ValidationError) as ctx_err: doc.validate() assert "'dictionary'" in str(ctx_err.exception) assert "Only dictionaries may be used in a DictField" in str(ctx_err.exception) def test_atomic_update_dict_field(self): """Ensure that the entire DictField can be atomically updated.""" class Simple(Document): mapping = DictField(field=ListField(IntField(required=True))) Simple.drop_collection() e = Simple() e.mapping["someints"] = [1, 2] e.save() e.update(set__mapping={"ints": [3, 4]}) e.reload() assert BaseDict == type(e.mapping) assert {"ints": [3, 4]} == e.mapping # try creating an invalid mapping with pytest.raises(ValueError): e.update(set__mapping={"somestrings": ["foo", "bar"]}) def test_dictfield_with_referencefield_complex_nesting_cases(self): """Ensure complex nesting inside DictField handles dereferencing of ReferenceField(dbref=True | False)""" # Relates to Issue #1453 class Doc(Document): s = StringField() class Simple(Document): mapping0 = DictField(ReferenceField(Doc, dbref=True)) mapping1 = DictField(ReferenceField(Doc, dbref=False)) mapping2 = DictField(ListField(ReferenceField(Doc, dbref=True))) mapping3 = DictField(ListField(ReferenceField(Doc, dbref=False))) mapping4 = DictField(DictField(field=ReferenceField(Doc, dbref=True))) mapping5 = DictField(DictField(field=ReferenceField(Doc, dbref=False))) mapping6 = DictField(ListField(DictField(ReferenceField(Doc, dbref=True)))) mapping7 = DictField(ListField(DictField(ReferenceField(Doc, dbref=False)))) mapping8 = DictField( ListField(DictField(ListField(ReferenceField(Doc, dbref=True)))) ) mapping9 = DictField( ListField(DictField(ListField(ReferenceField(Doc, dbref=False)))) ) Doc.drop_collection() Simple.drop_collection() d = Doc(s="aa").save() e = Simple() e.mapping0["someint"] = e.mapping1["someint"] = d e.mapping2["someint"] = e.mapping3["someint"] = [d] e.mapping4["someint"] = e.mapping5["someint"] = {"d": d} e.mapping6["someint"] = e.mapping7["someint"] = [{"d": d}] e.mapping8["someint"] = e.mapping9["someint"] = [{"d": [d]}] e.save() s = Simple.objects.first() assert isinstance(s.mapping0["someint"], Doc) assert isinstance(s.mapping1["someint"], Doc) assert isinstance(s.mapping2["someint"][0], Doc) assert isinstance(s.mapping3["someint"][0], Doc) assert isinstance(s.mapping4["someint"]["d"], Doc) assert isinstance(s.mapping5["someint"]["d"], Doc) assert isinstance(s.mapping6["someint"][0]["d"], Doc) 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)