Repository: Scille/umongo
Pull request #172 (2019-03-07 16:22:30)

Round datetimes to milliseconds in DateTimeField and StrictDateTimeField
The point of this change is to ensure consistency in the representation of the data before and after storing it to and retrieving it from the DB.

Basically, if the datetime is not rounded when deserialized in the object, the object has more precision than the database can handle. When committing the object in the DB, then fetching it back, precision is lost.

It is more consistent to lose the precision right away.

Real life use case:

REST API, POST request. The resource view function instantiates an object with a datetime with microsecond precision. The object is saved in DB. The view function returns the object. The representation of the object does not match what is stored in DB. A subsequent GET request will return a slightly different object. Things get even worse when ETag comes into play. On the next PUT request,  the ETag calculated on the POST return value does not match the ETag calculated on the DB content and the resource can't be modified.


Patch:
diff --git a/umongo/fields.py b/umongo/fields.py
index f0cc78f..7b109c4 100644
--- a/umongo/fields.py
+++ b/umongo/fields.py
@@ -193,8 +193,12 @@ class DateTimeField(BaseField, ma_fields.DateTime):
 
     def _deserialize(self, value, attr, data):
         if isinstance(value, datetime):
-            return value
-        return super()._deserialize(value, attr, data)
+            ret = value
+        else:
+            ret = super()._deserialize(value, attr, data)
+        # MongoDB stores datetimes with a millisecond precision.
+        # Don't keep more precision in the object than in the database.
+        return ret.replace(microsecond=round(ret.microsecond, -3))
 
 
 class LocalDateTimeField(BaseField, ma_fields.LocalDateTime):
@@ -242,8 +246,12 @@ class StrictDateTimeField(BaseField, ma_bonus_fields.StrictDateTime):
 
     def _deserialize(self, value, attr, data):
         if isinstance(value, datetime):
-            return self._set_tz_awareness(value)
-        return super()._deserialize(value, attr, data)
+            ret = self._set_tz_awareness(value)
+        else:
+            ret = super()._deserialize(value, attr, data)
+        # MongoDB stores datetimes with a millisecond precision.
+        # Don't keep more precision in the object than in the database.
+        return ret.replace(microsecond=round(ret.microsecond, -3))
 
     def _deserialize_from_mongo(self, value):
         return self._set_tz_awareness(value)


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 6c4c4e2..c0cb704 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -161,6 +161,11 @@ class TestFields(BaseTest):
         with pytest.raises(ValidationError):
             s.load({'a': "dummy"})
 
+        # Test DateTimeField rounds to milliseconds
+        s = MySchema()
+        data, _ = s.load({'a': datetime(2016, 8, 6, 12, 30, 30, 123456)})
+        assert data['a'].microsecond == 123000
+
     def test_strictdatetime(self):
 
         class MySchema(EmbeddedSchema):
@@ -215,6 +220,11 @@ class TestFields(BaseTest):
             assert d.get('b') == datetime(2016, 8, 5, 22, 0)
             assert d.get('c') == datetime(2016, 8, 6, tzinfo=tzoffset(None, 7200))
 
+        # Test StrictDateTimeField rounds to milliseconds
+        s = MySchema()
+        data, _ = s.load({'a': datetime(2016, 8, 6, 12, 30, 30, 123456)})
+        assert data['a'].microsecond == 123000
+
     def test_dict(self):
 
         class MySchema(Schema):