Repository: Scille/umongo
Pull request #162 (2019-01-19 17:38:01)

Add Decimal128 support
Fix Issue #161

coveralls: 
[![Coverage Status](https://coveralls.io/builds/21168187/badge)](https://coveralls.io/builds/21168187)

Coverage decreased (-0.1%) to 95.761% when pulling **99d0727b264706c2a545d14a7e65024eaf21a605 on serjshevchenko:decimal128** into **d71e9b46a201c3a0b02fc8655531146a43930c80 on Scille:master**.

serjshevchenko: As mentioned in Issue #161  descr., this only works since mongo3.4 cause only then Dicimal128 added.
lafrech: I added the mashmallow<3 constraint in another commit for clarity.

@serjshevchenko, please

- rebase (remove the setup.py modification from your commit)
- add yourself to AUTHORS.rst
- add missing line in the tests (see my comment above)

And I think this is good to go.

AFAIU, this is not a breaking change as the Decimal was already broken on MongoDB < 3.4. I'm afraid the error for people using MongoDB<3.4 won't be very helpful. When trying to record a Decimal128 into a MongoDB 3.2 instance, I get:

    raise AutoReconnect("connection closed")
    pymongo.errors.AutoReconnect: connection closed

I have no idea how to do better.

@touilleMan, thoughts ? Any objection to releasing this as 1.2.0?

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index dd179a6..91931a6 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -13,3 +13,4 @@ Contributors
 * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
 * Imblc `@imbolc <https://github.com/imbolc>`_
 * `@patlach42 <https://github.com/patlach42>`_
+* Serj Shevchenko `@serjshevchenko <https://github.com/serjshevchenko>`_
diff --git a/umongo/fields.py b/umongo/fields.py
index d8693df..35f177a 100644
--- a/umongo/fields.py
+++ b/umongo/fields.py
@@ -2,7 +2,7 @@ from datetime import datetime
 
 from marshmallow import ValidationError, missing
 from marshmallow import fields as ma_fields
-from bson import DBRef, ObjectId
+from bson import DBRef, ObjectId, Decimal128
 
 # from .registerer import retrieve_document
 from .exceptions import NotRegisteredDocumentError
@@ -152,7 +152,14 @@ class IntegerField(BaseField, ma_fields.Integer):
 
 
 class DecimalField(BaseField, ma_fields.Decimal):
-    pass
+    """
+    MongoDB version support (3.4+).
+    """
+    def _serialize_to_mongo(self, obj):
+        return Decimal128(obj)
+
+    def _deserialize_from_mongo(self, value):
+        return value.to_decimal()
 
 
 class BooleanField(BaseField, ma_fields.Boolean):


Test patch:
diff --git a/tests/test_data_proxy.py b/tests/test_data_proxy.py
index 7029db2..37f1ec9 100644
--- a/tests/test_data_proxy.py
+++ b/tests/test_data_proxy.py
@@ -435,6 +435,28 @@ class TestDataProxy(BaseTest):
         with pytest.raises(exceptions.UnknownFieldInDBError):
             d.from_mongo({'mongo_field': 42, 'xxx': 'foo'})
 
+    def test_iterators(self):
+        class MySchema(EmbeddedSchema):
+            field_a = fields.IntField(attribute='mongo_field_a')
+            field_b = fields.IntField(attribute='mongo_field_b')
+
+        DataProxy = data_proxy_factory('My', MySchema())
+        d = DataProxy()
+        d.from_mongo({'mongo_field_a': 42, 'mongo_field_b': 24})
+
+        assert set(d.keys()) == {'mongo_field_a', 'mongo_field_b'}
+        assert set(d.keys_by_mongo_name()) == {'mongo_field_a', 'mongo_field_b'}
+        assert set(d.values()) == {42, 24}
+        assert set(d.items()) == {('field_a', 42), ('field_b', 24)}
+        assert set(d.items_by_mongo_name()) == {('mongo_field_a', 42), ('mongo_field_b', 24)}
+
+        d.load({'field_a': 100, 'field_b': 200})
+        assert set(d.keys()) == {'mongo_field_a', 'mongo_field_b'}
+        assert set(d.keys_by_mongo_name()) == {'mongo_field_a', 'mongo_field_b'}
+        assert set(d.values()) == {100, 200}
+        assert set(d.items()) == {('field_a', 100), ('field_b', 200)}
+        assert set(d.items_by_mongo_name()) == {('mongo_field_a', 100), ('mongo_field_b', 200)}
+
 
 class TestNonStrictDataProxy(BaseTest):
 
diff --git a/tests/test_embedded_document.py b/tests/test_embedded_document.py
index 9715069..35a273e 100644
--- a/tests/test_embedded_document.py
+++ b/tests/test_embedded_document.py
@@ -245,6 +245,9 @@ class TestEmbeddedDocument(BaseTest):
         assert doc.parent.to_mongo() == {'d': 2, '_cls': 'GrandChild'}
 
     def test_abstract_inheritance(self):
+        class AlienClass:
+            pass
+
         @self.instance.register
         class AbstractParent(EmbeddedDocument):
             a = fields.IntField(attribute='in_mongo_a_parent')
@@ -262,7 +265,7 @@ class TestEmbeddedDocument(BaseTest):
                 abstract = True
 
         @self.instance.register
-        class ConcreteChild(AbstractParent):
+        class ConcreteChild(AbstractParent, AlienClass):
             c = fields.IntField()
 
             class Meta:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 9d4950f..f1e6cbc 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,4 +1,6 @@
-from bson import ObjectId, DBRef
+from decimal import Decimal
+
+from bson import ObjectId, DBRef, Decimal128
 import pytest
 from datetime import datetime
 from dateutil.tz.tz import tzutc, tzoffset
@@ -255,6 +257,9 @@ class TestFields(BaseTest):
         assert d3.to_mongo(update=True) == {'$set': {'in_mongo_dict': {'field': 'value'}}}
         assert d3.to_mongo() == {'in_mongo_dict': {'field': 'value'}}
 
+        d3.from_mongo({'in_mongo_dict': {}})
+        assert d3._data.get('in_mongo_dict') == {}
+
         d4 = MyDataProxy({'dict': None})
         assert d4.to_mongo() == {'in_mongo_dict': None}
         d4.from_mongo({'in_mongo_dict': None})
@@ -312,6 +317,11 @@ class TestFields(BaseTest):
         assert d.dump() == {'list': [2, 3, 4, 5]}
         assert d.to_mongo(update=True) == {'$set': {'in_mongo_list': [2, 3, 4, 5]}}
 
+        d.from_mongo({'in_mongo_list': [2, 3, 4, 5]})
+        assert repr(
+            d._data.get('in_mongo_list')
+        ) == '<object umongo.data_objects.List([2, 3, 4, 5])>'
+
         d2 = MyDataProxy()
         d2.from_mongo({})
         assert isinstance(d2.get('list'), List)
@@ -329,6 +339,11 @@ class TestFields(BaseTest):
         d3.from_mongo({'in_mongo_list': None})
         assert d3.get('list') is None
 
+        d3.from_mongo({'in_mongo_list': []})
+        assert repr(
+            d3._data.get('in_mongo_list')
+        ) == '<object umongo.data_objects.List([])>'
+
     def test_complexe_list(self):
 
         @self.instance.register
@@ -570,3 +585,54 @@ class TestFields(BaseTest):
         d3.from_mongo({
             'in_mongo_gref': Reference(ToRef1, ObjectId("5672d47b1d41c88dcd37ef05"))})
         assert not isinstance(d3._data['in_mongo_gref'].pk, Reference)
+
+    def test_decimal(self):
+
+        class MySchema(Schema):
+            price = fields.DecimalField(attribute='in_mongo_price')
+
+        MyDataProxy = data_proxy_factory('My', MySchema())
+        d = MyDataProxy()
+        d.load({'price': Decimal128('12.5678')})
+        assert d.dump() == {'price': Decimal('12.5678')}
+        assert d.to_mongo() == {'in_mongo_price': Decimal128('12.5678')}
+        assert d.get('price') == Decimal('12.5678')
+
+        d.load({'price': Decimal('12.5678')})
+        assert d.dump() == {'price': Decimal('12.5678')}
+        assert d.to_mongo() == {'in_mongo_price': Decimal128("12.5678")}
+        assert d.get('price') == Decimal('12.5678')
+
+        d.load({'price': '12.5678'})
+        assert d.dump() == {'price': Decimal('12.5678')}
+        assert d.to_mongo() == {'in_mongo_price': Decimal128("12.5678")}
+        assert d.get('price') == Decimal('12.5678')
+
+        d.load({'price': float('12.5678')})
+        assert d.dump() == {'price': Decimal('12.5678')}
+        assert d.to_mongo() == {'in_mongo_price': Decimal128("12.5678")}
+        assert d.get('price') == Decimal('12.5678')
+
+        d.set('price', Decimal128('11.1234'))
+        assert d.to_mongo(update=True) == {
+            '$set': {'in_mongo_price': Decimal128('11.1234')}}
+
+        d.set('price', Decimal("10.1234"))
+        assert d.to_mongo(update=True) == {
+            '$set': {'in_mongo_price': Decimal128('10.1234')}}
+
+        d.set('price', "9.1234")
+        assert d.to_mongo(update=True) == {
+            '$set': {'in_mongo_price': Decimal128('9.1234')}}
+
+        d.set('price', 8.1234)
+        assert d.to_mongo(update=True) == {
+            '$set': {'in_mongo_price': Decimal128('8.1234')}}
+
+        d.from_mongo({'in_mongo_price': Decimal128('7.1234')})
+        assert d._data == {
+            'in_mongo_price': Decimal('7.1234')
+        }
+
+        with pytest.raises(ValidationError):
+            d.set('price', 'str')