From d75d83defdbcc2e498f816b5cd20dcc24a1b7138 Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 11:30:42 -0700 Subject: [PATCH 1/7] Add UTs for #2372 test_encode_aware_datetime() fails for non-UTC timezones due to the bug. --- tests/test_helpers.py | 10 ++++++++++ tox.ini | 1 + 2 files changed, 11 insertions(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index c66e650b68..9e6797466f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -23,6 +23,7 @@ from werkzeug.http import http_date from flask._compat import StringIO, text_type from flask.helpers import get_debug_flag, make_response +from pytz import timezone def has_encoding(name): @@ -177,6 +178,15 @@ def test_jsonify_date_types(self, app, client): assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) + @pytest.mark.parametrize('tzname', ('UTC', 'PST8PDT', 'Asia/Seoul')) + def test_jsonify_aware_datetimes(self, tzname): + """Test if aware datetime.datetime objects are converted into GMT.""" + dt_naive = datetime.datetime(2017, 1, 1, 12, 34, 56) + dt_aware = timezone(tzname).localize(dt_naive) + dt_as_gmt = dt_aware.astimezone(timezone('GMT')) + expected = dt_as_gmt.strftime('"%a, %d %b %Y %H:%M:%S %Z"') + assert flask.json.JSONEncoder().encode(dt_aware) == expected + def test_jsonify_uuid_types(self, app, client): """Test jsonify with uuid.UUID types""" diff --git a/tox.ini b/tox.ini index cb6dd342a3..a0b4ad6609 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ deps = coverage greenlet blinker + pytz lowest: Werkzeug==0.9 lowest: Jinja2==2.4 From d41e2e6a5db3211ecd3c3f3f55934474406ffac2 Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 11:12:11 -0700 Subject: [PATCH 2/7] Correctly encode aware, non-UTC datetime objects http_date() requires timetuple in UTC, but JSONEncoder.default() was passing a local timetuple instead. --- CHANGES | 2 ++ flask/json/__init__.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 59e8c4c1e7..f37dfc54e1 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`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1621: https://github.com/pallets/flask/pull/1621 @@ -102,6 +103,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 Version 0.12.2 -------------- diff --git a/flask/json/__init__.py b/flask/json/__init__.py index 93e6fdc46c..6559c1aa74 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 @@ def default(self, o): 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): From 0e6cab357690614791ab4ca0da0ac65dbb803041 Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 13:06:26 -0700 Subject: [PATCH 3/7] Rewrite test_jsonify_aware_datetimes without pytz --- tests/test_helpers.py | 15 +++++++-------- tox.ini | 1 - 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 9e6797466f..b1418b812c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -23,7 +23,6 @@ from werkzeug.http import http_date from flask._compat import StringIO, text_type from flask.helpers import get_debug_flag, make_response -from pytz import timezone def has_encoding(name): @@ -178,14 +177,14 @@ def test_jsonify_date_types(self, app, client): assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) - @pytest.mark.parametrize('tzname', ('UTC', 'PST8PDT', 'Asia/Seoul')) - def test_jsonify_aware_datetimes(self, tzname): + @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.""" - dt_naive = datetime.datetime(2017, 1, 1, 12, 34, 56) - dt_aware = timezone(tzname).localize(dt_naive) - dt_as_gmt = dt_aware.astimezone(timezone('GMT')) - expected = dt_as_gmt.strftime('"%a, %d %b %Y %H:%M:%S %Z"') - assert flask.json.JSONEncoder().encode(dt_aware) == expected + tzinfo = datetime.timezone(datetime.timedelta(hours=tz[1]), name=tz[0]) + dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) + gmt = datetime.timezone(datetime.timedelta(), 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""" diff --git a/tox.ini b/tox.ini index a0b4ad6609..cb6dd342a3 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,6 @@ deps = coverage greenlet blinker - pytz lowest: Werkzeug==0.9 lowest: Jinja2==2.4 From eb9618347c680a038e2e6310228d85a53b080f93 Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 13:57:40 -0700 Subject: [PATCH 4/7] Use pytz again for tests This is because datetime.timezone is Python 3 only. The only alternative would be to hand-spin a datetime.tzinfo subclass, an overkill. This reverts commit 0e6cab357690614791ab4ca0da0ac65dbb803041. --- tests/test_helpers.py | 15 ++++++++------- tox.ini | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b1418b812c..9e6797466f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -23,6 +23,7 @@ from werkzeug.http import http_date from flask._compat import StringIO, text_type from flask.helpers import get_debug_flag, make_response +from pytz import timezone def has_encoding(name): @@ -177,14 +178,14 @@ def test_jsonify_date_types(self, app, client): 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): + @pytest.mark.parametrize('tzname', ('UTC', 'PST8PDT', 'Asia/Seoul')) + def test_jsonify_aware_datetimes(self, tzname): """Test if aware datetime.datetime objects are converted into GMT.""" - tzinfo = datetime.timezone(datetime.timedelta(hours=tz[1]), name=tz[0]) - dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) - gmt = datetime.timezone(datetime.timedelta(), name='GMT') - expected = dt.astimezone(gmt).strftime('"%a, %d %b %Y %H:%M:%S %Z"') - assert flask.json.JSONEncoder().encode(dt) == expected + dt_naive = datetime.datetime(2017, 1, 1, 12, 34, 56) + dt_aware = timezone(tzname).localize(dt_naive) + dt_as_gmt = dt_aware.astimezone(timezone('GMT')) + expected = dt_as_gmt.strftime('"%a, %d %b %Y %H:%M:%S %Z"') + assert flask.json.JSONEncoder().encode(dt_aware) == expected def test_jsonify_uuid_types(self, app, client): """Test jsonify with uuid.UUID types""" diff --git a/tox.ini b/tox.ini index cb6dd342a3..a0b4ad6609 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ deps = coverage greenlet blinker + pytz lowest: Werkzeug==0.9 lowest: Jinja2==2.4 From 34050630d67918dc8614b8ead8c683e66e4ececc Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 14:08:42 -0700 Subject: [PATCH 5/7] Skip aware datetime tests if pytz is unavailable --- tests/test_helpers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 9e6797466f..313427d604 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -23,7 +23,12 @@ from werkzeug.http import http_date from flask._compat import StringIO, text_type from flask.helpers import get_debug_flag, make_response -from pytz import timezone +try: + from pytz import timezone +except ImportError: + has_pytz = False +else: + has_pytz = True def has_encoding(name): @@ -178,6 +183,7 @@ def test_jsonify_date_types(self, app, client): assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) + @pytest.mark.skipif('not has_pytz') @pytest.mark.parametrize('tzname', ('UTC', 'PST8PDT', 'Asia/Seoul')) def test_jsonify_aware_datetimes(self, tzname): """Test if aware datetime.datetime objects are converted into GMT.""" From f80376027571ba5190ab6cf1411dd1dd9bb9c65e Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 14:14:18 -0700 Subject: [PATCH 6/7] Re-revert to not using pytz Will spin a tzinfo subclass. --- tests/test_helpers.py | 21 +++++++-------------- tox.ini | 1 - 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 313427d604..b1418b812c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -23,12 +23,6 @@ from werkzeug.http import http_date from flask._compat import StringIO, text_type from flask.helpers import get_debug_flag, make_response -try: - from pytz import timezone -except ImportError: - has_pytz = False -else: - has_pytz = True def has_encoding(name): @@ -183,15 +177,14 @@ def test_jsonify_date_types(self, app, client): assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) - @pytest.mark.skipif('not has_pytz') - @pytest.mark.parametrize('tzname', ('UTC', 'PST8PDT', 'Asia/Seoul')) - def test_jsonify_aware_datetimes(self, tzname): + @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.""" - dt_naive = datetime.datetime(2017, 1, 1, 12, 34, 56) - dt_aware = timezone(tzname).localize(dt_naive) - dt_as_gmt = dt_aware.astimezone(timezone('GMT')) - expected = dt_as_gmt.strftime('"%a, %d %b %Y %H:%M:%S %Z"') - assert flask.json.JSONEncoder().encode(dt_aware) == expected + tzinfo = datetime.timezone(datetime.timedelta(hours=tz[1]), name=tz[0]) + dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) + gmt = datetime.timezone(datetime.timedelta(), 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""" diff --git a/tox.ini b/tox.ini index a0b4ad6609..cb6dd342a3 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,6 @@ deps = coverage greenlet blinker - pytz lowest: Werkzeug==0.9 lowest: Jinja2==2.4 From 63ccdada1b9ddc69f97b00de1ac343894c1d8420 Mon Sep 17 00:00:00 2001 From: "Eugene M. Kim" Date: Wed, 14 Jun 2017 14:23:13 -0700 Subject: [PATCH 7/7] Actually hand-spin and use a tzinfo subclass This is for Python 2.x compatibility. Suggested-by: David Lord --- tests/test_helpers.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b1418b812c..73c6bee728 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', @@ -180,9 +201,9 @@ def test_jsonify_date_types(self, app, client): @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 = datetime.timezone(datetime.timedelta(hours=tz[1]), name=tz[0]) + tzinfo = FixedOffset(hours=tz[1], name=tz[0]) dt = datetime.datetime(2017, 1, 1, 12, 34, 56, tzinfo=tzinfo) - gmt = datetime.timezone(datetime.timedelta(), name='GMT') + 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