[Python-checkins] cpython: Closes #27595: Document PEP 495 (Local Time Disambiguation) features.

alexander.belopolsky python-checkins at python.org
Wed Aug 24 18:30:18 EDT 2016


https://hg.python.org/cpython/rev/dcd6d6be81a7
changeset:   102899:dcd6d6be81a7
user:        Alexander Belopolsky <alexander.belopolsky at gmail.com>
date:        Wed Aug 24 18:30:16 2016 -0400
summary:
  Closes #27595: Document PEP 495 (Local Time Disambiguation) features.

files:
  Doc/includes/tzinfo-examples.py |  130 ++++++++-------
  Doc/library/datetime.rst        |  159 ++++++++++++++-----
  2 files changed, 183 insertions(+), 106 deletions(-)


diff --git a/Doc/includes/tzinfo-examples.py b/Doc/includes/tzinfo-examples.py
--- a/Doc/includes/tzinfo-examples.py
+++ b/Doc/includes/tzinfo-examples.py
@@ -1,46 +1,13 @@
-from datetime import tzinfo, timedelta, datetime
+from datetime import tzinfo, timedelta, datetime, timezone
 
 ZERO = timedelta(0)
 HOUR = timedelta(hours=1)
-
-# A UTC class.
-
-class UTC(tzinfo):
-    """UTC"""
-
-    def utcoffset(self, dt):
-        return ZERO
-
-    def tzname(self, dt):
-        return "UTC"
-
-    def dst(self, dt):
-        return ZERO
-
-utc = UTC()
-
-# A class building tzinfo objects for fixed-offset time zones.
-# Note that FixedOffset(0, "UTC") is a different way to build a
-# UTC tzinfo object.
-
-class FixedOffset(tzinfo):
-    """Fixed offset in minutes east from UTC."""
-
-    def __init__(self, offset, name):
-        self.__offset = timedelta(minutes=offset)
-        self.__name = name
-
-    def utcoffset(self, dt):
-        return self.__offset
-
-    def tzname(self, dt):
-        return self.__name
-
-    def dst(self, dt):
-        return ZERO
+SECOND = timedelta(seconds=1)
 
 # A class capturing the platform's idea of local time.
-
+# (May result in wrong values on historical times in
+#  timezones where UTC offset and/or the DST rules had
+#  changed in the past.)
 import time as _time
 
 STDOFFSET = timedelta(seconds = -_time.timezone)
@@ -53,6 +20,16 @@
 
 class LocalTimezone(tzinfo):
 
+    def fromutc(self, dt):
+        assert dt.tzinfo is self
+        stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
+        args = _time.localtime(stamp)[:6]
+        dst_diff = DSTDIFF // SECOND
+        # Detect fold
+        fold = (args == _time.localtime(stamp - dst_diff))
+        return datetime(*args, microsecond=dt.microsecond,
+                        tzinfo=self, fold=fold)
+
     def utcoffset(self, dt):
         if self._isdst(dt):
             return DSTOFFSET
@@ -99,20 +76,37 @@
 # In the US, since 2007, DST starts at 2am (standard time) on the second
 # Sunday in March, which is the first Sunday on or after Mar 8.
 DSTSTART_2007 = datetime(1, 3, 8, 2)
-# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov.
-DSTEND_2007 = datetime(1, 11, 1, 1)
+# and ends at 2am (DST time) on the first Sunday of Nov.
+DSTEND_2007 = datetime(1, 11, 1, 2)
 # From 1987 to 2006, DST used to start at 2am (standard time) on the first
-# Sunday in April and to end at 2am (DST time; 1am standard time) on the last
+# Sunday in April and to end at 2am (DST time) on the last
 # Sunday of October, which is the first Sunday on or after Oct 25.
 DSTSTART_1987_2006 = datetime(1, 4, 1, 2)
-DSTEND_1987_2006 = datetime(1, 10, 25, 1)
+DSTEND_1987_2006 = datetime(1, 10, 25, 2)
 # From 1967 to 1986, DST used to start at 2am (standard time) on the last
-# Sunday in April (the one on or after April 24) and to end at 2am (DST time;
-# 1am standard time) on the last Sunday of October, which is the first Sunday
+# Sunday in April (the one on or after April 24) and to end at 2am (DST time)
+# on the last Sunday of October, which is the first Sunday
 # on or after Oct 25.
 DSTSTART_1967_1986 = datetime(1, 4, 24, 2)
 DSTEND_1967_1986 = DSTEND_1987_2006
 
+def us_dst_range(year):
+    # Find start and end times for US DST. For years before 1967, return
+    # start = end for no DST.
+    if 2006 < year:
+        dststart, dstend = DSTSTART_2007, DSTEND_2007
+    elif 1986 < year < 2007:
+        dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
+    elif 1966 < year < 1987:
+        dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
+    else:
+        return (datetime(year, 1, 1), ) * 2
+
+    start = first_sunday_on_or_after(dststart.replace(year=year))
+    end = first_sunday_on_or_after(dstend.replace(year=year))
+    return start, end
+
+
 class USTimeZone(tzinfo):
 
     def __init__(self, hours, reprname, stdname, dstname):
@@ -141,27 +135,39 @@
             # implementation) passes a datetime with dt.tzinfo is self.
             return ZERO
         assert dt.tzinfo is self
-
-        # Find start and end times for US DST. For years before 1967, return
-        # ZERO for no DST.
-        if 2006 < dt.year:
-            dststart, dstend = DSTSTART_2007, DSTEND_2007
-        elif 1986 < dt.year < 2007:
-            dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006
-        elif 1966 < dt.year < 1987:
-            dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986
-        else:
-            return ZERO
-
-        start = first_sunday_on_or_after(dststart.replace(year=dt.year))
-        end = first_sunday_on_or_after(dstend.replace(year=dt.year))
-
+        start, end = us_dst_range(dt.year)
         # Can't compare naive to aware objects, so strip the timezone from
         # dt first.
-        if start <= dt.replace(tzinfo=None) < end:
+        dt = dt.replace(tzinfo=None)
+        if start + HOUR <= dt < end - HOUR:
+            # DST is in effect.
             return HOUR
-        else:
-            return ZERO
+        if end - HOUR <= dt < end:
+            # Fold (an ambiguous hour): use dt.fold to disambiguate.
+            return ZERO if dt.fold else HOUR
+        if start <= dt < start + HOUR:
+            # Gap (a non-existent hour): reverse the fold rule.
+            return HOUR if dt.fold else ZERO
+        # DST is off.
+        return ZERO
+
+    def fromutc(self, dt):
+        assert dt.tzinfo is self
+        start, end = us_dst_range(dt.year)
+        start = start.replace(tzinfo=self)
+        end = end.replace(tzinfo=self)
+        std_time = dt + self.stdoffset
+        dst_time = std_time + HOUR
+        if end <= dst_time < end + HOUR:
+            # Repeated hour
+            return std_time.replace(fold=1)
+        if std_time < start or dst_time >= end:
+            # Standard time
+            return std_time
+        if start <= std_time < end - HOUR:
+            # Daylight saving time
+            return dst_time
+
 
 Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
 Central  = USTimeZone(-6, "Central",  "CST", "CDT")
diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst
--- a/Doc/library/datetime.rst
+++ b/Doc/library/datetime.rst
@@ -522,7 +522,7 @@
 
 Instance methods:
 
-.. method:: date.replace(year, month, day)
+.. method:: date.replace(year=self.year, month=self.month, day=self.day)
 
    Return a date with the same value, except for those parameters given new
    values by whichever keyword arguments are specified.  For example, if ``d ==
@@ -683,22 +683,26 @@
 
 Constructor:
 
-.. class:: datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
+.. class:: datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0)
 
    The year, month and day arguments are required.  *tzinfo* may be ``None``, or an
    instance of a :class:`tzinfo` subclass.  The remaining arguments may be integers,
    in the following ranges:
 
-   * ``MINYEAR <= year <= MAXYEAR``
-   * ``1 <= month <= 12``
-   * ``1 <= day <= number of days in the given month and year``
-   * ``0 <= hour < 24``
-   * ``0 <= minute < 60``
-   * ``0 <= second < 60``
-   * ``0 <= microsecond < 1000000``
+   * ``MINYEAR <= year <= MAXYEAR``,
+   * ``1 <= month <= 12``,
+   * ``1 <= day <= number of days in the given month and year``,
+   * ``0 <= hour < 24``,
+   * ``0 <= minute < 60``,
+   * ``0 <= second < 60``,
+   * ``0 <= microsecond < 1000000``,
+   * ``fold in [0, 1]``.
 
    If an argument outside those ranges is given, :exc:`ValueError` is raised.
 
+   .. versionadded:: 3.6
+      Added the ``fold`` argument.
+
 Other constructors, all class methods:
 
 .. classmethod:: datetime.today()
@@ -758,6 +762,8 @@
       instead of :exc:`ValueError` on :c:func:`localtime` or :c:func:`gmtime`
       failure.
 
+   .. versionchanged:: 3.6
+      :meth:`fromtimestamp` may return instances with :attr:`.fold` set to 1.
 
 .. classmethod:: datetime.utcfromtimestamp(timestamp)
 
@@ -794,7 +800,7 @@
    microsecond of the result are all 0, and :attr:`.tzinfo` is ``None``.
 
 
-.. classmethod:: datetime.combine(date, time[, tzinfo])
+.. classmethod:: datetime.combine(date, time, tzinfo=self.tzinfo)
 
    Return a new :class:`.datetime` object whose date components are equal to the
    given :class:`date` object's, and whose time components
@@ -886,6 +892,16 @@
    or ``None`` if none was passed.
 
 
+.. attribute:: datetime.fold
+
+   In ``[0, 1]``.  Used to disambiguate wall times during a repeated interval.  (A
+   repeated interval occurs when clocks are rolled back at the end of daylight saving
+   time or when the UTC offset for the current zone is decreased for political reasons.)
+   The value 0 (1) represents the earlier (later) of the two moments with the same wall
+   time representation.
+
+   .. versionadded:: 3.6
+
 Supported operations:
 
 +---------------------------------------+--------------------------------+
@@ -974,23 +990,34 @@
 
 .. method:: datetime.time()
 
-   Return :class:`.time` object with same hour, minute, second and microsecond.
+   Return :class:`.time` object with same hour, minute, second, microsecond and fold.
    :attr:`.tzinfo` is ``None``.  See also method :meth:`timetz`.
 
+   .. versionchanged:: 3.6
+      The fold value is copied to the returned :class:`.time` object.
+
 
 .. method:: datetime.timetz()
 
-   Return :class:`.time` object with same hour, minute, second, microsecond, and
+   Return :class:`.time` object with same hour, minute, second, microsecond, fold, and
    tzinfo attributes.  See also method :meth:`time`.
 
-
-.. method:: datetime.replace([year[, month[, day[, hour[, minute[, second[, microsecond[, tzinfo]]]]]]]])
+   .. versionchanged:: 3.6
+      The fold value is copied to the returned :class:`.time` object.
+
+
+.. method:: datetime.replace(year=self.year, month=self.month, day=self.day, \
+   hour=self.hour, minute=self.minute, second=self.second, microsecond=self.microsecond, \
+   tzinfo=self.tzinfo, * fold=0)
 
    Return a datetime with the same attributes, except for those attributes given
    new values by whichever keyword arguments are specified.  Note that
    ``tzinfo=None`` can be specified to create a naive datetime from an aware
    datetime with no conversion of date and time data.
 
+   .. versionadded:: 3.6
+      Added the ``fold`` argument.
+
 
 .. method:: datetime.astimezone(tz=None)
 
@@ -999,23 +1026,20 @@
    *self*, but in *tz*'s local time.
 
    If provided, *tz* must be an instance of a :class:`tzinfo` subclass, and its
-   :meth:`utcoffset` and :meth:`dst` methods must not return ``None``.  *self* must
-   be aware (``self.tzinfo`` must not be ``None``, and ``self.utcoffset()`` must
-   not return ``None``).
+   :meth:`utcoffset` and :meth:`dst` methods must not return ``None``.  If *self*
+   is naive (``self.tzinfo is None``), it is presumed to represent time in the
+   system timezone.
 
    If called without arguments (or with ``tz=None``) the system local
-   timezone is assumed.  The ``.tzinfo`` attribute of the converted
+   timezone is assumed for the target timezone.  The ``.tzinfo`` attribute of the converted
    datetime instance will be set to an instance of :class:`timezone`
    with the zone name and offset obtained from the OS.
 
    If ``self.tzinfo`` is *tz*, ``self.astimezone(tz)`` is equal to *self*:  no
    adjustment of date or time data is performed. Else the result is local
-   time in time zone *tz*, representing the same UTC time as *self*:  after
-   ``astz = dt.astimezone(tz)``, ``astz - astz.utcoffset()`` will usually have
-   the same date and time data as ``dt - dt.utcoffset()``. The discussion
-   of class :class:`tzinfo` explains the cases at Daylight Saving Time transition
-   boundaries where this cannot be achieved (an issue only if *tz* models both
-   standard and daylight time).
+   time in the timezone *tz*, representing the same UTC time as *self*:  after
+   ``astz = dt.astimezone(tz)``, ``astz - astz.utcoffset()`` will have
+   the same date and time data as ``dt - dt.utcoffset()``.
 
    If you merely want to attach a time zone object *tz* to a datetime *dt* without
    adjustment of date and time data, use ``dt.replace(tzinfo=tz)``.  If you
@@ -1037,6 +1061,10 @@
    .. versionchanged:: 3.3
       *tz* now can be omitted.
 
+   .. versionchanged:: 3.6
+      The :meth:`astimezone` method can now be called on naive instances that
+      are presumed to represent system local time.
+
 
 .. method:: datetime.utcoffset()
 
@@ -1113,6 +1141,10 @@
 
    .. versionadded:: 3.3
 
+   .. versionchanged:: 3.6
+      The :meth:`timestamp` method uses the :attr:`.fold` attribute to
+      disambiguate the times during a repeated interval.
+
    .. note::
 
       There is no method to obtain the POSIX timestamp directly from a
@@ -1342,16 +1374,17 @@
 A time object represents a (local) time of day, independent of any particular
 day, and subject to adjustment via a :class:`tzinfo` object.
 
-.. class:: time(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
+.. class:: time(hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0)
 
    All arguments are optional.  *tzinfo* may be ``None``, or an instance of a
    :class:`tzinfo` subclass.  The remaining arguments may be integers, in the
    following ranges:
 
-   * ``0 <= hour < 24``
-   * ``0 <= minute < 60``
-   * ``0 <= second < 60``
-   * ``0 <= microsecond < 1000000``.
+   * ``0 <= hour < 24``,
+   * ``0 <= minute < 60``,
+   * ``0 <= second < 60``,
+   * ``0 <= microsecond < 1000000``,
+   * ``fold in [0, 1]``.
 
    If an argument outside those ranges is given, :exc:`ValueError` is raised.  All
    default to ``0`` except *tzinfo*, which defaults to :const:`None`.
@@ -1404,6 +1437,17 @@
    ``None`` if none was passed.
 
 
+.. attribute:: time.fold
+
+   In ``[0, 1]``.  Used to disambiguate wall times during a repeated interval.  (A
+   repeated interval occurs when clocks are rolled back at the end of daylight saving
+   time or when the UTC offset for the current zone is decreased for political reasons.)
+   The value 0 (1) represents the earlier (later) of the two moments with the same wall
+   time representation.
+
+   .. versionadded:: 3.6
+
+
 Supported operations:
 
 * comparison of :class:`.time` to :class:`.time`, where *a* is considered less
@@ -1439,13 +1483,17 @@
 
 Instance methods:
 
-.. method:: time.replace([hour[, minute[, second[, microsecond[, tzinfo]]]]])
+.. method:: time.replace(hour=self.hour, minute=self.minute, second=self.second, \
+   microsecond=self.microsecond, tzinfo=self.tzinfo, * fold=0)
 
    Return a :class:`.time` with the same value, except for those attributes given
    new values by whichever keyword arguments are specified.  Note that
    ``tzinfo=None`` can be specified to create a naive :class:`.time` from an
    aware :class:`.time`, without conversion of the time data.
 
+   .. versionadded:: 3.6
+      Added the ``fold`` argument.
+
 
 .. method:: time.isoformat(timespec='auto')
 
@@ -1754,9 +1802,19 @@
 When DST starts (the "start" line), the local wall clock leaps from 1:59 to
 3:00.  A wall time of the form 2:MM doesn't really make sense on that day, so
 ``astimezone(Eastern)`` won't deliver a result with ``hour == 2`` on the day DST
-begins.  In order for :meth:`astimezone` to make this guarantee, the
-:meth:`tzinfo.dst` method must consider times in the "missing hour" (2:MM for
-Eastern) to be in daylight time.
+begins.  For example, at the Spring forward transition of 2016, we get
+
+    >>> u0 = datetime(2016, 3, 13, 5, tzinfo=timezone.utc)
+    >>> for i in range(4):
+    ...     u = u0 + i*HOUR
+    ...     t = u.astimezone(Eastern)
+    ...     print(u.time(), 'UTC =', t.time(), t.tzname())
+    ...
+    05:00:00 UTC = 00:00:00 EST
+    06:00:00 UTC = 01:00:00 EST
+    07:00:00 UTC = 03:00:00 EDT
+    08:00:00 UTC = 04:00:00 EDT
+
 
 When DST ends (the "end" line), there's a potentially worse problem: there's an
 hour that can't be spelled unambiguously in local wall time: the last hour of
@@ -1765,28 +1823,41 @@
 to 1:00 (standard time) again. Local times of the form 1:MM are ambiguous.
 :meth:`astimezone` mimics the local clock's behavior by mapping two adjacent UTC
 hours into the same local hour then.  In the Eastern example, UTC times of the
-form 5:MM and 6:MM both map to 1:MM when converted to Eastern.  In order for
-:meth:`astimezone` to make this guarantee, the :meth:`tzinfo.dst` method must
-consider times in the "repeated hour" to be in standard time.  This is easily
-arranged, as in the example, by expressing DST switch times in the time zone's
-standard local time.
-
-Applications that can't bear such ambiguities should avoid using hybrid
+form 5:MM and 6:MM both map to 1:MM when converted to Eastern, but earlier times
+have the :attr:`~datetime.fold` attribute set to 0 and the later times have it set to 1.
+For example, at the Fall back transition of 2016, we get
+
+    >>> u0 = datetime(2016, 11, 6, 4, tzinfo=timezone.utc)
+    >>> for i in range(4):
+    ...     u = u0 + i*HOUR
+    ...     t = u.astimezone(Eastern)
+    ...     print(u.time(), 'UTC =', t.time(), t.tzname(), t.fold)
+    ...
+    04:00:00 UTC = 00:00:00 EDT 0
+    05:00:00 UTC = 01:00:00 EDT 0
+    06:00:00 UTC = 01:00:00 EST 1
+    07:00:00 UTC = 02:00:00 EST 0
+
+Note that the :class:`datetime` instances that differ only by the value of the
+:attr:`~datetime.fold` attribute are considered equal in comparisons.
+
+Applications that can't bear wall-time ambiguities should explicitly check the
+value of the :attr:`~datetime.fold` atribute or avoid using hybrid
 :class:`tzinfo` subclasses; there are no ambiguities when using :class:`timezone`,
 or any other fixed-offset :class:`tzinfo` subclass (such as a class representing
 only EST (fixed offset -5 hours), or only EDT (fixed offset -4 hours)).
 
 .. seealso::
 
-   `pytz <https://pypi.python.org/pypi/pytz/>`_
+   `datetuil.tz <https://dateutil.readthedocs.io/en/stable/tz.html>`_
       The standard library has :class:`timezone` class for handling arbitrary
       fixed offsets from UTC and :attr:`timezone.utc` as UTC timezone instance.
 
-      *pytz* library brings the *IANA timezone database* (also known as the
+      *datetuil.tz* library brings the *IANA timezone database* (also known as the
       Olson database) to Python and its usage is recommended.
 
    `IANA timezone database <https://www.iana.org/time-zones>`_
-      The Time Zone Database (often called tz or zoneinfo) contains code and
+      The Time Zone Database (often called tz, tzdata or zoneinfo) contains code and
       data that represent the history of local time for many representative
       locations around the globe. It is updated periodically to reflect changes
       made by political bodies to time zone boundaries, UTC offsets, and
@@ -1806,7 +1877,7 @@
 made to civil time.
 
 
-.. class:: timezone(offset[, name])
+.. class:: timezone(offset, name=None)
 
   The *offset* argument must be specified as a :class:`timedelta`
   object representing the difference between the local time and UTC.  It must

-- 
Repository URL: https://hg.python.org/cpython


More information about the Python-checkins mailing list