diff --git a/CHANGES b/CHANGES index 7f9e968b..f0cb9723 100644 --- a/CHANGES +++ b/CHANGES @@ -79,6 +79,7 @@ Major release, unreleased - Removed error handler caching because it caused unexpected results for some exception inheritance hierarchies. Register handlers explicitly for each exception if you don't want to traverse the MRO. (`#2362`_) +- Fix incorrect JSON encoding of aware, non-UTC datetimes. (`#2374`_) - Template auto reloading will honor the ``run`` command's ``debug`` flag even if ``app.jinja_env`` was already accessed. (`#2373`_) @@ -104,6 +105,7 @@ Major release, unreleased .. _#2354: https://github.com/pallets/flask/pull/2354 .. _#2358: https://github.com/pallets/flask/pull/2358 .. _#2362: https://github.com/pallets/flask/pull/2362 +.. _#2374: https://github.com/pallets/flask/pull/2374 .. _#2373: https://github.com/pallets/flask/pull/2373 Version 0.12.2 diff --git a/flask/json/__init__.py b/flask/json/__init__.py index 93e6fdc4..6559c1aa 100644 --- a/flask/json/__init__.py +++ b/flask/json/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import io import uuid -from datetime import date +from datetime import date, datetime from flask.globals import current_app, request from flask._compat import text_type, PY2 @@ -62,6 +62,8 @@ class JSONEncoder(_json.JSONEncoder): return list(iterable) return JSONEncoder.default(self, o) """ + if isinstance(o, datetime): + return http_date(o.utctimetuple()) if isinstance(o, date): return http_date(o.timetuple()) if isinstance(o, uuid.UUID): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index c66e650b..73c6bee7 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -34,6 +34,27 @@ def has_encoding(name): return False +class FixedOffset(datetime.tzinfo): + """Fixed offset in hours east from UTC. + + This is a slight adaptation of the ``FixedOffset`` example found in + https://docs.python.org/2.7/library/datetime.html. + """ + + def __init__(self, hours, name): + self.__offset = datetime.timedelta(hours=hours) + self.__name = name + + def utcoffset(self, dt): + return self.__offset + + def tzname(self, dt): + return self.__name + + def dst(self, dt): + return datetime.timedelta() + + class TestJSON(object): def test_ignore_cached_json(self, app): with app.test_request_context('/', method='POST', data='malformed', @@ -177,6 +198,15 @@ class TestJSON(object): assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) + @pytest.mark.parametrize('tz', (('UTC', 0), ('PST', -8), ('KST', 9))) + def test_jsonify_aware_datetimes(self, tz): + """Test if aware datetime.datetime objects are converted into GMT.""" + tzinfo = FixedOffset(hours=tz[1], name=tz[0]) + dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) + gmt = FixedOffset(hours=0, name='GMT') + expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"') + assert flask.json.JSONEncoder().encode(dt) == expected + def test_jsonify_uuid_types(self, app, client): """Test jsonify with uuid.UUID types"""