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

Alexander Belopolsky webhook-mailer at python.org
Mon Jul 31 10:26:53 EDT 2017


https://github.com/python/cpython/commit/018d353c1c8c87767d2335cd884017c2ce12e045
commit: 018d353c1c8c87767d2335cd884017c2ce12e045
branch: master
author: Alexander Belopolsky <abalkin at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2017-07-31T10:26:50-04:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2017-07-26-13-18-29.bpo-5288.o_xEGj.rst
M Doc/library/datetime.rst
M Lib/datetime.py
M Lib/test/datetimetester.py
M Modules/_datetimemodule.c

diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
index 3880c2eb0b7..55be8694a06 100644
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -1071,16 +1071,20 @@ Instance methods:
 
    If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
    ``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
-   minutes with magnitude less than one day.
+   return ``None`` or a :class:`timedelta` object with magnitude less than one day.
+
+   .. versionchanged:: 3.7
+      The UTC offset is not restricted to a whole number of minutes.
 
 
 .. method:: datetime.dst()
 
    If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
    ``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
-   with magnitude less than one day.
+   ``None`` or a :class:`timedelta` object with magnitude less than one day.
+
+   .. versionchanged:: 3.7
+      The DST offset is not restricted to a whole number of minutes.
 
 
 .. method:: datetime.tzname()
@@ -1562,17 +1566,20 @@ Instance methods:
 
    If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
    ``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
-   minutes with magnitude less than one day.
+   return ``None`` or a :class:`timedelta` object with magnitude less than one day.
+
+   .. versionchanged:: 3.7
+      The UTC offset is not restricted to a whole number of minutes.
 
 
 .. method:: time.dst()
 
    If :attr:`.tzinfo` is ``None``, returns ``None``, else returns
    ``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
-   with magnitude less than one day.
+   ``None``, or a :class:`timedelta` object with magnitude less than one day.
 
+   .. versionchanged:: 3.7
+      The DST offset is not restricted to a whole number of minutes.
 
 .. method:: time.tzname()
 
@@ -1641,13 +1648,14 @@ Example:
 
 .. 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
    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
    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
-   -1439 to 1439 inclusive (1440 = 24\*60; the magnitude of the offset must be less
+   :class:`timedelta` object strictly between ``-timedelta(hours=24)`` and
+   ``timedelta(hours=24)`` (the magnitude of the offset must be less
    than one day).  Most implementations of :meth:`utcoffset` will probably look
    like one of these two::
 
@@ -1660,10 +1668,14 @@ Example:
    The default implementation of :meth:`utcoffset` raises
    :exc:`NotImplementedError`.
 
+   .. versionchanged:: 3.7
+      The UTC offset is not restricted to a whole number of minutes.
+
 
 .. 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
    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
@@ -1708,6 +1720,9 @@ Example:
 
    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)
 
@@ -1887,14 +1902,17 @@ made to civil time.
   The *offset* argument must be specified as a :class:`timedelta`
   object representing the difference between the local time and UTC.  It must
   be strictly between ``-timedelta(hours=24)`` and
-  ``timedelta(hours=24)`` and represent a whole number of minutes,
-  otherwise :exc:`ValueError` is raised.
+  ``timedelta(hours=24)``, otherwise :exc:`ValueError` is raised.
 
   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.
 
   .. versionadded:: 3.2
 
+  .. versionchanged:: 3.7
+     The UTC offset is not restricted to a whole number of minutes.
+
+
 .. method:: timezone.utcoffset(dt)
 
   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
   local time and UTC.
 
+  .. versionchanged:: 3.7
+     The UTC offset is not restricted to a whole number of minutes.
+
 .. method:: timezone.tzname(dt)
 
   Return the fixed value specified when the :class:`timezone` instance
@@ -2025,8 +2046,8 @@ format codes.
 |           | number, zero-padded on the     | 999999                 |       |
 |           | left.                          |                        |       |
 +-----------+--------------------------------+------------------------+-------+
-| ``%z``    | UTC offset in the form +HHMM   | (empty), +0000, -0400, | \(6)  |
-|           | or -HHMM (empty string if the  | +1030                  |       |
+| ``%z``    | UTC offset in the form         | (empty), +0000, -0400, | \(6)  |
+|           | ±HHMM[SS] (empty string if the | +1030                  |       |
 |           | object is naive).              |                        |       |
 +-----------+--------------------------------+------------------------+-------+
 | ``%Z``    | Time zone name (empty string   | (empty), UTC, EST, CST |       |
@@ -2139,12 +2160,19 @@ Notes:
    For an aware object:
 
    ``%z``
-      :meth:`utcoffset` is transformed into a 5-character string of the form
-      +HHMM or -HHMM, where HH is a 2-digit string giving the number of UTC
+      :meth:`utcoffset` is transformed into a string of the form
+      ±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
-      minutes.  For example, if :meth:`utcoffset` returns
-      ``timedelta(hours=-3, minutes=-30)``, ``%z`` is replaced with the string
-      ``'-0330'``.
+      minutes, SS is a 2-digit string string giving the number of UTC offset
+      seconds and uuuuuu is a 2-digit string string giving the number of UTC
+      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``
       If :meth:`tzname` returns ``None``, ``%Z`` is replaced by an empty
diff --git a/Lib/datetime.py b/Lib/datetime.py
index 76a6f957e08..2f03847be0a 100644
--- a/Lib/datetime.py
+++ b/Lib/datetime.py
@@ -206,10 +206,16 @@ def _wrap_strftime(object, format, timetuple):
                                 if offset.days < 0:
                                     offset = -offset
                                     sign = '-'
-                                h, m = divmod(offset, timedelta(hours=1))
-                                assert not m % timedelta(minutes=1), "whole minute"
-                                m //= timedelta(minutes=1)
-                                zreplace = '%c%02d%02d' % (sign, h, m)
+                                h, rest = divmod(offset, timedelta(hours=1))
+                                m, rest = divmod(rest, 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)
                     assert '%' not in zreplace
                     newformat.append(zreplace)
                 elif ch == 'Z':
@@ -241,7 +247,7 @@ def _check_tzname(name):
 # offset is what it returned.
 # If offset isn't None or timedelta, raises TypeError.
 # 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.
 def _check_utc_offset(name, offset):
     assert name in ("utcoffset", "dst")
@@ -250,9 +256,6 @@ def _check_utc_offset(name, offset):
     if not isinstance(offset, timedelta):
         raise TypeError("tzinfo.%s() must return None "
                         "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):
         raise ValueError("%s()=%s, must be strictly between "
                          "-timedelta(hours=24) and timedelta(hours=24)" %
@@ -960,11 +963,11 @@ def tzname(self, dt):
         raise NotImplementedError("tzinfo subclass must override tzname()")
 
     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()")
 
     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
         offset.
@@ -1262,8 +1265,8 @@ def __format__(self, fmt):
     # Timezone functions
 
     def utcoffset(self):
-        """Return the timezone offset in minutes east of UTC (negative west of
-        UTC)."""
+        """Return the timezone offset as timedelta, positive east of UTC
+         (negative west of UTC)."""
         if self._tzinfo is None:
             return None
         offset = self._tzinfo.utcoffset(None)
@@ -1284,8 +1287,8 @@ def tzname(self):
         return name
 
     def dst(self):
-        """Return 0 if DST is not in effect, or the DST offset (in minutes
-        eastward) if DST is in effect.
+        """Return 0 if DST is not in effect, or the DST offset (as timedelta
+        positive eastward) if DST is in effect.
 
         This is purely informational; the DST offset has already been added to
         the UTC offset returned by utcoffset() if applicable, so there's no
@@ -1714,7 +1717,7 @@ def strptime(cls, date_string, format):
         return _strptime._strptime_datetime(cls, date_string, format)
 
     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)."""
         if self._tzinfo is None:
             return None
@@ -1736,8 +1739,8 @@ def tzname(self):
         return name
 
     def dst(self):
-        """Return 0 if DST is not in effect, or the DST offset (in minutes
-        eastward) if DST is in effect.
+        """Return 0 if DST is not in effect, or the DST offset (as timedelta
+        positive eastward) if DST is in effect.
 
         This is purely informational; the DST offset has already been added to
         the UTC offset returned by utcoffset() if applicable, so there's no
@@ -1962,9 +1965,6 @@ def __new__(cls, offset, name=_Omitted):
             raise ValueError("offset must be a timedelta "
                              "strictly between -timedelta(hours=24) and "
                              "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)
 
     @classmethod
@@ -2053,8 +2053,15 @@ def _name_from_offset(delta):
         else:
             sign = '+'
         hours, rest = divmod(delta, timedelta(hours=1))
-        minutes = rest // timedelta(minutes=1)
-        return 'UTC{}{:02d}:{:02d}'.format(sign, hours, minutes)
+        minutes, rest = divmod(rest, timedelta(minutes=1))
+        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.min = timezone._create(timezone._minoffset)
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 22008884b54..29b70e1a8a0 100644
--- a/Lib/test/datetimetester.py
+++ b/Lib/test/datetimetester.py
@@ -255,14 +255,15 @@ def test_class_members(self):
         self.assertEqual(timezone.min.utcoffset(None), -limit)
         self.assertEqual(timezone.max.utcoffset(None), limit)
 
-
     def test_constructor(self):
         self.assertIs(timezone.utc, timezone(timedelta(0)))
         self.assertIsNot(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
-        for invalid in [timedelta(microseconds=1), timedelta(1, 1),
-                        timedelta(seconds=1), timedelta(1), -timedelta(1)]:
+        for invalid in [timedelta(1, 1), timedelta(1)]:
             self.assertRaises(ValueError, timezone, invalid)
             self.assertRaises(ValueError, timezone, -invalid)
 
@@ -301,6 +302,15 @@ def test_tzname(self):
         self.assertEqual('UTC-00:01', timezone(timedelta(minutes=-1)).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(5)
 
@@ -2152,6 +2162,9 @@ def test_more_strftime(self):
         t = self.theclass(2004, 12, 31, 6, 22, 33, 47)
         self.assertEqual(t.strftime("%m %d %y %f %S %M %H %j"),
                                     "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):
         dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234)
@@ -2717,8 +2730,8 @@ class C7(tzinfo):
             def utcoffset(self, dt): return timedelta(microseconds=61)
             def dst(self, dt): return timedelta(microseconds=-81)
         t = cls(1, 1, 1, tzinfo=C7())
-        self.assertRaises(ValueError, t.utcoffset)
-        self.assertRaises(ValueError, t.dst)
+        self.assertEqual(t.utcoffset(), timedelta(microseconds=61))
+        self.assertEqual(t.dst(), timedelta(microseconds=-81))
 
     def test_aware_compare(self):
         cls = self.theclass
@@ -4297,7 +4310,6 @@ def test_vilnius_1941_toutc(self):
         self.assertEqual(gdt.strftime("%c %Z"),
                          'Mon Jun 23 22:00:00 1941 UTC')
 
-
     def test_constructors(self):
         t = time(0, fold=1)
         dt = datetime(1, 1, 1, fold=1)
@@ -4372,7 +4384,6 @@ def test_fromtimestamp_lord_howe(self):
         self.assertEqual(t0.fold, 0)
         self.assertEqual(t1.fold, 1)
 
-
     @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
     def test_timestamp(self):
         dt0 = datetime(2014, 11, 2, 1, 30)
@@ -4390,7 +4401,6 @@ def test_timestamp_lord_howe(self):
         s1 = t.replace(fold=1).timestamp()
         self.assertEqual(s0 + 1800, s1)
 
-
     @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
     def test_astimezone(self):
         dt0 = datetime(2014, 11, 2, 1, 30)
@@ -4406,7 +4416,6 @@ def test_astimezone(self):
         self.assertEqual(adt0.fold, 0)
         self.assertEqual(adt1.fold, 0)
 
-
     def test_pickle_fold(self):
         t = time(fold=1)
         dt = datetime(1, 1, 1, fold=1)
diff --git a/Misc/NEWS.d/next/Library/2017-07-26-13-18-29.bpo-5288.o_xEGj.rst b/Misc/NEWS.d/next/Library/2017-07-26-13-18-29.bpo-5288.o_xEGj.rst
new file mode 100644
index 00000000000..a7eaa06107f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-07-26-13-18-29.bpo-5288.o_xEGj.rst
@@ -0,0 +1 @@
+Support tzinfo objects with sub-minute offsets.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index 28805d18da8..1b68ff3b372 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -859,12 +859,6 @@ new_timezone(PyObject *offset, PyObject *name)
         Py_INCREF(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) ||
         GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
         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)
         return 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) ||
             GET_TD_DAYS(offset) < -1 || GET_TD_DAYS(offset) >= 1) {
             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()
  * 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.
- * If utcoffset() returns an invalid timedelta (out of range, or not a whole
- * # of minutes), 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).
+ * If utcoffset() returns an out of range timedelta,
+ * ValueError is raised and this returns -1.  Else *none is
+ * set to 0 and the offset is returned (as timedelta, positive east of UTC).
  */
 static PyObject *
 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
  * 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()
- & 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,
  * 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 *
 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);
 
-/* 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
  * 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
  * integer in range), and if that's OK it's converted to an hours & minutes
  * 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
  * 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 *offset;
-    int hours, minutes, seconds;
+    int hours, minutes, seconds, microseconds;
     char sign;
 
     assert(buflen >= 1);
@@ -1139,15 +1127,22 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
         sign = '+';
     }
     /* Offset is not negative here. */
+    microseconds = GET_TD_MICROSECONDS(offset);
     seconds = GET_TD_SECONDS(offset);
     Py_DECREF(offset);
     minutes = divmod(seconds, 60, &seconds);
     hours = divmod(minutes, 60, &minutes);
-    if (seconds == 0)
-        PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes);
-    else
+    if (microseconds) {
+        PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d.%06d", sign,
+                      hours, sep, minutes, sep, seconds, microseconds);
+        return 0;
+    }
+    if (seconds) {
         PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d", sign, hours,
                       sep, minutes, sep, seconds);
+        return 0;
+    }
+    PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes);
     return 0;
 }
 
@@ -3241,7 +3236,7 @@ static PyMethodDef tzinfo_methods[] = {
            "values indicating West of UTC")},
 
     {"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,
      PyDoc_STR("datetime in UTC -> datetime in local time.")},
@@ -3375,7 +3370,7 @@ timezone_repr(PyDateTime_TimeZone *self)
 static PyObject *
 timezone_str(PyDateTime_TimeZone *self)
 {
-    int hours, minutes, seconds;
+    int hours, minutes, seconds, microseconds;
     PyObject *offset;
     char sign;
 
@@ -3401,12 +3396,20 @@ timezone_str(PyDateTime_TimeZone *self)
         Py_INCREF(offset);
     }
     /* Offset is not negative here. */
+    microseconds = GET_TD_MICROSECONDS(offset);
     seconds = GET_TD_SECONDS(offset);
     Py_DECREF(offset);
     minutes = divmod(seconds, 60, &seconds);
     hours = divmod(minutes, 60, &minutes);
-    /* XXX ignore sub-minute data, currently not allowed. */
-    assert(seconds == 0);
+    if (microseconds != 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);
 }
 



More information about the Python-checkins mailing list