Skip to content

Commit

Permalink
Merge 4282b0b into 9485f02
Browse files Browse the repository at this point in the history
  • Loading branch information
lafrech committed Apr 18, 2020
2 parents 9485f02 + 4282b0b commit ae727a4
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 137 deletions.
22 changes: 16 additions & 6 deletions tests/test_data_proxy.py
Expand Up @@ -462,16 +462,23 @@ class MyEmbedded(EmbeddedDocument):

class MySchema(EmbeddedSchema):
# EmbeddedField need instance to retrieve implementation
listed = fields.ListField(fields.EmbeddedField(MyEmbedded, instance=self.instance))
embedded = fields.EmbeddedField(MyEmbedded, instance=self.instance)
required = fields.IntField(required=True)
embedded = fields.EmbeddedField(MyEmbedded, instance=self.instance)
listed = fields.ListField(fields.EmbeddedField(MyEmbedded, instance=self.instance))
dicted = fields.DictField(
values=fields.EmbeddedField(MyEmbedded, instance=self.instance))

MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()

d.load({'embedded': {'required': 42}, 'required': 42, 'listed': [{'required': 42}]})
d.load({
'required': 42,
'embedded': {'required': 42},
'listed': [{'required': 42}],
'dicted': {'a': {'required': 42}}
})
d.required_validate()
# Empty list should not trigger required if embedded field has required fields
# Empty list/dict should not trigger required if embedded field has required fields
d.load({'embedded': {'required': 42}, 'required': 42})
d.required_validate()

Expand All @@ -489,10 +496,13 @@ class MySchema(EmbeddedSchema):
d.required_validate()
assert exc.value.messages == {'embedded': {'required': ['Missing data for required field.']}}

d.load({'embedded': {'required': 42}, 'required': 42, 'listed': [{}]})
d.load({'embedded': {'required': 42}, 'required': 42, 'listed': [{}], 'dicted': {'a': {}}})
with pytest.raises(ValidationError) as exc:
d.required_validate()
assert exc.value.messages == {'listed': {0: {'required': ['Missing data for required field.']}}}
assert exc.value.messages == {
'listed': {0: {'required': ['Missing data for required field.']}},
'dicted': {'a': {'value': {'required': ['Missing data for required field.']}}},
}

def test_unkown_field_in_db(self):
class MySchema(EmbeddedSchema):
Expand Down
142 changes: 123 additions & 19 deletions tests/test_fields.py
Expand Up @@ -8,7 +8,7 @@
from marshmallow import ValidationError, missing

from umongo.data_proxy import data_proxy_factory
from umongo import Document, EmbeddedDocument, Schema, EmbeddedSchema, fields, Reference
from umongo import Document, EmbeddedDocument, Schema, EmbeddedSchema, fields, Reference, validate
from umongo.data_objects import List, Dict

from .common import BaseTest
Expand Down Expand Up @@ -45,8 +45,10 @@ class MyDoc(Document):
embedded = fields.EmbeddedField(MyEmbedded)
embedded_required = fields.EmbeddedField(MyEmbedded, required=True)
embedded_list = fields.ListField(fields.EmbeddedField(MyEmbedded))
embedded_dict = fields.DictField(values=fields.EmbeddedField(MyEmbedded))

MyDoc(embedded={}, embedded_list=[{}]) # Required fields are check on commit
# Required fields are checked on commit
MyDoc(embedded={}, embedded_list=[{}], embedded_dict={'a': {}})
# Don't check required fields in not required missing embedded
MyDoc(embedded_required={'required_field': 42}).required_validate()
# Now trigger required fails
Expand All @@ -57,17 +59,32 @@ class MyDoc(Document):
MyDoc(embedded_required={'optional_field': 1}).required_validate()
assert exc.value.messages == {'embedded_required': {'required_field': ['Missing data for required field.']}}
with pytest.raises(ValidationError) as exc:
MyDoc(embedded={'optional_field': 1},
embedded_required={'required_field': 42}).required_validate()
MyDoc(
embedded={'optional_field': 1},
embedded_required={'required_field': 42}
).required_validate()
assert exc.value.messages == {'embedded': {'required_field': ['Missing data for required field.']}}
with pytest.raises(ValidationError) as exc:
MyDoc(embedded={'required_field': 1}, embedded_list=[{'optional_field': 1}], embedded_required={'required_field': 42}).required_validate()
assert exc.value.messages == {'embedded_list': {0: {'required_field': ['Missing data for required field.']}}}
MyDoc(
embedded={'required_field': 1},
embedded_list=[{'optional_field': 1}],
embedded_dict={'a': {'optional_field': 1}},
embedded_required={'required_field': 42}
).required_validate()
assert exc.value.messages == {
'embedded_list': {0: {'required_field': ['Missing data for required field.']}},
'embedded_dict': {'a': {'value': {'required_field': ['Missing data for required field.']}}},
}

# Check valid constructions
doc = MyDoc(embedded={'required_field': 1}, embedded_list=[], embedded_required={'required_field': 42})
doc = MyDoc(embedded={'required_field': 1}, embedded_list=[], embedded_dict={}, embedded_required={'required_field': 42})
doc.required_validate()
doc = MyDoc(embedded={'required_field': 1}, embedded_list=[{'required_field': 1}], embedded_required={'required_field': 42})
doc = MyDoc(
embedded={'required_field': 1},
embedded_list=[{'required_field': 1}],
embedded_dict={'a': {'required_field': 1}},
embedded_required={'required_field': 42},
)
doc.required_validate()


Expand Down Expand Up @@ -227,6 +244,13 @@ def test_dict(self):

class MySchema(Schema):
dict = fields.DictField(attribute='in_mongo_dict', allow_none=True)
kdict = fields.DictField(keys=fields.StringField(validate=validate.Length(0, 1)))
vdict = fields.DictField(values=fields.IntField(validate=validate.Range(max=5)))
kvdict = fields.DictField(
keys=fields.StringField(validate=validate.Length(0, 1)),
values=fields.IntField(validate=validate.Range(max=5))
)
dtdict = fields.DictField(values=fields.DateTimeField)

MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
Expand All @@ -237,18 +261,22 @@ class MySchema(Schema):
assert d.get('dict') == {'a': 1, 'b': {'c': True}}
assert d.to_mongo() == {'in_mongo_dict': {'a': 1, 'b': {'c': True}}}

# Must manually set_dirty to take the changes into account
dict_ = d.get('dict')
dict_['a'] = 1
assert d.to_mongo(update=True) is None
dict_.set_modified()
assert d.to_mongo(update=True) == {'$set': {'in_mongo_dict': {'a': 1, 'b': {'c': True}}}}
dict_.clear_modified()
assert d.to_mongo(update=True) is None

# Test repr readability
repr_d = repr(d.get('dict'))
assert any(
repr_d == "<object umongo.data_objects.Dict({})>".format(d)
for d in ("{'a': 1, 'b': {'c': True}}", "{'b': {'c': True}, 'a': 1}")
)

d2 = MyDataProxy({'dict': {'a': 1, 'b': {'c': True}}})
assert d2.to_mongo() == {'in_mongo_dict': {'a': 1, 'b': {'c': True}}}

assert d2.to_mongo(update=True) == {'$set': {'in_mongo_dict': {'a': 1, 'b': {'c': True}}}}
d2.set('dict', {})
assert d2.to_mongo() == {'in_mongo_dict': {}}
assert d2.to_mongo(update=True) == {'$set': {'in_mongo_dict': {}}}
Expand All @@ -264,23 +292,38 @@ class MySchema(Schema):

d3.from_mongo({'in_mongo_dict': {}})
assert d3._data.get('in_mongo_dict') == {}
d3.get('dict')['field'] = 'value'
assert d3.to_mongo(update=True) is None
d3.get('dict').set_modified()
assert d3.to_mongo(update=True) == {'$set': {'in_mongo_dict': {'field': 'value'}}}
assert d3.to_mongo() == {'in_mongo_dict': {'field': 'value'}}
d3.get('dict')['c'] = 3
assert d3.to_mongo(update=True) == {'$set': {'in_mongo_dict': {'c': 3}}}
assert d3.to_mongo() == {'in_mongo_dict': {'c': 3}}

d4 = MyDataProxy({'dict': None})
assert d4.to_mongo() == {'in_mongo_dict': None}
d4.from_mongo({'in_mongo_dict': None})
assert d4.get('dict') is None

with pytest.raises(ValidationError) as exc:
MyDataProxy({'kdict': {'ab': 1}})
assert exc.value.messages == {'kdict': {'ab': {'key': ['Length must be between 0 and 1.']}}}
with pytest.raises(ValidationError) as exc:
MyDataProxy({'vdict': {'a': 9}})
assert exc.value.messages == {
'vdict': {'a': {'value': ['Must be less than or equal to 5.']}}}
with pytest.raises(ValidationError) as exc:
MyDataProxy({'kvdict': {'ab': 9}})
assert exc.value.messages == {'kvdict': {'ab': {
'key': ['Length must be between 0 and 1.'],
'value': ['Must be less than or equal to 5.']
}}}

d5 = MyDataProxy({'dtdict': {'a': "2016-08-06T00:00:00"}})
assert d5.to_mongo() == {'dtdict': {'a': dt.datetime(2016, 8, 6)}}

def test_dict_default(self):

class MySchema(Schema):
# Passing a mutable as default is a bad idea in real life
d_dict = fields.DictField(default={'1': 1, '2': 2})
c_dict = fields.DictField(default=lambda: {'1': 1, '2': 2})
d_dict = fields.DictField(values=fields.IntField, default={'1': 1, '2': 2})
c_dict = fields.DictField(values=fields.IntField, default=lambda: {'1': 1, '2': 2})

MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
Expand All @@ -302,6 +345,67 @@ class MySchema(Schema):
assert isinstance(d.get('d_dict'), Dict)
assert isinstance(d.get('c_dict'), Dict)

def test_complex_dict(self):

@self.instance.register
class MyEmbeddedDocument(EmbeddedDocument):
field = fields.IntField()

@self.instance.register
class ToRefDoc(Document):
pass

@self.instance.register
class MyDoc(Document):
embeds = fields.DictField(values=fields.EmbeddedField(MyEmbeddedDocument))
refs = fields.DictField(values=fields.ReferenceField(ToRefDoc))

MySchema = MyDoc.Schema

obj_id1 = ObjectId()
obj_id2 = ObjectId()
to_ref_doc1 = ToRefDoc.build_from_mongo(data={'_id': obj_id1})
MyDataProxy = data_proxy_factory('My', MySchema())
d = MyDataProxy()
d.load({
'embeds': {
'a': MyEmbeddedDocument(field=1),
'b': {'field': 2},
},
'refs': {
'1': to_ref_doc1,
'2': Reference(ToRefDoc, obj_id2),
}
})
assert d.to_mongo() == {
'embeds': {'a': {'field': 1}, 'b': {'field': 2}},
'refs': {'1': obj_id1, '2': obj_id2},
}
assert isinstance(d.get('embeds'), Dict)
assert isinstance(d.get('refs'), Dict)
for e in d.get('refs').values():
assert isinstance(e, Reference)
for e in d.get('embeds').values():
assert isinstance(e, MyEmbeddedDocument)
# Test dict modification as well
refs_dict = d.get('refs')
refs_dict.update({'3': to_ref_doc1, '4': Reference(ToRefDoc, obj_id2)})
for e in refs_dict.values():
assert isinstance(e, Reference)
embeds_dict = d.get('embeds')
embeds_dict.update({'c': MyEmbeddedDocument(field=3), 'd': {'field': 4}})
for e in embeds_dict.values():
assert isinstance(e, MyEmbeddedDocument)
# Modifying an EmbeddedDocument inside a dict should count a dict modification
d.clear_modified()
d.get('refs')['1'] = obj_id2
assert d.to_mongo(update=True) == {'$set': {'refs': {
'1': obj_id2, '2': obj_id2, '3': obj_id1, '4': obj_id2}}}
d.clear_modified()
d.get('embeds')['b'].field = 42
assert d.to_mongo(update=True) == {'$set': {'embeds': {
'a': {'field': 1}, 'b': {'field': 42}, 'c': {'field': 3}, 'd': {'field': 4}}}}

def test_list(self):

class MySchema(Schema):
Expand Down
19 changes: 18 additions & 1 deletion tests/test_marshmallow.py
Expand Up @@ -81,8 +81,10 @@ class Accessory(EmbeddedDocument):
@self.instance.register
class Bag(Document):
id = fields.EmbeddedField(Accessory, attribute='_id', required=True)
names = fields.ListField(fields.StringField())
names = fields.ListField(fields.StringField)
content = fields.ListField(fields.EmbeddedField(Accessory))
relations = fields.DictField(fields.StringField, fields.StringField)
inventory = fields.DictField(fields.StringField, fields.EmbeddedField(Accessory))

ma_field = Bag.schema.fields['id'].as_marshmallow_field(params={
'load_only': True,
Expand All @@ -100,6 +102,17 @@ class Bag(Document):
assert ma_field.load_only is True
assert ma_field.inner.required is True
assert ma_field.inner.nested._declared_fields['value'].dump_only
ma_field = Bag.schema.fields['relations'].as_marshmallow_field(params={
'load_only': True,
'params': {'dump_only': True}})
assert ma_field.load_only is True
assert ma_field.value_field.dump_only is True
ma_field = Bag.schema.fields['inventory'].as_marshmallow_field(params={
'load_only': True,
'params': {'required': True, 'params': {'value': {'dump_only': True}}}})
assert ma_field.load_only is True
assert ma_field.value_field.required is True
assert ma_field.value_field.nested._declared_fields['value'].dump_only

def test_pass_meta_attributes(self):
@self.instance.register
Expand All @@ -111,6 +124,7 @@ class Accessory(EmbeddedDocument):
class Bag(Document):
id = fields.EmbeddedField(Accessory, attribute='_id', required=True)
content = fields.ListField(fields.EmbeddedField(Accessory))
inventory = fields.DictField(fields.StringField, fields.EmbeddedField(Accessory))

ma_schema = Bag.schema.as_marshmallow_schema(meta={'exclude': ('id',)})
assert ma_schema.Meta.exclude == ('id',)
Expand All @@ -120,6 +134,9 @@ class Bag(Document):
ma_schema = Bag.schema.as_marshmallow_schema(params={
'content': {'params': {'meta': {'exclude': ('value',)}}}})
assert ma_schema._declared_fields['content'].inner.nested.Meta.exclude == ('value',)
ma_schema = Bag.schema.as_marshmallow_schema(params={
'inventory': {'params': {'meta': {'exclude': ('value',)}}}})
assert ma_schema._declared_fields['inventory'].value_field.nested.Meta.exclude == ('value',)

class DumpOnlyIdSchema(marshmallow.Schema):
class Meta:
Expand Down
9 changes: 7 additions & 2 deletions umongo/builder.py
Expand Up @@ -12,7 +12,7 @@
from .exceptions import DocumentDefinitionError, NotRegisteredDocumentError
from .schema import Schema, on_need_add_id_field, add_child_field
from .indexes import parse_index
from .fields import ListField, EmbeddedField
from .fields import ListField, DictField, EmbeddedField


def camel_to_snake(name):
Expand Down Expand Up @@ -200,7 +200,12 @@ def _patch_field(self, field):
field.instance = self.instance
if isinstance(field, ListField):
self._patch_field(field.inner)
if isinstance(field, EmbeddedField):
elif isinstance(field, DictField):
if field.key_field:
self._patch_field(field.key_field)
if field.value_field:
self._patch_field(field.value_field)
elif isinstance(field, EmbeddedField):
for embedded_field in field.schema.fields.values():
self._patch_field(embedded_field)

Expand Down

0 comments on commit ae727a4

Please sign in to comment.