Skip to content

[3.8] bpo-37642: Update max and min offset in datetime module (GH-14878) #15227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Lib/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -2269,7 +2269,7 @@ def fromutc(self, dt):
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
Expand All @@ -2293,8 +2293,11 @@ def _name_from_offset(delta):
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
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,31 @@ def test_deepcopy(self):
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
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -1873,3 +1873,4 @@ Geoff Shannon
Batuhan Taskaya
Aleksandr Balezin
Robert Leenders
Ngalim Siregar
Original file line number Diff line number Diff line change
@@ -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
11 changes: 9 additions & 2 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down