From 27b38b99b3a154fa5c25cd67fe01fb4fc04604b0 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 15 Aug 2019 15:08:57 -0400 Subject: [PATCH] bpo-37642: Update acceptable offsets in timezone (GH-14878) (#15227) This fixes an inconsistency between the Python and C implementations of the datetime module. The pure python version of the code was not accepting offsets greater than 23:59 but less than 24:00. This is an accidental legacy of the original implementation, which was put in place before tzinfo allowed sub-minute time zone offsets. GH-14878 (cherry picked from commit 92c7e30adf5c81a54d6e5e555a6bdfaa60157a0d) --- Lib/datetime.py | 9 ++++--- Lib/test/datetimetester.py | 25 +++++++++++++++++++ Misc/ACKS | 1 + .../2019-07-21-20-59-31.bpo-37642.L61Bvy.rst | 3 +++ Modules/_datetimemodule.c | 11 ++++++-- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-07-21-20-59-31.bpo-37642.L61Bvy.rst diff --git a/Lib/datetime.py b/Lib/datetime.py index d4c7a1ff90..0adf1dd67d 100644 --- a/Lib/datetime.py +++ b/Lib/datetime.py @@ -2269,7 +2269,7 @@ class timezone(tzinfo): raise TypeError("fromutc() argument must be a datetime instance" " or None") - _maxoffset = timedelta(hours=23, minutes=59) + _maxoffset = timedelta(hours=24, microseconds=-1) _minoffset = -_maxoffset @staticmethod @@ -2293,8 +2293,11 @@ class timezone(tzinfo): return f'UTC{sign}{hours:02d}:{minutes:02d}' timezone.utc = timezone._create(timedelta(0)) -timezone.min = timezone._create(timezone._minoffset) -timezone.max = timezone._create(timezone._maxoffset) +# bpo-37642: These attributes are rounded to the nearest minute for backwards +# compatibility, even though the constructor will accept a wider range of +# values. This may change in the future. +timezone.min = timezone._create(-timedelta(hours=23, minutes=59)) +timezone.max = timezone._create(timedelta(hours=23, minutes=59)) _EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) # Some time zone algebra. For a datetime x, let diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 99b620ce2f..d0101c98bc 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -388,6 +388,31 @@ class TestTimeZone(unittest.TestCase): tz_copy = copy.deepcopy(tz) self.assertIs(tz_copy, tz) + def test_offset_boundaries(self): + # Test timedeltas close to the boundaries + time_deltas = [ + timedelta(hours=23, minutes=59), + timedelta(hours=23, minutes=59, seconds=59), + timedelta(hours=23, minutes=59, seconds=59, microseconds=999999), + ] + time_deltas.extend([-delta for delta in time_deltas]) + + for delta in time_deltas: + with self.subTest(test_type='good', delta=delta): + timezone(delta) + + # Test timedeltas on and outside the boundaries + bad_time_deltas = [ + timedelta(hours=24), + timedelta(hours=24, microseconds=1), + ] + bad_time_deltas.extend([-delta for delta in bad_time_deltas]) + + for delta in bad_time_deltas: + with self.subTest(test_type='bad', delta=delta): + with self.assertRaises(ValueError): + timezone(delta) + ############################################################################# # Base class for testing a particular aspect of timedelta, time, date and diff --git a/Misc/ACKS b/Misc/ACKS index 311259f1e5..ab874e9299 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1874,3 +1874,4 @@ Geoff Shannon Batuhan Taskaya Aleksandr Balezin Robert Leenders +Ngalim Siregar diff --git a/Misc/NEWS.d/next/Library/2019-07-21-20-59-31.bpo-37642.L61Bvy.rst b/Misc/NEWS.d/next/Library/2019-07-21-20-59-31.bpo-37642.L61Bvy.rst new file mode 100644 index 0000000000..09ff257597 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-07-21-20-59-31.bpo-37642.L61Bvy.rst @@ -0,0 +1,3 @@ +Allowed the pure Python implementation of :class:`datetime.timezone` to represent +sub-minute offsets close to minimum and maximum boundaries, specifically in the +ranges (23:59, 24:00) and (-23:59, 24:00). Patch by Ngalim Siregar diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 80ecfc3fff..beb1c3cfbc 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1099,7 +1099,9 @@ new_timezone(PyObject *offset, PyObject *name) Py_INCREF(PyDateTime_TimeZone_UTC); return PyDateTime_TimeZone_UTC; } - if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) || + if ((GET_TD_DAYS(offset) == -1 && + GET_TD_SECONDS(offset) == 0 && + GET_TD_MICROSECONDS(offset) < 1) || GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) { PyErr_Format(PyExc_ValueError, "offset must be a timedelta" " strictly between -timedelta(hours=24) and" @@ -1169,7 +1171,9 @@ call_tzinfo_method(PyObject *tzinfo, const char *name, PyObject *tzinfoarg) if (offset == Py_None || offset == NULL) return offset; if (PyDelta_Check(offset)) { - if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) || + if ((GET_TD_DAYS(offset) == -1 && + GET_TD_SECONDS(offset) == 0 && + GET_TD_MICROSECONDS(offset) < 1) || GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) { Py_DECREF(offset); PyErr_Format(PyExc_ValueError, "offset must be a timedelta" @@ -6484,6 +6488,9 @@ PyInit__datetime(void) PyDateTime_TimeZone_UTC = x; CAPI.TimeZone_UTC = PyDateTime_TimeZone_UTC; + /* bpo-37642: These attributes are rounded to the nearest minute for backwards + * compatibility, even though the constructor will accept a wider range of + * values. This may change in the future.*/ delta = new_delta(-1, 60, 0, 1); /* -23:59 */ if (delta == NULL) return NULL; -- 2.50.1