mirror of https://github.com/python/cpython
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.
This commit is contained in:
parent
c6ea8974e2
commit
018d353c1c
|
@ -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:
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
.. 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:
|
||||||
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:
|
||||||
|
|
||||||
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.
|
||||||
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.
|
||||||
: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.
|
||||||
| | 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:
|
||||||
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):
|
||||||
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):
|
||||||
# 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):
|
||||||
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:
|
||||||
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:
|
||||||
# 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:
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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:
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Support tzinfo objects with sub-minute offsets.
|
|
@ -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)
|
||||||
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)
|
||||||
* 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)
|
||||||
/* 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)
|
||||||
|
|
||||||
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,
|
||||||
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,17 +1127,24 @@ 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 *
|
||||||
make_Zreplacement(PyObject *object, PyObject *tzinfoarg)
|
make_Zreplacement(PyObject *object, PyObject *tzinfoarg)
|
||||||
|
@ -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)
|
||||||
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)
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue