Repository: marshmallow-code/marshmallow

Issue:
DateTime loses microseconds on deserialization if dateutil is not installed
```py
fields.DateTime().deserialize('2019-02-22T17:56:43.820462')
datetime.datetime(2019, 2, 22, 17, 56, 43)
```

This is obvious in the last line of `from_iso_datetime`:

```py
def from_iso_datetime(datetimestring, use_dateutil=True):
    """Parse an ISO8601-formatted datetime string and return a datetime object.

    Use dateutil's parser if possible and return a timezone-aware datetime.
    """
    if not _iso8601_datetime_re.match(datetimestring):
        raise ValueError('Not a valid ISO8601-formatted datetime string')
    # Use dateutil's parser if possible
    if dateutil_available and use_dateutil:
        return parser.isoparse(datetimestring)
    else:
        # Strip off timezone info.
        return datetime.datetime.strptime(datetimestring[:19], '%Y-%m-%dT%H:%M:%S')
```

The case is addressed in `from_iso_time`:

```py
def from_iso_time(timestring, use_dateutil=True):
    """Parse an ISO8601-formatted datetime string and return a datetime.time
    object.
    """
    if not _iso8601_time_re.match(timestring):
        raise ValueError('Not a valid ISO8601-formatted time string')
    if dateutil_available and use_dateutil:
        return parser.parse(timestring).time()
    else:
        if len(timestring) > 8:  # has microseconds
            fmt = '%H:%M:%S.%f'
        else:
            fmt = '%H:%M:%S'
        return datetime.datetime.strptime(timestring, fmt).time()
```

It is easier because there is no timezone info.

`from_iso_datetime` should be able to discriminate TZ info from microseconds.

I noticed this because an unrelated test fails if python-dateutil is not installed.

Discussion:

Patch:
diff --git a/src/marshmallow/utils.py b/src/marshmallow/utils.py
index 42458a6f..113906d6 100755
--- a/src/marshmallow/utils.py
+++ b/src/marshmallow/utils.py
@@ -283,6 +283,9 @@ def from_iso(datestring, use_dateutil=True):
         return parser.parse(datestring)
     else:
         # Strip off timezone info.
+        if '.' in datestring:
+            # datestring contains microseconds
+            return datetime.datetime.strptime(datestring[:26], '%Y-%m-%dT%H:%M:%S.%f')
         return datetime.datetime.strptime(datestring[:19], '%Y-%m-%dT%H:%M:%S')
 
 


Test patch:
diff --git a/tests/base.py b/tests/base.py
index 0e4b16c4..d57ad310 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -49,14 +49,15 @@ def assert_datetime_equal(dt1, dt2):
     assert_date_equal(dt1, dt2)
     assert dt1.hour == dt2.hour
     assert dt1.minute == dt2.minute
+    assert dt1.second == dt2.second
+    assert dt1.microsecond == dt2.microsecond
 
 
-def assert_time_equal(t1, t2, microseconds=True):
+def assert_time_equal(t1, t2):
     assert t1.hour == t2.hour
     assert t1.minute == t2.minute
     assert t1.second == t2.second
-    if microseconds:
-        assert t1.microsecond == t2.microsecond
+    assert t1.microsecond == t2.microsecond
 
 
 ##### Models #####
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index a879398f..01d5a637 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -324,15 +324,15 @@ class TestFieldDeserialization:
     def test_custom_date_format_datetime_field_deserialization(self):
 
         dtime = dt.datetime.now()
-        datestring = dtime.strftime('%H:%M:%S %Y-%m-%d')
+        datestring = dtime.strftime('%H:%M:%S.%f %Y-%m-%d')
 
         field = fields.DateTime(format='%d-%m-%Y %H:%M:%S')
         #deserialization should fail when datestring is not of same format
         with pytest.raises(ValidationError) as excinfo:
             field.deserialize(datestring)
-        msg = 'Not a valid datetime.'.format(datestring)
+        msg = 'Not a valid datetime.'
         assert msg in str(excinfo)
-        field = fields.DateTime(format='%H:%M:%S %Y-%m-%d')
+        field = fields.DateTime(format='%H:%M:%S.%f %Y-%m-%d')
         assert_datetime_equal(field.deserialize(datestring), dtime)
 
         field = fields.DateTime()
@@ -340,7 +340,7 @@ class TestFieldDeserialization:
 
     @pytest.mark.parametrize('fmt', ['rfc', 'rfc822'])
     def test_rfc_datetime_field_deserialization(self, fmt):
-        dtime = dt.datetime.now()
+        dtime = dt.datetime.now().replace(microsecond=0)
         datestring = utils.rfcformat(dtime)
         field = fields.DateTime(format=fmt)
         assert_datetime_equal(field.deserialize(datestring), dtime)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 75ec9ea2..a5d68621 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -185,15 +185,16 @@ def test_from_datestring():
 
 @pytest.mark.parametrize('use_dateutil', [True, False])
 def test_from_rfc(use_dateutil):
-    d = dt.datetime.now()
+    d = dt.datetime.now().replace(microsecond=0)
     rfc = utils.rfcformat(d)
     result = utils.from_rfc(rfc, use_dateutil=use_dateutil)
     assert type(result) == dt.datetime
     assert_datetime_equal(result, d)
 
 @pytest.mark.parametrize('use_dateutil', [True, False])
-def test_from_iso(use_dateutil):
-    d = dt.datetime.now()
+@pytest.mark.parametrize('timezone', [None, central])
+def test_from_iso_datetime(use_dateutil, timezone):
+    d = dt.datetime.now(tz=timezone)
     formatted = d.isoformat()
     result = utils.from_iso(formatted, use_dateutil=use_dateutil)
     assert type(result) == dt.datetime
@@ -215,7 +216,7 @@ def test_from_iso_time_with_microseconds(use_dateutil):
     formatted = t.isoformat()
     result = utils.from_iso_time(formatted, use_dateutil=use_dateutil)
     assert type(result) == dt.time
-    assert_time_equal(result, t, microseconds=True)
+    assert_time_equal(result, t)
 
 @pytest.mark.parametrize('use_dateutil', [True, False])
 def test_from_iso_time_without_microseconds(use_dateutil):
@@ -223,7 +224,7 @@ def test_from_iso_time_without_microseconds(use_dateutil):
     formatted = t.isoformat()
     result = utils.from_iso_time(formatted, use_dateutil=use_dateutil)
     assert type(result) == dt.time
-    assert_time_equal(result, t, microseconds=True)
+    assert_time_equal(result, t)
 
 @pytest.mark.parametrize('use_dateutil', [True, False])
 def test_from_iso_date(use_dateutil):