Commit 018d353c authored by Alexander Belopolsky's avatar Alexander Belopolsky Committed by GitHub

Closes issue bpo-5288: Allow tzinfo objects with sub-minute offsets. (#2896)

* Closes issue bpo-5288: Allow tzinfo objects with sub-minute offsets.

* bpo-5288: Implemented %z formatting of sub-minute offsets.

* bpo-5288: Removed mentions of the whole minute limitation on TZ offsets.

* bpo-5288: Removed one more mention of the whole minute limitation.

Thanks @csabella!

* Fix a formatting error in the docs

* Addressed review comments.

Thanks, @haypo.
parent c6ea8974
...@@ -1071,16 +1071,20 @@ Instance methods: ...@@ -1071,16 +1071,20 @@ Instance methods:
If :attr:`.tzinfo` is ``None``, returns ``None``, else returns If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
``self.tzinfo.utcoffset(self)``, and raises an exception if the latter doesn't ``self.tzinfo.utcoffset(self)``, and raises an exception if the latter doesn't
return ``None``, or a :class:`timedelta` object representing a whole number of return ``None`` or a :class:`timedelta` object with magnitude less than one day.
minutes with magnitude less than one day.
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.
.. method:: datetime.dst() .. method:: datetime.dst()
If :attr:`.tzinfo` is ``None``, returns ``None``, else returns If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
``self.tzinfo.dst(self)``, and raises an exception if the latter doesn't return ``self.tzinfo.dst(self)``, and raises an exception if the latter doesn't return
``None``, or a :class:`timedelta` object representing a whole number of minutes ``None`` or a :class:`timedelta` object with magnitude less than one day.
with magnitude less than one day.
.. versionchanged:: 3.7
The DST offset is not restricted to a whole number of minutes.
.. method:: datetime.tzname() .. method:: datetime.tzname()
...@@ -1562,17 +1566,20 @@ Instance methods: ...@@ -1562,17 +1566,20 @@ Instance methods:
If :attr:`.tzinfo` is ``None``, returns ``None``, else returns If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
``self.tzinfo.utcoffset(None)``, and raises an exception if the latter doesn't ``self.tzinfo.utcoffset(None)``, and raises an exception if the latter doesn't
return ``None`` or a :class:`timedelta` object representing a whole number of return ``None`` or a :class:`timedelta` object with magnitude less than one day.
minutes with magnitude less than one day.
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.
.. method:: time.dst() .. method:: time.dst()
If :attr:`.tzinfo` is ``None``, returns ``None``, else returns If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
``self.tzinfo.dst(None)``, and raises an exception if the latter doesn't return ``self.tzinfo.dst(None)``, and raises an exception if the latter doesn't return
``None``, or a :class:`timedelta` object representing a whole number of minutes ``None``, or a :class:`timedelta` object with magnitude less than one day.
with magnitude less than one day.
.. versionchanged:: 3.7
The DST offset is not restricted to a whole number of minutes.
.. method:: time.tzname() .. method:: time.tzname()
...@@ -1641,13 +1648,14 @@ Example: ...@@ -1641,13 +1648,14 @@ Example:
.. method:: tzinfo.utcoffset(dt) .. method:: tzinfo.utcoffset(dt)
Return offset of local time from UTC, in minutes east of UTC. If local time is Return offset of local time from UTC, as a :class:`timedelta` object that is
positive east of UTC. If local time is
west of UTC, this should be negative. Note that this is intended to be the west of UTC, this should be negative. Note that this is intended to be the
total offset from UTC; for example, if a :class:`tzinfo` object represents both total offset from UTC; for example, if a :class:`tzinfo` object represents both
time zone and DST adjustments, :meth:`utcoffset` should return their sum. If time zone and DST adjustments, :meth:`utcoffset` should return their sum. If
the UTC offset isn't known, return ``None``. Else the value returned must be a the UTC offset isn't known, return ``None``. Else the value returned must be a
:class:`timedelta` object specifying a whole number of minutes in the range :class:`timedelta` object strictly between ``-timedelta(hours=24)`` and
-1439 to 1439 inclusive (1440 = 24\*60; the magnitude of the offset must be less ``timedelta(hours=24)`` (the magnitude of the offset must be less
than one day). Most implementations of :meth:`utcoffset` will probably look than one day). Most implementations of :meth:`utcoffset` will probably look
like one of these two:: like one of these two::
...@@ -1660,10 +1668,14 @@ Example: ...@@ -1660,10 +1668,14 @@ Example:
The default implementation of :meth:`utcoffset` raises The default implementation of :meth:`utcoffset` raises
:exc:`NotImplementedError`. :exc:`NotImplementedError`.
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.
.. method:: tzinfo.dst(dt) .. method:: tzinfo.dst(dt)
Return the daylight saving time (DST) adjustment, in minutes east of UTC, or Return the daylight saving time (DST) adjustment, as a :class:`timedelta`
object or
``None`` if DST information isn't known. Return ``timedelta(0)`` if DST is not ``None`` if DST information isn't known. Return ``timedelta(0)`` if DST is not
in effect. If DST is in effect, return the offset as a :class:`timedelta` object in effect. If DST is in effect, return the offset as a :class:`timedelta` object
(see :meth:`utcoffset` for details). Note that DST offset, if applicable, has (see :meth:`utcoffset` for details). Note that DST offset, if applicable, has
...@@ -1708,6 +1720,9 @@ Example: ...@@ -1708,6 +1720,9 @@ Example:
The default implementation of :meth:`dst` raises :exc:`NotImplementedError`. The default implementation of :meth:`dst` raises :exc:`NotImplementedError`.
.. versionchanged:: 3.7
The DST offset is not restricted to a whole number of minutes.
.. method:: tzinfo.tzname(dt) .. method:: tzinfo.tzname(dt)
...@@ -1887,14 +1902,17 @@ made to civil time. ...@@ -1887,14 +1902,17 @@ made to civil time.
The *offset* argument must be specified as a :class:`timedelta` The *offset* argument must be specified as a :class:`timedelta`
object representing the difference between the local time and UTC. It must object representing the difference between the local time and UTC. It must
be strictly between ``-timedelta(hours=24)`` and be strictly between ``-timedelta(hours=24)`` and
``timedelta(hours=24)`` and represent a whole number of minutes, ``timedelta(hours=24)``, otherwise :exc:`ValueError` is raised.
otherwise :exc:`ValueError` is raised.
The *name* argument is optional. If specified it must be a string that The *name* argument is optional. If specified it must be a string that
will be used as the value returned by the :meth:`datetime.tzname` method. will be used as the value returned by the :meth:`datetime.tzname` method.
.. versionadded:: 3.2 .. versionadded:: 3.2
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.
.. method:: timezone.utcoffset(dt) .. method:: timezone.utcoffset(dt)
Return the fixed value specified when the :class:`timezone` instance is Return the fixed value specified when the :class:`timezone` instance is
...@@ -1902,6 +1920,9 @@ made to civil time. ...@@ -1902,6 +1920,9 @@ made to civil time.
:class:`timedelta` instance equal to the difference between the :class:`timedelta` instance equal to the difference between the
local time and UTC. local time and UTC.
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.
.. method:: timezone.tzname(dt) .. method:: timezone.tzname(dt)
Return the fixed value specified when the :class:`timezone` instance Return the fixed value specified when the :class:`timezone` instance
...@@ -2025,8 +2046,8 @@ format codes. ...@@ -2025,8 +2046,8 @@ format codes.
| | number, zero-padded on the | 999999 | | | | number, zero-padded on the | 999999 | |
| | left. | | | | | left. | | |
+-----------+--------------------------------+------------------------+-------+ +-----------+--------------------------------+------------------------+-------+
| ``%z`` | UTC offset in the form +HHMM | (empty), +0000, -0400, | \(6) | | ``%z`` | UTC offset in the form | (empty), +0000, -0400, | \(6) |
| | or -HHMM (empty string if the | +1030 | | | | ±HHMM[SS] (empty string if the | +1030 | |
| | object is naive). | | | | | object is naive). | | |
+-----------+--------------------------------+------------------------+-------+ +-----------+--------------------------------+------------------------+-------+
| ``%Z`` | Time zone name (empty string | (empty), UTC, EST, CST | | | ``%Z`` | Time zone name (empty string | (empty), UTC, EST, CST | |
...@@ -2139,12 +2160,19 @@ Notes: ...@@ -2139,12 +2160,19 @@ Notes:
For an aware object: For an aware object:
``%z`` ``%z``
:meth:`utcoffset` is transformed into a 5-character string of the form :meth:`utcoffset` is transformed into a string of the form
+HHMM or -HHMM, where HH is a 2-digit string giving the number of UTC ±HHMM[SS[.uuuuuu]], where HH is a 2-digit string giving the number of UTC
offset hours, and MM is a 2-digit string giving the number of UTC offset offset hours, and MM is a 2-digit string giving the number of UTC offset
minutes. For example, if :meth:`utcoffset` returns minutes, SS is a 2-digit string string giving the number of UTC offset
``timedelta(hours=-3, minutes=-30)``, ``%z`` is replaced with the string seconds and uuuuuu is a 2-digit string string giving the number of UTC
``'-0330'``. offset microseconds. The uuuuuu part is omitted when the offset is a
whole number of minutes and both the uuuuuu and the SS parts are omitted
when the offset is a whole number of minutes. For example, if
:meth:`utcoffset` returns ``timedelta(hours=-3, minutes=-30)``, ``%z`` is
replaced with the string ``'-0330'``.
.. versionchanged:: 3.7
The UTC offset is not restricted to a whole number of minutes.
``%Z`` ``%Z``
If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
......
...@@ -206,9 +206,15 @@ def _wrap_strftime(object, format, timetuple): ...@@ -206,9 +206,15 @@ def _wrap_strftime(object, format, timetuple):
if offset.days < 0: if offset.days < 0:
offset = -offset offset = -offset
sign = '-' sign = '-'
h, m = divmod(offset, timedelta(hours=1)) h, rest = divmod(offset, timedelta(hours=1))
assert not m % timedelta(minutes=1), "whole minute" m, rest = divmod(rest, timedelta(minutes=1))
m //= timedelta(minutes=1) s = rest.seconds
u = offset.microseconds
if u:
zreplace = '%c%02d%02d%02d.%06d' % (sign, h, m, s, u)
elif s:
zreplace = '%c%02d%02d%02d' % (sign, h, m, s)
else:
zreplace = '%c%02d%02d' % (sign, h, m) zreplace = '%c%02d%02d' % (sign, h, m)
assert '%' not in zreplace assert '%' not in zreplace
newformat.append(zreplace) newformat.append(zreplace)
...@@ -241,7 +247,7 @@ def _check_tzname(name): ...@@ -241,7 +247,7 @@ def _check_tzname(name):
# offset is what it returned. # offset is what it returned.
# If offset isn't None or timedelta, raises TypeError. # If offset isn't None or timedelta, raises TypeError.
# If offset is None, returns None. # If offset is None, returns None.
# Else offset is checked for being in range, and a whole # of minutes. # Else offset is checked for being in range.
# If it is, its integer value is returned. Else ValueError is raised. # If it is, its integer value is returned. Else ValueError is raised.
def _check_utc_offset(name, offset): def _check_utc_offset(name, offset):
assert name in ("utcoffset", "dst") assert name in ("utcoffset", "dst")
...@@ -250,9 +256,6 @@ def _check_utc_offset(name, offset): ...@@ -250,9 +256,6 @@ def _check_utc_offset(name, offset):
if not isinstance(offset, timedelta): if not isinstance(offset, timedelta):
raise TypeError("tzinfo.%s() must return None " raise TypeError("tzinfo.%s() must return None "
"or timedelta, not '%s'" % (name, type(offset))) "or timedelta, not '%s'" % (name, type(offset)))
if offset.microseconds:
raise ValueError("tzinfo.%s() must return a whole number "
"of seconds, got %s" % (name, offset))
if not -timedelta(1) < offset < timedelta(1): if not -timedelta(1) < offset < timedelta(1):
raise ValueError("%s()=%s, must be strictly between " raise ValueError("%s()=%s, must be strictly between "
"-timedelta(hours=24) and timedelta(hours=24)" % "-timedelta(hours=24) and timedelta(hours=24)" %
...@@ -960,11 +963,11 @@ class tzinfo: ...@@ -960,11 +963,11 @@ class tzinfo:
raise NotImplementedError("tzinfo subclass must override tzname()") raise NotImplementedError("tzinfo subclass must override tzname()")
def utcoffset(self, dt): def utcoffset(self, dt):
"datetime -> minutes east of UTC (negative for west of UTC)" "datetime -> timedelta, positive for east of UTC, negative for west of UTC"
raise NotImplementedError("tzinfo subclass must override utcoffset()") raise NotImplementedError("tzinfo subclass must override utcoffset()")
def dst(self, dt): def dst(self, dt):
"""datetime -> DST offset in minutes east of UTC. """datetime -> DST offset as timedelta, positive for east of UTC.
Return 0 if DST not in effect. utcoffset() must include the DST Return 0 if DST not in effect. utcoffset() must include the DST
offset. offset.
...@@ -1262,8 +1265,8 @@ class time: ...@@ -1262,8 +1265,8 @@ class time:
# Timezone functions # Timezone functions
def utcoffset(self): def utcoffset(self):
"""Return the timezone offset in minutes east of UTC (negative west of """Return the timezone offset as timedelta, positive east of UTC
UTC).""" (negative west of UTC)."""
if self._tzinfo is None: if self._tzinfo is None:
return None return None
offset = self._tzinfo.utcoffset(None) offset = self._tzinfo.utcoffset(None)
...@@ -1284,8 +1287,8 @@ class time: ...@@ -1284,8 +1287,8 @@ class time:
return name return name
def dst(self): def dst(self):
"""Return 0 if DST is not in effect, or the DST offset (in minutes """Return 0 if DST is not in effect, or the DST offset (as timedelta
eastward) if DST is in effect. positive eastward) if DST is in effect.
This is purely informational; the DST offset has already been added to This is purely informational; the DST offset has already been added to
the UTC offset returned by utcoffset() if applicable, so there's no the UTC offset returned by utcoffset() if applicable, so there's no
...@@ -1714,7 +1717,7 @@ class datetime(date): ...@@ -1714,7 +1717,7 @@ class datetime(date):
return _strptime._strptime_datetime(cls, date_string, format) return _strptime._strptime_datetime(cls, date_string, format)
def utcoffset(self): def utcoffset(self):
"""Return the timezone offset in minutes east of UTC (negative west of """Return the timezone offset as timedelta positive east of UTC (negative west of
UTC).""" UTC)."""
if self._tzinfo is None: if self._tzinfo is None:
return None return None
...@@ -1736,8 +1739,8 @@ class datetime(date): ...@@ -1736,8 +1739,8 @@ class datetime(date):
return name return name
def dst(self): def dst(self):
"""Return 0 if DST is not in effect, or the DST offset (in minutes """Return 0 if DST is not in effect, or the DST offset (as timedelta
eastward) if DST is in effect. positive eastward) if DST is in effect.
This is purely informational; the DST offset has already been added to This is purely informational; the DST offset has already been added to
the UTC offset returned by utcoffset() if applicable, so there's no the UTC offset returned by utcoffset() if applicable, so there's no
...@@ -1962,9 +1965,6 @@ class timezone(tzinfo): ...@@ -1962,9 +1965,6 @@ class timezone(tzinfo):
raise ValueError("offset must be a timedelta " raise ValueError("offset must be a timedelta "
"strictly between -timedelta(hours=24) and " "strictly between -timedelta(hours=24) and "
"timedelta(hours=24).") "timedelta(hours=24).")
if (offset.microseconds != 0 or offset.seconds % 60 != 0):
raise ValueError("offset must be a timedelta "
"representing a whole number of minutes")
return cls._create(offset, name) return cls._create(offset, name)
@classmethod @classmethod
...@@ -2053,8 +2053,15 @@ class timezone(tzinfo): ...@@ -2053,8 +2053,15 @@ class timezone(tzinfo):
else: else:
sign = '+' sign = '+'
hours, rest = divmod(delta, timedelta(hours=1)) hours, rest = divmod(delta, timedelta(hours=1))
minutes = rest // timedelta(minutes=1) minutes, rest = divmod(rest, timedelta(minutes=1))
return 'UTC{}{:02d}:{:02d}'.format(sign, hours, minutes) seconds = rest.seconds
microseconds = rest.microseconds
if microseconds:
return (f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}'
f'.{microseconds:06d}')
if seconds:
return f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}'
return f'UTC{sign}{hours:02d}:{minutes:02d}'
timezone.utc = timezone._create(timedelta(0)) timezone.utc = timezone._create(timedelta(0))
timezone.min = timezone._create(timezone._minoffset) timezone.min = timezone._create(timezone._minoffset)
......
...@@ -255,14 +255,15 @@ class TestTimeZone(unittest.TestCase): ...@@ -255,14 +255,15 @@ class TestTimeZone(unittest.TestCase):
self.assertEqual(timezone.min.utcoffset(None), -limit) self.assertEqual(timezone.min.utcoffset(None), -limit)
self.assertEqual(timezone.max.utcoffset(None), limit) self.assertEqual(timezone.max.utcoffset(None), limit)
def test_constructor(self): def test_constructor(self):
self.assertIs(timezone.utc, timezone(timedelta(0))) self.assertIs(timezone.utc, timezone(timedelta(0)))
self.assertIsNot(timezone.utc, timezone(timedelta(0), 'UTC')) self.assertIsNot(timezone.utc, timezone(timedelta(0), 'UTC'))
self.assertEqual(timezone.utc, timezone(timedelta(0), 'UTC')) self.assertEqual(timezone.utc, timezone(timedelta(0), 'UTC'))
for subminute in [timedelta(microseconds=1), timedelta(seconds=1)]:
tz = timezone(subminute)
self.assertNotEqual(tz.utcoffset(None) % timedelta(minutes=1), 0)
# invalid offsets # invalid offsets
for invalid in [timedelta(microseconds=1), timedelta(1, 1), for invalid in [timedelta(1, 1), timedelta(1)]:
timedelta(seconds=1), timedelta(1), -timedelta(1)]:
self.assertRaises(ValueError, timezone, invalid) self.assertRaises(ValueError, timezone, invalid)
self.assertRaises(ValueError, timezone, -invalid) self.assertRaises(ValueError, timezone, -invalid)
...@@ -301,6 +302,15 @@ class TestTimeZone(unittest.TestCase): ...@@ -301,6 +302,15 @@ class TestTimeZone(unittest.TestCase):
self.assertEqual('UTC-00:01', timezone(timedelta(minutes=-1)).tzname(None)) self.assertEqual('UTC-00:01', timezone(timedelta(minutes=-1)).tzname(None))
self.assertEqual('XYZ', timezone(-5 * HOUR, 'XYZ').tzname(None)) self.assertEqual('XYZ', timezone(-5 * HOUR, 'XYZ').tzname(None))
# Sub-minute offsets:
self.assertEqual('UTC+01:06:40', timezone(timedelta(0, 4000)).tzname(None))
self.assertEqual('UTC-01:06:40',
timezone(-timedelta(0, 4000)).tzname(None))
self.assertEqual('UTC+01:06:40.000001',
timezone(timedelta(0, 4000, 1)).tzname(None))
self.assertEqual('UTC-01:06:40.000001',
timezone(-timedelta(0, 4000, 1)).tzname(None))
with self.assertRaises(TypeError): self.EST.tzname('') with self.assertRaises(TypeError): self.EST.tzname('')
with self.assertRaises(TypeError): self.EST.tzname(5) with self.assertRaises(TypeError): self.EST.tzname(5)
...@@ -2152,6 +2162,9 @@ class TestDateTime(TestDate): ...@@ -2152,6 +2162,9 @@ class TestDateTime(TestDate):
t = self.theclass(2004, 12, 31, 6, 22, 33, 47) t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
self.assertEqual(t.strftime("%m %d %y %f %S %M %H %j"), self.assertEqual(t.strftime("%m %d %y %f %S %M %H %j"),
"12 31 04 000047 33 22 06 366") "12 31 04 000047 33 22 06 366")
tz = timezone(-timedelta(hours=2, seconds=33, microseconds=123))
t = t.replace(tzinfo=tz)
self.assertEqual(t.strftime("%z"), "-020033.000123")
def test_extract(self): def test_extract(self):
dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
...@@ -2717,8 +2730,8 @@ class TZInfoBase: ...@@ -2717,8 +2730,8 @@ class TZInfoBase:
def utcoffset(self, dt): return timedelta(microseconds=61) def utcoffset(self, dt): return timedelta(microseconds=61)
def dst(self, dt): return timedelta(microseconds=-81) def dst(self, dt): return timedelta(microseconds=-81)
t = cls(1, 1, 1, tzinfo=C7()) t = cls(1, 1, 1, tzinfo=C7())
self.assertRaises(ValueError, t.utcoffset) self.assertEqual(t.utcoffset(), timedelta(microseconds=61))
self.assertRaises(ValueError, t.dst) self.assertEqual(t.dst(), timedelta(microseconds=-81))
def test_aware_compare(self): def test_aware_compare(self):
cls = self.theclass cls = self.theclass
...@@ -4297,7 +4310,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): ...@@ -4297,7 +4310,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase):
self.assertEqual(gdt.strftime("%c %Z"), self.assertEqual(gdt.strftime("%c %Z"),
'Mon Jun 23 22:00:00 1941 UTC') 'Mon Jun 23 22:00:00 1941 UTC')
def test_constructors(self): def test_constructors(self):
t = time(0, fold=1) t = time(0, fold=1)
dt = datetime(1, 1, 1, fold=1) dt = datetime(1, 1, 1, fold=1)
...@@ -4372,7 +4384,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): ...@@ -4372,7 +4384,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase):
self.assertEqual(t0.fold, 0) self.assertEqual(t0.fold, 0)
self.assertEqual(t1.fold, 1) self.assertEqual(t1.fold, 1)
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
def test_timestamp(self): def test_timestamp(self):
dt0 = datetime(2014, 11, 2, 1, 30) dt0 = datetime(2014, 11, 2, 1, 30)
...@@ -4390,7 +4401,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): ...@@ -4390,7 +4401,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase):
s1 = t.replace(fold=1).timestamp() s1 = t.replace(fold=1).timestamp()
self.assertEqual(s0 + 1800, s1) self.assertEqual(s0 + 1800, s1)
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
def test_astimezone(self): def test_astimezone(self):
dt0 = datetime(2014, 11, 2, 1, 30) dt0 = datetime(2014, 11, 2, 1, 30)
...@@ -4406,7 +4416,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase): ...@@ -4406,7 +4416,6 @@ class TestLocalTimeDisambiguation(unittest.TestCase):
self.assertEqual(adt0.fold, 0) self.assertEqual(adt0.fold, 0)
self.assertEqual(adt1.fold, 0) self.assertEqual(adt1.fold, 0)
def test_pickle_fold(self): def test_pickle_fold(self):
t = time(fold=1) t = time(fold=1)
dt = datetime(1, 1, 1, fold=1) dt = datetime(1, 1, 1, fold=1)
......
Support tzinfo objects with sub-minute offsets.
...@@ -859,12 +859,6 @@ new_timezone(PyObject *offset, PyObject *name) ...@@ -859,12 +859,6 @@ new_timezone(PyObject *offset, PyObject *name)
Py_INCREF(PyDateTime_TimeZone_UTC); Py_INCREF(PyDateTime_TimeZone_UTC);
return PyDateTime_TimeZone_UTC; return PyDateTime_TimeZone_UTC;
} }
if (GET_TD_MICROSECONDS(offset) != 0 || GET_TD_SECONDS(offset) % 60 != 0) {
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
" representing a whole number of minutes,"
" not %R.", offset);
return NULL;
}
if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) || if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) ||
GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) { GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
PyErr_Format(PyExc_ValueError, "offset must be a timedelta" PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
...@@ -935,12 +929,6 @@ call_tzinfo_method(PyObject *tzinfo, const char *name, PyObject *tzinfoarg) ...@@ -935,12 +929,6 @@ call_tzinfo_method(PyObject *tzinfo, const char *name, PyObject *tzinfoarg)
if (offset == Py_None || offset == NULL) if (offset == Py_None || offset == NULL)
return offset; return offset;
if (PyDelta_Check(offset)) { if (PyDelta_Check(offset)) {
if (GET_TD_MICROSECONDS(offset) != 0) {
Py_DECREF(offset);
PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
" representing a whole number of seconds");
return NULL;
}
if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) || if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) ||
GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) { GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
Py_DECREF(offset); Py_DECREF(offset);
...@@ -966,9 +954,9 @@ call_tzinfo_method(PyObject *tzinfo, const char *name, PyObject *tzinfoarg) ...@@ -966,9 +954,9 @@ call_tzinfo_method(PyObject *tzinfo, const char *name, PyObject *tzinfoarg)
* result. tzinfo must be an instance of the tzinfo class. If utcoffset() * result. tzinfo must be an instance of the tzinfo class. If utcoffset()
* returns None, call_utcoffset returns 0 and sets *none to 1. If uctoffset() * returns None, call_utcoffset returns 0 and sets *none to 1. If uctoffset()
* doesn't return None or timedelta, TypeError is raised and this returns -1. * doesn't return None or timedelta, TypeError is raised and this returns -1.
* If utcoffset() returns an invalid timedelta (out of range, or not a whole * If utcoffset() returns an out of range timedelta,
* # of minutes), ValueError is raised and this returns -1. Else *none is * ValueError is raised and this returns -1. Else *none is
* set to 0 and the offset is returned (as int # of minutes east of UTC). * set to 0 and the offset is returned (as timedelta, positive east of UTC).
*/ */
static PyObject * static PyObject *
call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg) call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg)
...@@ -979,10 +967,10 @@ call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg) ...@@ -979,10 +967,10 @@ call_utcoffset(PyObject *tzinfo, PyObject *tzinfoarg)
/* Call tzinfo.dst(tzinfoarg), and extract an integer from the /* Call tzinfo.dst(tzinfoarg), and extract an integer from the
* result. tzinfo must be an instance of the tzinfo class. If dst() * result. tzinfo must be an instance of the tzinfo class. If dst()
* returns None, call_dst returns 0 and sets *none to 1. If dst() * returns None, call_dst returns 0 and sets *none to 1. If dst()
& doesn't return None or timedelta, TypeError is raised and this * doesn't return None or timedelta, TypeError is raised and this
* returns -1. If dst() returns an invalid timedelta for a UTC offset, * returns -1. If dst() returns an invalid timedelta for a UTC offset,
* ValueError is raised and this returns -1. Else *none is set to 0 and * ValueError is raised and this returns -1. Else *none is set to 0 and
* the offset is returned (as an int # of minutes east of UTC). * the offset is returned (as timedelta, positive east of UTC).
*/ */
static PyObject * static PyObject *
call_dst(PyObject *tzinfo, PyObject *tzinfoarg) call_dst(PyObject *tzinfo, PyObject *tzinfoarg)
...@@ -1100,13 +1088,13 @@ format_ctime(PyDateTime_Date *date, int hours, int minutes, int seconds) ...@@ -1100,13 +1088,13 @@ format_ctime(PyDateTime_Date *date, int hours, int minutes, int seconds)
static PyObject *delta_negative(PyDateTime_Delta *self); static PyObject *delta_negative(PyDateTime_Delta *self);
/* Add an hours & minutes UTC offset string to buf. buf has no more than /* Add formatted UTC offset string to buf. buf has no more than
* buflen bytes remaining. The UTC offset is gotten by calling * buflen bytes remaining. The UTC offset is gotten by calling
* tzinfo.uctoffset(tzinfoarg). If that returns None, \0 is stored into * tzinfo.uctoffset(tzinfoarg). If that returns None, \0 is stored into
* *buf, and that's all. Else the returned value is checked for sanity (an * *buf, and that's all. Else the returned value is checked for sanity (an
* integer in range), and if that's OK it's converted to an hours & minutes * integer in range), and if that's OK it's converted to an hours & minutes
* string of the form * string of the form
* sign HH sep MM * sign HH sep MM [sep SS [. UUUUUU]]
* Returns 0 if everything is OK. If the return value from utcoffset() is * Returns 0 if everything is OK. If the return value from utcoffset() is
* bogus, an appropriate exception is set and -1 is returned. * bogus, an appropriate exception is set and -1 is returned.
*/ */
...@@ -1115,7 +1103,7 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, ...@@ -1115,7 +1103,7 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
PyObject *tzinfo, PyObject *tzinfoarg) PyObject *tzinfo, PyObject *tzinfoarg)
{ {
PyObject *offset; PyObject *offset;
int hours, minutes, seconds; int hours, minutes, seconds, microseconds;
char sign; char sign;
assert(buflen >= 1); assert(buflen >= 1);
...@@ -1139,16 +1127,23 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, ...@@ -1139,16 +1127,23 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
sign = '+'; sign = '+';
} }
/* Offset is not negative here. */ /* Offset is not negative here. */
microseconds = GET_TD_MICROSECONDS(offset);
seconds = GET_TD_SECONDS(offset); seconds = GET_TD_SECONDS(offset);
Py_DECREF(offset); Py_DECREF(offset);
minutes = divmod(seconds, 60, &seconds); minutes = divmod(seconds, 60, &seconds);
hours = divmod(minutes, 60, &minutes); hours = divmod(minutes, 60, &minutes);
if (seconds == 0) if (microseconds) {
PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes); PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d.%06d", sign,
else hours, sep, minutes, sep, seconds, microseconds);
return 0;
}
if (seconds) {
PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d", sign, hours, PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d", sign, hours,
sep, minutes, sep, seconds); sep, minutes, sep, seconds);
return 0; return 0;
}
PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes);
return 0;
} }
static PyObject * static PyObject *
...@@ -3241,7 +3236,7 @@ static PyMethodDef tzinfo_methods[] = { ...@@ -3241,7 +3236,7 @@ static PyMethodDef tzinfo_methods[] = {
"values indicating West of UTC")}, "values indicating West of UTC")},
{"dst", (PyCFunction)tzinfo_dst, METH_O, {"dst", (PyCFunction)tzinfo_dst, METH_O,
PyDoc_STR("datetime -> DST offset in minutes east of UTC.")}, PyDoc_STR("datetime -> DST offset as timedelta positive east of UTC.")},
{"fromutc", (PyCFunction)tzinfo_fromutc, METH_O, {"fromutc", (PyCFunction)tzinfo_fromutc, METH_O,
PyDoc_STR("datetime in UTC -> datetime in local time.")}, PyDoc_STR("datetime in UTC -> datetime in local time.")},
...@@ -3375,7 +3370,7 @@ timezone_repr(PyDateTime_TimeZone *self) ...@@ -3375,7 +3370,7 @@ timezone_repr(PyDateTime_TimeZone *self)
static PyObject * static PyObject *
timezone_str(PyDateTime_TimeZone *self) timezone_str(PyDateTime_TimeZone *self)
{ {
int hours, minutes, seconds; int hours, minutes, seconds, microseconds;
PyObject *offset; PyObject *offset;
char sign; char sign;
...@@ -3401,12 +3396,20 @@ timezone_str(PyDateTime_TimeZone *self) ...@@ -3401,12 +3396,20 @@ timezone_str(PyDateTime_TimeZone *self)
Py_INCREF(offset); Py_INCREF(offset);
} }
/* Offset is not negative here. */ /* Offset is not negative here. */
microseconds = GET_TD_MICROSECONDS(offset);
seconds = GET_TD_SECONDS(offset); seconds = GET_TD_SECONDS(offset);
Py_DECREF(offset); Py_DECREF(offset);
minutes = divmod(seconds, 60, &seconds); minutes = divmod(seconds, 60, &seconds);
hours = divmod(minutes, 60, &minutes); hours = divmod(minutes, 60, &minutes);
/* XXX ignore sub-minute data, currently not allowed. */ if (microseconds != 0) {
assert(seconds == 0); return PyUnicode_FromFormat("UTC%c%02d:%02d:%02d.%06d",
sign, hours, minutes,
seconds, microseconds);
}
if (seconds != 0) {
return PyUnicode_FromFormat("UTC%c%02d:%02d:%02d",
sign, hours, minutes, seconds);
}
return PyUnicode_FromFormat("UTC%c%02d:%02d", sign, hours, minutes); return PyUnicode_FromFormat("UTC%c%02d:%02d", sign, hours, minutes);
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment