Repository: marshmallow-code/marshmallow
Pull request #1308 (2019-07-17 21:59:15)

Adjusted documentation for Regexp validator method
I added a brief comment indicating that the Regexp method uses re's match() function. Knowing that match() is used for the validator could help newcomers write better regular expression validators.



Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 192fedbf..e2f4f54d 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -132,3 +132,4 @@ Contributors (chronological)
 - Kostas Konstantopoulos `@kdop <https://github.com/kdop>`_
 - Stephen J. Fuhry `@fuhrysteve <https://github.com/fuhrysteve>`_
 - `@dursk <https://github.com/dursk>`_
+- Ezra MacDonald `@kdop <https://github.com/macdonaldezra>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a05acd32..c3036721 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,20 +4,14 @@ Changelog
 3.0.0 (unreleased)
 ++++++++++++++++++
 
-Features:
-
-- *Backwards-incompatible*: ``DateTime`` does not affect timezone information
-  on serialization and deserialization.
-- Add ``NaiveDateTime`` and ``AwareDateTime`` to enforce timezone awareness.
-
-Deprecations/Removals:
-
-- ``LocalDateTime`` is removed.
-
 Bug fixes:
 
 - Fix behavior of `List(Nested("self"))` (`#779 (comment) <https://github.com/marshmallow-code/marshmallow/issues/779#issuecomment-396354987>`_).
 
+Support:
+
+- Document usage of  `validate.Regexp`'s usage `re.search` (:issue:`1285`). Thanks :user:`macdonaldezra`.
+
 3.0.0rc8 (2019-07-04)
 +++++++++++++++++++++
 
@@ -33,7 +27,7 @@ Features:
 - Improve error messages for ``validate.Range``.
 - Use ``raise from exc`` for better stack traces (:pr:`1254`). Thanks
   :user:`fuhrysteve`.
-- python-dateutil is no longer used. This resolves the inconsistent behavior
+- python-dateutil is no longer used. This resolves the inconsistent behavior 
   based on the presence of python-dateutil (:issue:`497`, :issue:`1234`).
 
 Bug fixes:
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index 8a827dbd..b87b59dd 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -788,44 +788,6 @@ The ``Meta`` option ``dateformat`` used to pass format to `DateTime <marshmallow
     MySchema().dump({"x": dt.datetime(2017, 9, 19), "y": dt.date(2017, 9, 19)})
     # => {{'x': '2017-09', 'y': '09-19'}}
 
-``DateTime`` leaves timezone information untouched during serialization
-***********************************************************************
-
-``DateTime`` does not convert naive datetimes to UTC on serialization and
-``LocalDateTime`` is removed.
-
-.. code-block:: python
-
-    # 2.x
-    class MySchema(Schema):
-        x = fields.DateTime()
-        y = fields.DateTime()
-        z = fields.LocalDateTime()
-
-
-    MySchema().dump(
-        {
-            "x": dt.datetime(2017, 9, 19),
-            "y": dt.datetime(2017, 9, 19, tzinfo=dt.timezone(dt.timedelta(hours=2))),
-            "z": dt.datetime(2017, 9, 19, tzinfo=dt.timezone(dt.timedelta(hours=2))),
-        }
-    )
-    # => {{'x': '2017-09-19T00:00:00+00:00', 'y': '2017-09-18T22:00:00+00:00', 'z': '2017-09-19T00:00:00+02:00'}}
-
-    # 3.x
-    class MySchema(Schema):
-        x = fields.DateTime()
-        y = fields.DateTime()
-
-
-    MySchema().dump(
-        {
-            "x": dt.datetime(2017, 9, 19),
-            "y": dt.datetime(2017, 9, 19, tzinfo=dt.timezone(dt.timedelta(hours=2))),
-        }
-    )
-    # => {{'x': '2017-09-19T00:00:00', 'y': '2017-09-19T00:00:00+02:00'}}
-
 The ``prefix`` ``Schema`` parameter is removed
 **********************************************
 
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index e48a5305..69f7f2d0 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -11,12 +11,7 @@ from collections.abc import Mapping as _Mapping
 
 from marshmallow import validate, utils, class_registry
 from marshmallow.base import FieldABC, SchemaABC
-from marshmallow.utils import (
-    is_collection,
-    missing as missing_,
-    resolve_field_instance,
-    is_aware,
-)
+from marshmallow.utils import is_collection, missing as missing_, resolve_field_instance
 from marshmallow.exceptions import (
     ValidationError,
     StringNotCollectionError,
@@ -40,8 +35,7 @@ __all__ = [
     "Boolean",
     "Float",
     "DateTime",
-    "NaiveDateTime",
-    "AwareDateTime",
+    "LocalDateTime",
     "Time",
     "Date",
     "TimeDelta",
@@ -1033,13 +1027,19 @@ class Boolean(Field):
 
 
 class DateTime(Field):
-    """A formatted datetime string.
+    """A formatted datetime string in UTC.
 
     Example: ``'2014-12-22T03:12:58.019077+00:00'``
 
+    Timezone-naive `datetime` objects are converted to
+    UTC (+00:00) by :meth:`Schema.dump <marshmallow.Schema.dump>`.
+    :meth:`Schema.load <marshmallow.Schema.load>` returns `datetime`
+    objects that are timezone-aware.
+
     :param str format: Either ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601),
         or a date format string. If `None`, defaults to "iso".
     :param kwargs: The same keyword arguments that :class:`Field` receives.
+
     """
 
     SERIALIZATION_FUNCS = {
@@ -1062,9 +1062,9 @@ class DateTime(Field):
 
     SCHEMA_OPTS_VAR_NAME = "datetimeformat"
 
+    localtime = False
     default_error_messages = {
         "invalid": "Not a valid {obj_type}.",
-        "invalid_awareness": "Not a valid {awareness} {obj_type}.",
         "format": '"{input}" cannot be formatted as a {obj_type}.',
     }
 
@@ -1090,7 +1090,7 @@ class DateTime(Field):
         format_func = self.SERIALIZATION_FUNCS.get(data_format)
         if format_func:
             try:
-                return format_func(value)
+                return format_func(value, localtime=self.localtime)
             except (TypeError, AttributeError, ValueError):
                 self.fail("format", input=value, obj_type=self.OBJ_TYPE)
         else:
@@ -1121,63 +1121,15 @@ class DateTime(Field):
         return dt.datetime.strptime(value, data_format)
 
 
-class NaiveDateTime(DateTime):
-    """A formatted naive datetime string.
-
-    :param str format: See :class:`DateTime`.
-    :param timezone timezone: Used on deserialization. If `None`,
-        aware datetimes are rejected. If not `None`, aware datetimes are
-        converted to this timezone before their timezone information is
-        removed.
-    :param kwargs: The same keyword arguments that :class:`Field` receives.
-    """
-
-    AWARENESS = "naive"
+class LocalDateTime(DateTime):
+    """A formatted datetime string in localized time, relative to UTC.
 
-    def __init__(self, format=None, *, timezone=None, **kwargs):
-        super().__init__(format=format, **kwargs)
-        self.timezone = timezone
+        ex. ``"Sun, 10 Nov 2013 08:23:45 -0600"``
 
-    def _deserialize(self, value, attr, data, **kwargs):
-        ret = super()._deserialize(value, attr, data, **kwargs)
-        if is_aware(ret):
-            if self.timezone is None:
-                self.fail(
-                    "invalid_awareness",
-                    awareness=self.AWARENESS,
-                    obj_type=self.OBJ_TYPE,
-                )
-            ret = ret.astimezone(self.timezone).replace(tzinfo=None)
-        return ret
-
-
-class AwareDateTime(DateTime):
-    """A formatted aware datetime string.
-
-    :param str format: See :class:`DateTime`.
-    :param timezone default_timezone: Used on deserialization. If `None`, naive
-        datetimes are rejected. If not `None`, naive datetimes are set this
-        timezone.
-    :param kwargs: The same keyword arguments that :class:`Field` receives.
+    Takes the same arguments as :class:`DateTime <marshmallow.fields.DateTime>`.
     """
 
-    AWARENESS = "aware"
-
-    def __init__(self, format=None, *, default_timezone=None, **kwargs):
-        super().__init__(format=format, **kwargs)
-        self.default_timezone = default_timezone
-
-    def _deserialize(self, value, attr, data, **kwargs):
-        ret = super()._deserialize(value, attr, data, **kwargs)
-        if not is_aware(ret):
-            if self.default_timezone is None:
-                self.fail(
-                    "invalid_awareness",
-                    awareness=self.AWARENESS,
-                    obj_type=self.OBJ_TYPE,
-                )
-            ret = ret.replace(tzinfo=self.default_timezone)
-        return ret
+    localtime = True
 
 
 class Time(Field):
diff --git a/src/marshmallow/utils.py b/src/marshmallow/utils.py
index ce5d876e..7ee9e1f4 100644
--- a/src/marshmallow/utils.py
+++ b/src/marshmallow/utils.py
@@ -137,13 +137,6 @@ class UTC(dt.tzinfo):
 UTC = utc = UTC()  # UTC is a singleton
 
 
-# https://stackoverflow.com/a/27596917
-def is_aware(datetime):
-    return (
-        datetime.tzinfo is not None and datetime.tzinfo.utcoffset(datetime) is not None
-    )
-
-
 def from_rfc(datestring):
     """Parse a RFC822-formatted datetime string and return a datetime object.
 
@@ -152,11 +145,19 @@ def from_rfc(datestring):
     return parsedate_to_datetime(datestring)
 
 
-def rfcformat(datetime):
+def rfcformat(datetime, *, localtime=False):
     """Return the RFC822-formatted representation of a datetime object.
 
     :param datetime datetime: The datetime.
+    :param bool localtime: If ``True``, return the date relative to the local
+        timezone instead of UTC, displaying the proper offset,
+        e.g. "Sun, 10 Nov 2013 08:23:45 -0600"
     """
+    if localtime and datetime.tzinfo is None:
+        datetime = UTC.localize(datetime)
+    if not localtime and datetime.tzinfo is not None:
+        # Remove timezone to format as "-0000" rather than "+0000"
+        datetime = datetime.astimezone(UTC).replace(tzinfo=None)
     return format_datetime(datetime)
 
 
@@ -235,15 +236,19 @@ def from_iso_date(value):
     return dt.date(**kw)
 
 
-def isoformat(datetime):
-    """Return the ISO8601-formatted representation of a datetime object.
-
-    :param datetime datetime: The datetime.
-    """
-    return datetime.isoformat()
+def isoformat(datetime, *args, localtime=False, **kwargs):
+    """Return the ISO8601-formatted UTC representation of a datetime object."""
+    if localtime and datetime.tzinfo is not None:
+        localized = datetime
+    else:
+        if datetime.tzinfo is None:
+            localized = UTC.localize(datetime)
+        else:
+            localized = datetime.astimezone(UTC)
+    return localized.isoformat(*args, **kwargs)
 
 
-def to_iso_date(date):
+def to_iso_date(date, *args, **kwargs):
     return dt.date.isoformat(date)
 
 
diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py
index ab4a7b97..705c1ab1 100644
--- a/src/marshmallow/validate.py
+++ b/src/marshmallow/validate.py
@@ -334,7 +334,11 @@ class Equal(Validator):
 
 
 class Regexp(Validator):
-    """Validate ``value`` against the provided regex.
+    """Validator which succeeds if the ``value`` matches ``regex``.
+
+    .. note::
+
+        Uses `re.match`, which searches for a match at the beginning of a string.
 
     :param regex: The regular expression string to use. Can also be a compiled
         regular expression pattern.


Test patch:
diff --git a/tests/base.py b/tests/base.py
index e502a28e..4c3a9466 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -19,6 +19,7 @@ ALL_FIELDS = [
     fields.Float,
     fields.Number,
     fields.DateTime,
+    fields.LocalDateTime,
     fields.Time,
     fields.Date,
     fields.TimeDelta,
@@ -38,6 +39,14 @@ def assert_date_equal(d1, d2):
     assert d1.day == d2.day
 
 
+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):
     assert t1.hour == t2.hour
     assert t1.minute == t2.minute
@@ -154,6 +163,7 @@ class UserSchema(Schema):
     )
     created_iso = fields.DateTime(format="iso", attribute="created", dump_only=True)
     updated = fields.DateTime()
+    updated_local = fields.LocalDateTime(attribute="updated", dump_only=True)
     species = fields.String(attribute="SPECIES")
     id = fields.String(default="no-id")
     uppername = Uppercased(attribute="name", dump_only=True)
@@ -201,6 +211,7 @@ class UserMetaSchema(Schema):
     balance = fields.Decimal()
     is_old = fields.Method("get_is_old")
     lowername = fields.Function(get_lowername)
+    updated_local = fields.LocalDateTime(attribute="updated", dump_only=True)
     species = fields.String(attribute="SPECIES")
     homepage = fields.Url()
     email = fields.Email()
@@ -231,6 +242,7 @@ class UserMetaSchema(Schema):
             "balance",
             "is_old",
             "lowername",
+            "updated_local",
             "species",
             "registered",
             "hair_colors",
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index 3caad6b4..e9264e64 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -9,7 +9,13 @@ from marshmallow import EXCLUDE, INCLUDE, RAISE, fields, utils, Schema, validate
 from marshmallow.exceptions import ValidationError
 from marshmallow.validate import Equal
 
-from tests.base import assert_date_equal, assert_time_equal, central, ALL_FIELDS
+from tests.base import (
+    assert_date_equal,
+    assert_datetime_equal,
+    assert_time_equal,
+    central,
+    ALL_FIELDS,
+)
 
 
 class TestDeserializingNone:
@@ -393,7 +399,6 @@ class TestFieldDeserialization:
             42,
             "",
             [],
-            "2018",
             "2018-01-01",
             dt.datetime.now().strftime("%H:%M:%S %Y-%m-%d"),
             dt.datetime.now().strftime("%m-%d-%Y %H:%M:%S"),
@@ -404,160 +409,48 @@ class TestFieldDeserialization:
         with pytest.raises(ValidationError, match="Not a valid datetime."):
             field.deserialize(in_value)
 
+    def test_datetime_passed_year_is_invalid(self):
+        field = fields.DateTime()
+        with pytest.raises(ValidationError):
+            field.deserialize("1916")
+
+    def test_datetime_passed_date_is_invalid(self):
+        field = fields.DateTime()
+        with pytest.raises(ValidationError):
+            field.deserialize("2017-04-13")
+
     def test_custom_date_format_datetime_field_deserialization(self):
-        # Datetime string with format "%H:%M:%S.%f %Y-%m-%d"
-        datestring = "10:11:12.123456 2019-01-02"
+        dtime = dt.datetime.now()
+        datestring = dtime.strftime("%H:%M:%S.%f %Y-%m-%d")
 
-        # Deserialization should fail when datestring is not of same format
         field = fields.DateTime(format="%d-%m-%Y %H:%M:%S")
+        # deserialization should fail when datestring is not of same format
         with pytest.raises(ValidationError, match="Not a valid datetime."):
             field.deserialize(datestring)
-
         field = fields.DateTime(format="%H:%M:%S.%f %Y-%m-%d")
-        assert field.deserialize(datestring) == dt.datetime(
-            2019, 1, 2, 10, 11, 12, 123456
-        )
-
-        field = fields.NaiveDateTime(format="%H:%M:%S.%f %Y-%m-%d")
-        assert field.deserialize(datestring) == dt.datetime(
-            2019, 1, 2, 10, 11, 12, 123456
-        )
-
-        field = fields.AwareDateTime(format="%H:%M:%S.%f %Y-%m-%d")
-        with pytest.raises(ValidationError, match="Not a valid aware datetime."):
-            field.deserialize(datestring)
+        assert_datetime_equal(field.deserialize(datestring), dtime)
 
     @pytest.mark.parametrize("fmt", ["rfc", "rfc822"])
-    @pytest.mark.parametrize(
-        ("value", "expected", "aware"),
-        [
-            (
-                "Sun, 10 Nov 2013 01:23:45 -0000",
-                dt.datetime(2013, 11, 10, 1, 23, 45),
-                False,
-            ),
-            (
-                "Sun, 10 Nov 2013 01:23:45 +0000",
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-                True,
-            ),
-            (
-                "Sun, 10 Nov 2013 01:23:45 -0600",
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-                True,
-            ),
-        ],
-    )
-    def test_rfc_datetime_field_deserialization(self, fmt, value, expected, aware):
+    def test_rfc_datetime_field_deserialization(self, fmt):
+        dtime = dt.datetime.now().replace(microsecond=0)
+        datestring = utils.rfcformat(dtime)
         field = fields.DateTime(format=fmt)
-        assert field.deserialize(value) == expected
-        field = fields.NaiveDateTime(format=fmt)
-        if aware:
-            with pytest.raises(ValidationError, match="Not a valid naive datetime."):
-                field.deserialize(value)
-        else:
-            assert field.deserialize(value) == expected
-        field = fields.AwareDateTime(format=fmt)
-        if not aware:
-            with pytest.raises(ValidationError, match="Not a valid aware datetime."):
-                field.deserialize(value)
-        else:
-            assert field.deserialize(value) == expected
+        assert_datetime_equal(field.deserialize(datestring), dtime)
 
     @pytest.mark.parametrize("fmt", ["iso", "iso8601"])
-    @pytest.mark.parametrize(
-        ("value", "expected", "aware"),
-        [
-            ("2013-11-10T01:23:45", dt.datetime(2013, 11, 10, 1, 23, 45), False),
-            (
-                "2013-11-10T01:23:45+00:00",
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-                True,
-            ),
-            (
-                # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1251
-                "2013-11-10T01:23:45.123+00:00",
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45, 123000)),
-                True,
-            ),
-            (
-                "2013-11-10T01:23:45.123456+00:00",
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45, 123456)),
-                True,
-            ),
-            (
-                "2013-11-10T01:23:45-06:00",
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-                True,
-            ),
-        ],
-    )
-    def test_iso_datetime_field_deserialization(self, fmt, value, expected, aware):
+    def test_iso_datetime_field_deserialization(self, fmt):
+        dtime = dt.datetime.now()
+        datestring = dtime.isoformat()
         field = fields.DateTime(format=fmt)
-        assert field.deserialize(value) == expected
-        field = fields.NaiveDateTime(format=fmt)
-        if aware:
-            with pytest.raises(ValidationError, match="Not a valid naive datetime."):
-                field.deserialize(value)
-        else:
-            assert field.deserialize(value) == expected
-        field = fields.AwareDateTime(format=fmt)
-        if not aware:
-            with pytest.raises(ValidationError, match="Not a valid aware datetime."):
-                field.deserialize(value)
-        else:
-            assert field.deserialize(value) == expected
-
-    @pytest.mark.parametrize(
-        ("fmt", "timezone", "value", "expected"),
-        [
-            ("iso", None, "2013-11-10T01:23:45", dt.datetime(2013, 11, 10, 1, 23, 45)),
-            (
-                "iso",
-                utils.UTC,
-                "2013-11-10T01:23:45+00:00",
-                dt.datetime(2013, 11, 10, 1, 23, 45),
-            ),
-            (
-                "iso",
-                central,
-                "2013-11-10T01:23:45-03:00",
-                dt.datetime(2013, 11, 9, 22, 23, 45),
-            ),
-            (
-                "rfc",
-                None,
-                "Sun, 10 Nov 2013 01:23:45 -0000",
-                dt.datetime(2013, 11, 10, 1, 23, 45),
-            ),
-            (
-                "rfc",
-                utils.UTC,
-                "Sun, 10 Nov 2013 01:23:45 +0000",
-                dt.datetime(2013, 11, 10, 1, 23, 45),
-            ),
-            (
-                "rfc",
-                central,
-                "Sun, 10 Nov 2013 01:23:45 -0300",
-                dt.datetime(2013, 11, 9, 22, 23, 45),
-            ),
-        ],
-    )
-    def test_naive_datetime_with_timezone(self, fmt, timezone, value, expected):
-        field = fields.NaiveDateTime(format=fmt, timezone=timezone)
-        assert field.deserialize(value) == expected
+        assert_datetime_equal(field.deserialize(datestring), dtime)
 
-    @pytest.mark.parametrize("timezone", (utils.UTC, central))
-    @pytest.mark.parametrize(
-        ("fmt", "value"),
-        [("iso", "2013-11-10T01:23:45"), ("rfc", "Sun, 10 Nov 2013 01:23:45")],
-    )
-    def test_aware_datetime_default_timezone(self, fmt, timezone, value):
-        field = fields.AwareDateTime(format=fmt, default_timezone=timezone)
-        assert field.deserialize(value) == dt.datetime(
-            2013, 11, 10, 1, 23, 45, tzinfo=timezone
-        )
+    def test_localdatetime_field_deserialization(self):
+        dtime = dt.datetime.now()
+        localized_dtime = central.localize(dtime)
+        field = fields.DateTime(format="iso")
+        result = field.deserialize(localized_dtime.isoformat())
+        assert_datetime_equal(result, dtime)
+        assert result.tzinfo is not None
 
     def test_time_field_deserialization(self):
         field = fields.Time()
diff --git a/tests/test_schema.py b/tests/test_schema.py
index df4e940c..d41a5933 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -569,6 +569,11 @@ def test_tz_datetime_field(user, serialized_user):
     assert serialized_user["updated"] == expected
 
 
+def test_local_datetime_field(user, serialized_user):
+    expected = utils.isoformat(user.updated, localtime=True)
+    assert serialized_user["updated_local"] == expected
+
+
 def test_class_variable(serialized_user):
     assert serialized_user["species"] == "Homo sapiens"
 
@@ -1631,6 +1636,7 @@ def test_meta_serializer_fields():
     assert result["uppername"] == "JOHN"
     assert result["is_old"] is False
     assert result["created"] == utils.isoformat(u.created)
+    assert result["updated_local"] == utils.isoformat(u.updated, localtime=True)
     assert result["finger_count"] == 10
     assert result["various_data"] == dict(u.various_data)
 
@@ -1639,6 +1645,7 @@ def test_meta_fields_mapping(user):
     s = UserMetaSchema()
     s.dump(user)  # need to call dump to update fields
     assert type(s.fields["balance"]) == fields.Decimal
+    assert type(s.fields["updated_local"]) == fields.LocalDateTime
     # Inferred fields
     assert type(s.fields["name"]._field_cache[fields.String]) == fields.String
     assert type(s.fields["created"]._field_cache[fields.DateTime]) == fields.DateTime
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index c0793b75..6d24c6d6 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -11,7 +11,7 @@ import pytest
 from marshmallow import Schema, fields, utils, missing as missing_
 from marshmallow.exceptions import ValidationError
 
-from tests.base import User, ALL_FIELDS, central
+from tests.base import User, ALL_FIELDS
 
 
 class DateTimeList:
@@ -498,6 +498,11 @@ class TestFieldSerialization:
             "Years": 999,
         }
 
+    def test_datetime_serializes_to_iso_by_default(self, user):
+        field = fields.DateTime()  # No format specified
+        expected = utils.isoformat(user.created, localtime=False)
+        assert field.serialize("created", user) == expected
+
     @pytest.mark.parametrize("value", ["invalid", [], 24])
     def test_datetime_invalid_serialization(self, value, user):
         field = fields.DateTime()
@@ -510,52 +515,28 @@ class TestFieldSerialization:
         ] == '"{}" cannot be formatted as a datetime.'.format(value)
 
     @pytest.mark.parametrize("fmt", ["rfc", "rfc822"])
-    @pytest.mark.parametrize(
-        ("value", "expected"),
-        [
-            (dt.datetime(2013, 11, 10, 1, 23, 45), "Sun, 10 Nov 2013 01:23:45 -0000"),
-            (
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-                "Sun, 10 Nov 2013 01:23:45 +0000",
-            ),
-            (
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-                "Sun, 10 Nov 2013 01:23:45 -0600",
-            ),
-        ],
-    )
-    def test_datetime_field_rfc822(self, fmt, value, expected):
+    def test_datetime_field_rfc822(self, fmt, user):
         field = fields.DateTime(format=fmt)
-        assert field.serialize("d", {"d": value}) == expected
+        expected = utils.rfcformat(user.created, localtime=False)
+        assert field.serialize("created", user) == expected
 
-    @pytest.mark.parametrize("fmt", ["iso", "iso8601", None])
-    @pytest.mark.parametrize(
-        ("value", "expected"),
-        [
-            (dt.datetime(2013, 11, 10, 1, 23, 45), "2013-11-10T01:23:45"),
-            (
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45, 123456)),
-                "2013-11-10T01:23:45.123456+00:00",
-            ),
-            (
-                utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-                "2013-11-10T01:23:45+00:00",
-            ),
-            (
-                central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-                "2013-11-10T01:23:45-06:00",
-            ),
-        ],
-    )
-    def test_datetime_field_iso8601(self, fmt, value, expected):
-        if fmt is None:
-            # Test default is ISO
-            field = fields.DateTime()
-        else:
-            field = fields.DateTime(format=fmt)
-        assert field.serialize("d", {"d": value}) == expected
+    def test_localdatetime_rfc_field(self, user):
+        field = fields.LocalDateTime(format="rfc")
+        expected = utils.rfcformat(user.created, localtime=True)
+        assert field.serialize("created", user) == expected
+
+    @pytest.mark.parametrize("fmt", ["iso", "iso8601"])
+    def test_datetime_iso8601(self, fmt, user):
+        field = fields.DateTime(format=fmt)
+        expected = utils.isoformat(user.created, localtime=False)
+        assert field.serialize("created", user) == expected
+
+    def test_localdatetime_iso(self, user):
+        field = fields.LocalDateTime(format="iso")
+        expected = utils.isoformat(user.created, localtime=True)
+        assert field.serialize("created", user) == expected
 
-    def test_datetime_field_format(self, user):
+    def test_datetime_format(self, user):
         format = "%Y-%m-%d"
         field = fields.DateTime(format=format)
         assert field.serialize("created", user) == user.created.strftime(format)
@@ -870,7 +851,7 @@ class TestSchemaSerialization:
         data = {"name": "Mick"}
         result = AliasingUserSerializer().dump(data)
         assert result["name"] == "Mick"
-        assert result["birthdate"] == "2017-09-29T00:00:00"
+        assert result["birthdate"] == "2017-09-29T00:00:00+00:00"
 
     def test_serialize_with_missing_param_callable(self):
         class AliasingUserSerializer(Schema):
@@ -880,7 +861,7 @@ class TestSchemaSerialization:
         data = {"name": "Mick"}
         result = AliasingUserSerializer().dump(data)
         assert result["name"] == "Mick"
-        assert result["birthdate"] == "2017-09-29T00:00:00"
+        assert result["birthdate"] == "2017-09-29T00:00:00+00:00"
 
 
 def test_serializing_named_tuple():
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e211b1f6..b688d23a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -6,7 +6,12 @@ from copy import copy, deepcopy
 import pytest
 
 from marshmallow import utils, fields, Schema
-from tests.base import central, assert_time_equal, assert_date_equal
+from tests.base import (
+    assert_datetime_equal,
+    central,
+    assert_time_equal,
+    assert_date_equal,
+)
 
 
 def test_missing_singleton_copy():
@@ -115,93 +120,73 @@ def test_is_collection():
     assert utils.is_collection({"foo": "bar"}) is False
 
 
-@pytest.mark.parametrize(
-    ("value", "expected"),
-    [
-        (dt.datetime(2013, 11, 10, 1, 23, 45), "Sun, 10 Nov 2013 01:23:45 -0000"),
-        (
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-            "Sun, 10 Nov 2013 01:23:45 +0000",
-        ),
-        (
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-            "Sun, 10 Nov 2013 01:23:45 -0600",
-        ),
-    ],
-)
-def test_rfc_format(value, expected):
-    assert utils.rfcformat(value) == expected
+def test_rfcformat_gmt_naive():
+    d = dt.datetime(2013, 11, 10, 1, 23, 45)
+    assert utils.rfcformat(d) == "Sun, 10 Nov 2013 01:23:45 -0000"
 
 
-@pytest.mark.parametrize(
-    ("value", "expected"),
-    [
-        (dt.datetime(2013, 11, 10, 1, 23, 45), "2013-11-10T01:23:45"),
-        (
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45, 123456)),
-            "2013-11-10T01:23:45.123456+00:00",
-        ),
-        (
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-            "2013-11-10T01:23:45+00:00",
-        ),
-        (
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-            "2013-11-10T01:23:45-06:00",
-        ),
-    ],
-)
-def test_isoformat(value, expected):
-    assert utils.isoformat(value) == expected
+def test_rfcformat_central():
+    d = central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False)
+    assert utils.rfcformat(d) == "Sun, 10 Nov 2013 07:23:45 -0000"
 
 
-@pytest.mark.parametrize(
-    ("value", "expected"),
-    [
-        ("Sun, 10 Nov 2013 01:23:45 -0000", dt.datetime(2013, 11, 10, 1, 23, 45)),
-        (
-            "Sun, 10 Nov 2013 01:23:45 +0000",
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-        ),
-        (
-            "Sun, 10 Nov 2013 01:23:45 -0600",
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-        ),
-    ],
-)
-def test_from_rfc(value, expected):
-    result = utils.from_rfc(value)
+def test_rfcformat_naive_localized():
+    d = dt.datetime(2013, 11, 10, 1, 23, 45)
+    assert utils.rfcformat(d, localtime=True) == "Sun, 10 Nov 2013 01:23:45 +0000"
+
+
+def test_rfcformat_central_localized():
+    d = central.localize(dt.datetime(2013, 11, 10, 8, 23, 45), is_dst=False)
+    assert utils.rfcformat(d, localtime=True) == "Sun, 10 Nov 2013 08:23:45 -0600"
+
+
+def test_isoformat():
+    d = dt.datetime(2013, 11, 10, 1, 23, 45)
+    assert utils.isoformat(d) == "2013-11-10T01:23:45+00:00"
+
+
+def test_isoformat_tzaware():
+    d = central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False)
+    assert utils.isoformat(d) == "2013-11-10T07:23:45+00:00"
+
+
+def test_isoformat_localtime():
+    d = central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False)
+    assert utils.isoformat(d, localtime=True) == "2013-11-10T01:23:45-06:00"
+
+
+def test_from_rfc():
+    d = dt.datetime.now().replace(microsecond=0)
+    rfc = utils.rfcformat(d)
+    result = utils.from_rfc(rfc)
     assert type(result) == dt.datetime
-    assert result == expected
+    assert_datetime_equal(result, d)
 
 
-@pytest.mark.parametrize(
-    ("value", "expected"),
-    [
-        ("2013-11-10T01:23:45", dt.datetime(2013, 11, 10, 1, 23, 45)),
-        (
-            "2013-11-10T01:23:45+00:00",
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45)),
-        ),
-        (
-            # Regression test for https://github.com/marshmallow-code/marshmallow/issues/1251
-            "2013-11-10T01:23:45.123+00:00",
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45, 123000)),
-        ),
-        (
-            "2013-11-10T01:23:45.123456+00:00",
-            utils.UTC.localize(dt.datetime(2013, 11, 10, 1, 23, 45, 123456)),
-        ),
-        (
-            "2013-11-10T01:23:45-06:00",
-            central.localize(dt.datetime(2013, 11, 10, 1, 23, 45), is_dst=False),
-        ),
-    ],
-)
-def test_from_iso_datetime(value, expected):
-    result = utils.from_iso_datetime(value)
+@pytest.mark.parametrize("timezone", [None, central])
+def test_from_iso_datetime(timezone):
+    d = dt.datetime.now(tz=timezone)
+    formatted = d.isoformat()
+    result = utils.from_iso_datetime(formatted)
+    assert type(result) == dt.datetime
+    assert_datetime_equal(result, d)
+
+    # Test with 3-digit only microseconds
+    #  Regression test for https://github.com/marshmallow-code/marshmallow/issues/1251
+    d = dt.datetime.now(tz=timezone).replace(microsecond=123000)
+    formatted = d.isoformat()
+    formatted = formatted[:23] + formatted[26:]
+    result = utils.from_iso_datetime(formatted)
     assert type(result) == dt.datetime
-    assert result == expected
+    assert_datetime_equal(result, d)
+
+
+def test_from_iso_with_tz():
+    d = central.localize(dt.datetime.now())
+    formatted = d.isoformat()
+    result = utils.from_iso_datetime(formatted)
+    assert_datetime_equal(result, d)
+    assert result.tzinfo is not None
 
 
 def test_from_iso_time_with_microseconds():