]> granicus.if.org Git - python/commitdiff
Closes issue #24773: Implement PEP 495 (Local Time Disambiguation).
authorAlexander Belopolsky <alexander.belopolsky@gmail.com>
Fri, 22 Jul 2016 22:47:04 +0000 (18:47 -0400)
committerAlexander Belopolsky <alexander.belopolsky@gmail.com>
Fri, 22 Jul 2016 22:47:04 +0000 (18:47 -0400)
Include/datetime.h
Lib/datetime.py
Lib/test/datetimetester.py
Lib/test/libregrtest/cmdline.py
Misc/NEWS
Modules/_datetimemodule.c
Tools/tz/zdump.py [new file with mode: 0644]

index 06cbc4abbd60e2663041aebc87dfa6c93b190565..3bf35cbb7f20f6ea06c7929470f8ac59c8ab84d0 100644 (file)
@@ -81,6 +81,7 @@ typedef struct
 typedef struct
 {
     _PyDateTime_TIMEHEAD
+    unsigned char fold;
     PyObject *tzinfo;
 } PyDateTime_Time;              /* hastzinfo true */
 
@@ -108,6 +109,7 @@ typedef struct
 typedef struct
 {
     _PyDateTime_DATETIMEHEAD
+    unsigned char fold;
     PyObject *tzinfo;
 } PyDateTime_DateTime;          /* hastzinfo true */
 
@@ -125,6 +127,7 @@ typedef struct
     ((((PyDateTime_DateTime*)o)->data[7] << 16) |       \
      (((PyDateTime_DateTime*)o)->data[8] << 8)  |       \
       ((PyDateTime_DateTime*)o)->data[9])
+#define PyDateTime_DATE_GET_FOLD(o)        (((PyDateTime_DateTime*)o)->fold)
 
 /* Apply for time instances. */
 #define PyDateTime_TIME_GET_HOUR(o)        (((PyDateTime_Time*)o)->data[0])
@@ -134,6 +137,7 @@ typedef struct
     ((((PyDateTime_Time*)o)->data[3] << 16) |           \
      (((PyDateTime_Time*)o)->data[4] << 8)  |           \
       ((PyDateTime_Time*)o)->data[5])
+#define PyDateTime_TIME_GET_FOLD(o)        (((PyDateTime_Time*)o)->fold)
 
 /* Apply for time delta instances */
 #define PyDateTime_DELTA_GET_DAYS(o)         (((PyDateTime_Delta*)o)->days)
@@ -162,6 +166,11 @@ typedef struct {
     PyObject *(*DateTime_FromTimestamp)(PyObject*, PyObject*, PyObject*);
     PyObject *(*Date_FromTimestamp)(PyObject*, PyObject*);
 
+    /* PEP 495 constructors */
+    PyObject *(*DateTime_FromDateAndTimeAndFold)(int, int, int, int, int, int, int,
+        PyObject*, int, PyTypeObject*);
+    PyObject *(*Time_FromTimeAndFold)(int, int, int, int, PyObject*, int, PyTypeObject*);
+
 } PyDateTime_CAPI;
 
 #define PyDateTime_CAPSULE_NAME "datetime.datetime_CAPI"
@@ -217,10 +226,18 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
     PyDateTimeAPI->DateTime_FromDateAndTime(year, month, day, hour, \
         min, sec, usec, Py_None, PyDateTimeAPI->DateTimeType)
 
+#define PyDateTime_FromDateAndTimeAndFold(year, month, day, hour, min, sec, usec, fold) \
+    PyDateTimeAPI->DateTime_FromDateAndTimeAndFold(year, month, day, hour, \
+        min, sec, usec, Py_None, fold, PyDateTimeAPI->DateTimeType)
+
 #define PyTime_FromTime(hour, minute, second, usecond) \
     PyDateTimeAPI->Time_FromTime(hour, minute, second, usecond, \
         Py_None, PyDateTimeAPI->TimeType)
 
+#define PyTime_FromTimeAndFold(hour, minute, second, usecond, fold) \
+    PyDateTimeAPI->Time_FromTimeAndFold(hour, minute, second, usecond, \
+        Py_None, fold, PyDateTimeAPI->TimeType)
+
 #define PyDelta_FromDSU(days, seconds, useconds) \
     PyDateTimeAPI->Delta_FromDelta(days, seconds, useconds, 1, \
         PyDateTimeAPI->DeltaType)
index b1321a34e3832dc521c9aab23bdbd550ca164714..19d2f676e550ce860a2bf242d27cafacac8028aa 100644 (file)
@@ -250,9 +250,9 @@ 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 % timedelta(minutes=1) or offset.microseconds:
+    if offset.microseconds:
         raise ValueError("tzinfo.%s() must return a whole number "
-                         "of minutes, got %s" % (name, offset))
+                         "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)" %
@@ -930,7 +930,7 @@ class date:
 
     # Pickle support.
 
-    def _getstate(self):
+    def _getstate(self, protocol=3):
         yhi, ylo = divmod(self._year, 256)
         return bytes([yhi, ylo, self._month, self._day]),
 
@@ -938,8 +938,8 @@ class date:
         yhi, ylo, self._month, self._day = string
         self._year = yhi * 256 + ylo
 
-    def __reduce__(self):
-        return (self.__class__, self._getstate())
+    def __reduce_ex__(self, protocol):
+        return (self.__class__, self._getstate(protocol))
 
 _date_class = date  # so functions w/ args named "date" can get at the class
 
@@ -947,6 +947,7 @@ date.min = date(1, 1, 1)
 date.max = date(9999, 12, 31)
 date.resolution = timedelta(days=1)
 
+
 class tzinfo:
     """Abstract base class for time zone info classes.
 
@@ -1038,11 +1039,11 @@ class time:
     dst()
 
     Properties (readonly):
-    hour, minute, second, microsecond, tzinfo
+    hour, minute, second, microsecond, tzinfo, fold
     """
-    __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode'
+    __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode', '_fold'
 
-    def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None):
+    def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0):
         """Constructor.
 
         Arguments:
@@ -1050,8 +1051,9 @@ class time:
         hour, minute (required)
         second, microsecond (default to zero)
         tzinfo (default to None)
+        fold (keyword only, default to True)
         """
-        if isinstance(hour, bytes) and len(hour) == 6 and hour[0] < 24:
+        if isinstance(hour, bytes) and len(hour) == 6 and hour[0]&0x7F < 24:
             # Pickle support
             self = object.__new__(cls)
             self.__setstate(hour, minute or None)
@@ -1067,6 +1069,7 @@ class time:
         self._microsecond = microsecond
         self._tzinfo = tzinfo
         self._hashcode = -1
+        self._fold = fold
         return self
 
     # Read-only field accessors
@@ -1095,6 +1098,10 @@ class time:
         """timezone info object"""
         return self._tzinfo
 
+    @property
+    def fold(self):
+        return self._fold
+
     # Standard conversions, __hash__ (and helpers)
 
     # Comparisons of time objects with other.
@@ -1160,9 +1167,13 @@ class time:
     def __hash__(self):
         """Hash."""
         if self._hashcode == -1:
-            tzoff = self.utcoffset()
+            if self.fold:
+                t = self.replace(fold=0)
+            else:
+                t = self
+            tzoff = t.utcoffset()
             if not tzoff:  # zero or None
-                self._hashcode = hash(self._getstate()[0])
+                self._hashcode = hash(t._getstate()[0])
             else:
                 h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff,
                               timedelta(hours=1))
@@ -1186,10 +1197,11 @@ class time:
             else:
                 sign = "+"
             hh, mm = divmod(off, timedelta(hours=1))
-            assert not mm % timedelta(minutes=1), "whole minute"
-            mm //= timedelta(minutes=1)
+            mm, ss = divmod(mm, timedelta(minutes=1))
             assert 0 <= hh < 24
             off = "%s%02d%s%02d" % (sign, hh, sep, mm)
+            if ss:
+                off += ':%02d' % ss.seconds
         return off
 
     def __repr__(self):
@@ -1206,6 +1218,9 @@ class time:
         if self._tzinfo is not None:
             assert s[-1:] == ")"
             s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
+        if self._fold:
+            assert s[-1:] == ")"
+            s = s[:-1] + ", fold=1)"
         return s
 
     def isoformat(self, timespec='auto'):
@@ -1284,7 +1299,7 @@ class time:
         return offset
 
     def replace(self, hour=None, minute=None, second=None, microsecond=None,
-                tzinfo=True):
+                tzinfo=True, *, fold=None):
         """Return a new time with new values for the specified fields."""
         if hour is None:
             hour = self.hour
@@ -1296,14 +1311,19 @@ class time:
             microsecond = self.microsecond
         if tzinfo is True:
             tzinfo = self.tzinfo
-        return time(hour, minute, second, microsecond, tzinfo)
+        if fold is None:
+            fold = self._fold
+        return time(hour, minute, second, microsecond, tzinfo, fold=fold)
 
     # Pickle support.
 
-    def _getstate(self):
+    def _getstate(self, protocol=3):
         us2, us3 = divmod(self._microsecond, 256)
         us1, us2 = divmod(us2, 256)
-        basestate = bytes([self._hour, self._minute, self._second,
+        h = self._hour
+        if self._fold and protocol > 3:
+            h += 128
+        basestate = bytes([h, self._minute, self._second,
                            us1, us2, us3])
         if self._tzinfo is None:
             return (basestate,)
@@ -1313,12 +1333,18 @@ class time:
     def __setstate(self, string, tzinfo):
         if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class):
             raise TypeError("bad tzinfo state arg")
-        self._hour, self._minute, self._second, us1, us2, us3 = string
+        h, self._minute, self._second, us1, us2, us3 = string
+        if h > 127:
+            self._fold = 1
+            self._hour = h - 128
+        else:
+            self._fold = 0
+            self._hour = h
         self._microsecond = (((us1 << 8) | us2) << 8) | us3
         self._tzinfo = tzinfo
 
-    def __reduce__(self):
-        return (time, self._getstate())
+    def __reduce_ex__(self, protocol):
+        return (time, self._getstate(protocol))
 
 _time_class = time  # so functions w/ args named "time" can get at the class
 
@@ -1335,8 +1361,8 @@ class datetime(date):
     __slots__ = date.__slots__ + time.__slots__
 
     def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
-                microsecond=0, tzinfo=None):
-        if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2] <= 12:
+                microsecond=0, tzinfo=None, *, fold=0):
+        if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2]&0x7F <= 12:
             # Pickle support
             self = object.__new__(cls)
             self.__setstate(year, month)
@@ -1356,6 +1382,7 @@ class datetime(date):
         self._microsecond = microsecond
         self._tzinfo = tzinfo
         self._hashcode = -1
+        self._fold = fold
         return self
 
     # Read-only field accessors
@@ -1384,6 +1411,10 @@ class datetime(date):
         """timezone info object"""
         return self._tzinfo
 
+    @property
+    def fold(self):
+        return self._fold
+
     @classmethod
     def _fromtimestamp(cls, t, utc, tz):
         """Construct a datetime from a POSIX timestamp (like time.time()).
@@ -1402,7 +1433,23 @@ class datetime(date):
         converter = _time.gmtime if utc else _time.localtime
         y, m, d, hh, mm, ss, weekday, jday, dst = converter(t)
         ss = min(ss, 59)    # clamp out leap seconds if the platform has them
-        return cls(y, m, d, hh, mm, ss, us, tz)
+        result = cls(y, m, d, hh, mm, ss, us, tz)
+        if tz is None:
+            # As of version 2015f max fold in IANA database is
+            # 23 hours at 1969-09-30 13:00:00 in Kwajalein.
+            # Let's probe 24 hours in the past to detect a transition:
+            max_fold_seconds = 24 * 3600
+            y, m, d, hh, mm, ss = converter(t - max_fold_seconds)[:6]
+            probe1 = cls(y, m, d, hh, mm, ss, us, tz)
+            trans = result - probe1 - timedelta(0, max_fold_seconds)
+            if trans.days < 0:
+                y, m, d, hh, mm, ss = converter(t + trans // timedelta(0, 1))[:6]
+                probe2 = cls(y, m, d, hh, mm, ss, us, tz)
+                if probe2 == result:
+                    result._fold = 1
+        else:
+            result = tz.fromutc(result)
+        return result
 
     @classmethod
     def fromtimestamp(cls, t, tz=None):
@@ -1412,10 +1459,7 @@ class datetime(date):
         """
         _check_tzinfo_arg(tz)
 
-        result = cls._fromtimestamp(t, tz is not None, tz)
-        if tz is not None:
-            result = tz.fromutc(result)
-        return result
+        return cls._fromtimestamp(t, tz is not None, tz)
 
     @classmethod
     def utcfromtimestamp(cls, t):
@@ -1443,7 +1487,7 @@ class datetime(date):
             raise TypeError("time argument must be a time instance")
         return cls(date.year, date.month, date.day,
                    time.hour, time.minute, time.second, time.microsecond,
-                   time.tzinfo)
+                   time.tzinfo, fold=time.fold)
 
     def timetuple(self):
         "Return local time tuple compatible with time.localtime()."
@@ -1458,12 +1502,46 @@ class datetime(date):
                                   self.hour, self.minute, self.second,
                                   dst)
 
+    def _mktime(self):
+        """Return integer POSIX timestamp."""
+        epoch = datetime(1970, 1, 1)
+        max_fold_seconds = 24 * 3600
+        t = (self - epoch) // timedelta(0, 1)
+        def local(u):
+            y, m, d, hh, mm, ss = _time.localtime(u)[:6]
+            return (datetime(y, m, d, hh, mm, ss) - epoch) // timedelta(0, 1)
+
+        # Our goal is to solve t = local(u) for u.
+        a = local(t) - t
+        u1 = t - a
+        t1 = local(u1)
+        if t1 == t:
+            # We found one solution, but it may not be the one we need.
+            # Look for an earlier solution (if `fold` is 0), or a
+            # later one (if `fold` is 1).
+            u2 = u1 + (-max_fold_seconds, max_fold_seconds)[self.fold]
+            b = local(u2) - u2
+            if a == b:
+                return u1
+        else:
+            b = t1 - u1
+            assert a != b
+        u2 = t - b
+        t2 = local(u2)
+        if t2 == t:
+            return u2
+        if t1 == t:
+            return u1
+        # We have found both offsets a and b, but neither t - a nor t - b is
+        # a solution.  This means t is in the gap.
+        return (max, min)[self.fold](u1, u2)
+
+
     def timestamp(self):
         "Return POSIX timestamp as float"
         if self._tzinfo is None:
-            return _time.mktime((self.year, self.month, self.day,
-                                 self.hour, self.minute, self.second,
-                                 -1, -1, -1)) + self.microsecond / 1e6
+            s = self._mktime()
+            return s + self.microsecond / 1e6
         else:
             return (self - _EPOCH).total_seconds()
 
@@ -1482,15 +1560,16 @@ class datetime(date):
 
     def time(self):
         "Return the time part, with tzinfo None."
-        return time(self.hour, self.minute, self.second, self.microsecond)
+        return time(self.hour, self.minute, self.second, self.microsecond, fold=self.fold)
 
     def timetz(self):
         "Return the time part, with same tzinfo."
         return time(self.hour, self.minute, self.second, self.microsecond,
-                    self._tzinfo)
+                    self._tzinfo, fold=self.fold)
 
     def replace(self, year=None, month=None, day=None, hour=None,
-                minute=None, second=None, microsecond=None, tzinfo=True):
+                minute=None, second=None, microsecond=None, tzinfo=True,
+                *, fold=None):
         """Return a new datetime with new values for the specified fields."""
         if year is None:
             year = self.year
@@ -1508,46 +1587,45 @@ class datetime(date):
             microsecond = self.microsecond
         if tzinfo is True:
             tzinfo = self.tzinfo
-        return datetime(year, month, day, hour, minute, second, microsecond,
-                        tzinfo)
+        if fold is None:
+            fold = self.fold
+        return datetime(year, month, day, hour, minute, second,
+                          microsecond, tzinfo, fold=fold)
+
+    def _local_timezone(self):
+        if self.tzinfo is None:
+            ts = self._mktime()
+        else:
+            ts = (self - _EPOCH) // timedelta(seconds=1)
+        localtm = _time.localtime(ts)
+        local = datetime(*localtm[:6])
+        try:
+            # Extract TZ data if available
+            gmtoff = localtm.tm_gmtoff
+            zone = localtm.tm_zone
+        except AttributeError:
+            delta = local - datetime(*_time.gmtime(ts)[:6])
+            zone = _time.strftime('%Z', localtm)
+            tz = timezone(delta, zone)
+        else:
+            tz = timezone(timedelta(seconds=gmtoff), zone)
+        return tz
 
     def astimezone(self, tz=None):
         if tz is None:
-            if self.tzinfo is None:
-                raise ValueError("astimezone() requires an aware datetime")
-            ts = (self - _EPOCH) // timedelta(seconds=1)
-            localtm = _time.localtime(ts)
-            local = datetime(*localtm[:6])
-            try:
-                # Extract TZ data if available
-                gmtoff = localtm.tm_gmtoff
-                zone = localtm.tm_zone
-            except AttributeError:
-                # Compute UTC offset and compare with the value implied
-                # by tm_isdst.  If the values match, use the zone name
-                # implied by tm_isdst.
-                delta = local - datetime(*_time.gmtime(ts)[:6])
-                dst = _time.daylight and localtm.tm_isdst > 0
-                gmtoff = -(_time.altzone if dst else _time.timezone)
-                if delta == timedelta(seconds=gmtoff):
-                    tz = timezone(delta, _time.tzname[dst])
-                else:
-                    tz = timezone(delta)
-            else:
-                tz = timezone(timedelta(seconds=gmtoff), zone)
-
+            tz = self._local_timezone()
         elif not isinstance(tz, tzinfo):
             raise TypeError("tz argument must be an instance of tzinfo")
 
         mytz = self.tzinfo
         if mytz is None:
-            raise ValueError("astimezone() requires an aware datetime")
+            mytz = self._local_timezone()
 
         if tz is mytz:
             return self
 
         # Convert self to UTC, and attach the new time zone object.
-        myoffset = self.utcoffset()
+        myoffset = mytz.utcoffset(self)
         if myoffset is None:
             raise ValueError("astimezone() requires an aware datetime")
         utc = (self - myoffset).replace(tzinfo=tz)
@@ -1594,9 +1672,11 @@ class datetime(date):
             else:
                 sign = "+"
             hh, mm = divmod(off, timedelta(hours=1))
-            assert not mm % timedelta(minutes=1), "whole minute"
-            mm //= timedelta(minutes=1)
+            mm, ss = divmod(mm, timedelta(minutes=1))
             s += "%s%02d:%02d" % (sign, hh, mm)
+            if ss:
+                assert not ss.microseconds
+                s += ":%02d" % ss.seconds
         return s
 
     def __repr__(self):
@@ -1613,6 +1693,9 @@ class datetime(date):
         if self._tzinfo is not None:
             assert s[-1:] == ")"
             s = s[:-1] + ", tzinfo=%r" % self._tzinfo + ")"
+        if self._fold:
+            assert s[-1:] == ")"
+            s = s[:-1] + ", fold=1)"
         return s
 
     def __str__(self):
@@ -1715,6 +1798,12 @@ class datetime(date):
         else:
             myoff = self.utcoffset()
             otoff = other.utcoffset()
+            # Assume that allow_mixed means that we are called from __eq__
+            if allow_mixed:
+                if myoff != self.replace(fold=not self.fold).utcoffset():
+                    return 2
+                if otoff != other.replace(fold=not other.fold).utcoffset():
+                    return 2
             base_compare = myoff == otoff
 
         if base_compare:
@@ -1782,9 +1871,13 @@ class datetime(date):
 
     def __hash__(self):
         if self._hashcode == -1:
-            tzoff = self.utcoffset()
+            if self.fold:
+                t = self.replace(fold=0)
+            else:
+                t = self
+            tzoff = t.utcoffset()
             if tzoff is None:
-                self._hashcode = hash(self._getstate()[0])
+                self._hashcode = hash(t._getstate()[0])
             else:
                 days = _ymd2ord(self.year, self.month, self.day)
                 seconds = self.hour * 3600 + self.minute * 60 + self.second
@@ -1793,11 +1886,14 @@ class datetime(date):
 
     # Pickle support.
 
-    def _getstate(self):
+    def _getstate(self, protocol=3):
         yhi, ylo = divmod(self._year, 256)
         us2, us3 = divmod(self._microsecond, 256)
         us1, us2 = divmod(us2, 256)
-        basestate = bytes([yhi, ylo, self._month, self._day,
+        m = self._month
+        if self._fold and protocol > 3:
+            m += 128
+        basestate = bytes([yhi, ylo, m, self._day,
                            self._hour, self._minute, self._second,
                            us1, us2, us3])
         if self._tzinfo is None:
@@ -1808,14 +1904,20 @@ class datetime(date):
     def __setstate(self, string, tzinfo):
         if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class):
             raise TypeError("bad tzinfo state arg")
-        (yhi, ylo, self._month, self._day, self._hour,
+        (yhi, ylo, m, self._day, self._hour,
          self._minute, self._second, us1, us2, us3) = string
+        if m > 127:
+            self._fold = 1
+            self._month = m - 128
+        else:
+            self._fold = 0
+            self._month = m
         self._year = yhi * 256 + ylo
         self._microsecond = (((us1 << 8) | us2) << 8) | us3
         self._tzinfo = tzinfo
 
-    def __reduce__(self):
-        return (self.__class__, self._getstate())
+    def __reduce_ex__(self, protocol):
+        return (self.__class__, self._getstate(protocol))
 
 
 datetime.min = datetime(1, 1, 1)
index 8fc01390b38119196c71b2e301398403a7841ef7..e0d23da707c771fc887049480b3cb154277f2628 100644 (file)
@@ -2,14 +2,22 @@
 
 See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases
 """
+from test.support import requires
+
+import itertools
+import bisect
 
 import copy
 import decimal
 import sys
+import os
 import pickle
 import random
+import struct
 import unittest
 
+from array import array
+
 from operator import lt, le, gt, ge, eq, ne, truediv, floordiv, mod
 
 from test import support
@@ -1592,6 +1600,10 @@ class TestDateTime(TestDate):
         self.assertEqual(t.isoformat(' '), "0002-03-02 00:00:00")
         # str is ISO format with the separator forced to a blank.
         self.assertEqual(str(t), "0002-03-02 00:00:00")
+        # ISO format with timezone
+        tz = FixedOffset(timedelta(seconds=16), 'XXX')
+        t = self.theclass(2, 3, 2, tzinfo=tz)
+        self.assertEqual(t.isoformat(), "0002-03-02T00:00:00+00:00:16")
 
     def test_format(self):
         dt = self.theclass(2007, 9, 10, 4, 5, 1, 123)
@@ -1711,6 +1723,9 @@ class TestDateTime(TestDate):
         self.assertRaises(ValueError, self.theclass,
                           2000, 1, 31, 23, 59, 59,
                           1000000)
+        # Positional fold:
+        self.assertRaises(TypeError, self.theclass,
+                          2000, 1, 31, 23, 59, 59, 0, None, 1)
 
     def test_hash_equality(self):
         d = self.theclass(2000, 12, 31, 23, 30, 17)
@@ -1894,16 +1909,20 @@ class TestDateTime(TestDate):
         t = self.theclass(1970, 1, 1, 1, 2, 3, 4)
         self.assertEqual(t.timestamp(),
                          18000.0 + 3600 + 2*60 + 3 + 4*1e-6)
-        # Missing hour may produce platform-dependent result
-        t = self.theclass(2012, 3, 11, 2, 30)
-        self.assertIn(self.theclass.fromtimestamp(t.timestamp()),
-                      [t - timedelta(hours=1), t + timedelta(hours=1)])
+        # Missing hour
+        t0 = self.theclass(2012, 3, 11, 2, 30)
+        t1 = t0.replace(fold=1)
+        self.assertEqual(self.theclass.fromtimestamp(t1.timestamp()),
+                         t0 - timedelta(hours=1))
+        self.assertEqual(self.theclass.fromtimestamp(t0.timestamp()),
+                         t1 + timedelta(hours=1))
         # Ambiguous hour defaults to DST
         t = self.theclass(2012, 11, 4, 1, 30)
         self.assertEqual(self.theclass.fromtimestamp(t.timestamp()), t)
 
         # Timestamp may raise an overflow error on some platforms
-        for t in [self.theclass(1,1,1), self.theclass(9999,12,12)]:
+        # XXX: Do we care to support the first and last year?
+        for t in [self.theclass(2,1,1), self.theclass(9998,12,12)]:
             try:
                 s = t.timestamp()
             except OverflowError:
@@ -1922,6 +1941,7 @@ class TestDateTime(TestDate):
         self.assertEqual(t.timestamp(),
                          18000 + 3600 + 2*60 + 3 + 4*1e-6)
 
+    @support.run_with_tz('MSK-03')  # Something east of Greenwich
     def test_microsecond_rounding(self):
         for fts in [self.theclass.fromtimestamp,
                     self.theclass.utcfromtimestamp]:
@@ -2127,6 +2147,7 @@ class TestDateTime(TestDate):
         self.assertRaises(ValueError, base.replace, year=2001)
 
     def test_astimezone(self):
+        return  # The rest is no longer applicable
         # Pretty boring!  The TZ test is more interesting here.  astimezone()
         # simply can't be applied to a naive object.
         dt = self.theclass.now()
@@ -2619,9 +2640,9 @@ class TZInfoBase:
         self.assertRaises(ValueError, t.utcoffset)
         self.assertRaises(ValueError, t.dst)
 
-        # Not a whole number of minutes.
+        # Not a whole number of seconds.
         class C7(tzinfo):
-            def utcoffset(self, dt): return timedelta(seconds=61)
+            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)
@@ -3994,5 +4015,777 @@ class Oddballs(unittest.TestCase):
         with self.assertRaises(TypeError):
             datetime(10, 10, 10, 10, 10, 10, 10.)
 
+#############################################################################
+# Local Time Disambiguation
+
+# An experimental reimplementation of fromutc that respects the "fold" flag.
+
+class tzinfo2(tzinfo):
+
+    def fromutc(self, dt):
+        "datetime in UTC -> datetime in local time."
+
+        if not isinstance(dt, datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+        # Returned value satisfies
+        #          dt + ldt.utcoffset() = ldt
+        off0 = dt.replace(fold=0).utcoffset()
+        off1 = dt.replace(fold=1).utcoffset()
+        if off0 is None or off1 is None or dt.dst() is None:
+            raise ValueError
+        if off0 == off1:
+            ldt = dt + off0
+            off1 = ldt.utcoffset()
+            if off0 == off1:
+                return ldt
+        # Now, we discovered both possible offsets, so
+        # we can just try four possible solutions:
+        for off in [off0, off1]:
+            ldt = dt + off
+            if ldt.utcoffset() == off:
+                return ldt
+            ldt = ldt.replace(fold=1)
+            if ldt.utcoffset() == off:
+                return ldt
+
+        raise ValueError("No suitable local time found")
+
+# Reimplementing simplified US timezones to respect the "fold" flag:
+
+class USTimeZone2(tzinfo2):
+
+    def __init__(self, hours, reprname, stdname, dstname):
+        self.stdoffset = timedelta(hours=hours)
+        self.reprname = reprname
+        self.stdname = stdname
+        self.dstname = dstname
+
+    def __repr__(self):
+        return self.reprname
+
+    def tzname(self, dt):
+        if self.dst(dt):
+            return self.dstname
+        else:
+            return self.stdname
+
+    def utcoffset(self, dt):
+        return self.stdoffset + self.dst(dt)
+
+    def dst(self, dt):
+        if dt is None or dt.tzinfo is None:
+            # An exception instead may be sensible here, in one or more of
+            # the cases.
+            return ZERO
+        assert dt.tzinfo is self
+
+        # Find first Sunday in April.
+        start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
+        assert start.weekday() == 6 and start.month == 4 and start.day <= 7
+
+        # Find last Sunday in October.
+        end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
+        assert end.weekday() == 6 and end.month == 10 and end.day >= 25
+
+        # Can't compare naive to aware objects, so strip the timezone from
+        # dt first.
+        dt = dt.replace(tzinfo=None)
+        if start + HOUR <= dt < end:
+            # DST is in effect.
+            return HOUR
+        elif end <= dt < end + HOUR:
+            # Fold (an ambiguous hour): use dt.fold to disambiguate.
+            return ZERO if dt.fold else HOUR
+        elif start <= dt < start + HOUR:
+            # Gap (a non-existent hour): reverse the fold rule.
+            return HOUR if dt.fold else ZERO
+        else:
+            # DST is off.
+            return ZERO
+
+Eastern2  = USTimeZone2(-5, "Eastern2",  "EST", "EDT")
+Central2  = USTimeZone2(-6, "Central2",  "CST", "CDT")
+Mountain2 = USTimeZone2(-7, "Mountain2", "MST", "MDT")
+Pacific2  = USTimeZone2(-8, "Pacific2",  "PST", "PDT")
+
+# Europe_Vilnius_1941 tzinfo implementation reproduces the following
+# 1941 transition from Olson's tzdist:
+#
+# Zone NAME           GMTOFF RULES  FORMAT [UNTIL]
+# ZoneEurope/Vilnius  1:00   -      CET    1940 Aug  3
+#                     3:00   -      MSK    1941 Jun 24
+#                     1:00   C-Eur  CE%sT  1944 Aug
+#
+# $ zdump -v Europe/Vilnius | grep 1941
+# Europe/Vilnius  Mon Jun 23 20:59:59 1941 UTC = Mon Jun 23 23:59:59 1941 MSK isdst=0 gmtoff=10800
+# Europe/Vilnius  Mon Jun 23 21:00:00 1941 UTC = Mon Jun 23 23:00:00 1941 CEST isdst=1 gmtoff=7200
+
+class Europe_Vilnius_1941(tzinfo):
+    def _utc_fold(self):
+        return [datetime(1941, 6, 23, 21, tzinfo=self),  # Mon Jun 23 21:00:00 1941 UTC
+                datetime(1941, 6, 23, 22, tzinfo=self)]  # Mon Jun 23 22:00:00 1941 UTC
+
+    def _loc_fold(self):
+        return [datetime(1941, 6, 23, 23, tzinfo=self),  # Mon Jun 23 23:00:00 1941 MSK / CEST
+                datetime(1941, 6, 24, 0, tzinfo=self)]   # Mon Jun 24 00:00:00 1941 CEST
+
+    def utcoffset(self, dt):
+        fold_start, fold_stop = self._loc_fold()
+        if dt < fold_start:
+            return 3 * HOUR
+        if dt < fold_stop:
+            return (2 if dt.fold else 3) * HOUR
+        # if dt >= fold_stop
+        return 2 * HOUR
+
+    def dst(self, dt):
+        fold_start, fold_stop = self._loc_fold()
+        if dt < fold_start:
+            return 0 * HOUR
+        if dt < fold_stop:
+            return (1 if dt.fold else 0) * HOUR
+        # if dt >= fold_stop
+        return 1 * HOUR
+
+    def tzname(self, dt):
+        fold_start, fold_stop = self._loc_fold()
+        if dt < fold_start:
+            return 'MSK'
+        if dt < fold_stop:
+            return ('MSK', 'CEST')[dt.fold]
+        # if dt >= fold_stop
+        return 'CEST'
+
+    def fromutc(self, dt):
+        assert dt.fold == 0
+        assert dt.tzinfo is self
+        if dt.year != 1941:
+            raise NotImplementedError
+        fold_start, fold_stop = self._utc_fold()
+        if dt < fold_start:
+            return dt + 3 * HOUR
+        if dt < fold_stop:
+            return (dt + 2 * HOUR).replace(fold=1)
+        # if dt >= fold_stop
+        return dt + 2 * HOUR
+
+
+class TestLocalTimeDisambiguation(unittest.TestCase):
+
+    def test_vilnius_1941_fromutc(self):
+        Vilnius = Europe_Vilnius_1941()
+
+        gdt = datetime(1941, 6, 23, 20, 59, 59, tzinfo=timezone.utc)
+        ldt = gdt.astimezone(Vilnius)
+        self.assertEqual(ldt.strftime("%c %Z%z"),
+                         'Mon Jun 23 23:59:59 1941 MSK+0300')
+        self.assertEqual(ldt.fold, 0)
+        self.assertFalse(ldt.dst())
+
+        gdt = datetime(1941, 6, 23, 21, tzinfo=timezone.utc)
+        ldt = gdt.astimezone(Vilnius)
+        self.assertEqual(ldt.strftime("%c %Z%z"),
+                         'Mon Jun 23 23:00:00 1941 CEST+0200')
+        self.assertEqual(ldt.fold, 1)
+        self.assertTrue(ldt.dst())
+
+        gdt = datetime(1941, 6, 23, 22, tzinfo=timezone.utc)
+        ldt = gdt.astimezone(Vilnius)
+        self.assertEqual(ldt.strftime("%c %Z%z"),
+                         'Tue Jun 24 00:00:00 1941 CEST+0200')
+        self.assertEqual(ldt.fold, 0)
+        self.assertTrue(ldt.dst())
+
+    def test_vilnius_1941_toutc(self):
+        Vilnius = Europe_Vilnius_1941()
+
+        ldt = datetime(1941, 6, 23, 22, 59, 59, tzinfo=Vilnius)
+        gdt = ldt.astimezone(timezone.utc)
+        self.assertEqual(gdt.strftime("%c %Z"),
+                         'Mon Jun 23 19:59:59 1941 UTC')
+
+        ldt = datetime(1941, 6, 23, 23, 59, 59, tzinfo=Vilnius)
+        gdt = ldt.astimezone(timezone.utc)
+        self.assertEqual(gdt.strftime("%c %Z"),
+                         'Mon Jun 23 20:59:59 1941 UTC')
+
+        ldt = datetime(1941, 6, 23, 23, 59, 59, tzinfo=Vilnius, fold=1)
+        gdt = ldt.astimezone(timezone.utc)
+        self.assertEqual(gdt.strftime("%c %Z"),
+                         'Mon Jun 23 21:59:59 1941 UTC')
+
+        ldt = datetime(1941, 6, 24, 0, tzinfo=Vilnius)
+        gdt = ldt.astimezone(timezone.utc)
+        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)
+        self.assertEqual(t.fold, 1)
+        self.assertEqual(dt.fold, 1)
+        with self.assertRaises(TypeError):
+            time(0, 0, 0, 0, None, 0)
+
+    def test_member(self):
+        dt = datetime(1, 1, 1, fold=1)
+        t = dt.time()
+        self.assertEqual(t.fold, 1)
+        t = dt.timetz()
+        self.assertEqual(t.fold, 1)
+
+    def test_replace(self):
+        t = time(0)
+        dt = datetime(1, 1, 1)
+        self.assertEqual(t.replace(fold=1).fold, 1)
+        self.assertEqual(dt.replace(fold=1).fold, 1)
+        self.assertEqual(t.replace(fold=0).fold, 0)
+        self.assertEqual(dt.replace(fold=0).fold, 0)
+        # Check that replacement of other fields does not change "fold".
+        t = t.replace(fold=1, tzinfo=Eastern)
+        dt = dt.replace(fold=1, tzinfo=Eastern)
+        self.assertEqual(t.replace(tzinfo=None).fold, 1)
+        self.assertEqual(dt.replace(tzinfo=None).fold, 1)
+        # Check that fold is a keyword-only argument
+        with self.assertRaises(TypeError):
+            t.replace(1, 1, 1, None, 1)
+        with self.assertRaises(TypeError):
+            dt.replace(1, 1, 1, 1, 1, 1, 1, None, 1)
+
+    def test_comparison(self):
+        t = time(0)
+        dt = datetime(1, 1, 1)
+        self.assertEqual(t, t.replace(fold=1))
+        self.assertEqual(dt, dt.replace(fold=1))
+
+    def test_hash(self):
+        t = time(0)
+        dt = datetime(1, 1, 1)
+        self.assertEqual(hash(t), hash(t.replace(fold=1)))
+        self.assertEqual(hash(dt), hash(dt.replace(fold=1)))
+
+    @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
+    def test_fromtimestamp(self):
+        s = 1414906200
+        dt0 = datetime.fromtimestamp(s)
+        dt1 = datetime.fromtimestamp(s + 3600)
+        self.assertEqual(dt0.fold, 0)
+        self.assertEqual(dt1.fold, 1)
+
+    @support.run_with_tz('Australia/Lord_Howe')
+    def test_fromtimestamp_lord_howe(self):
+        tm = _time.localtime(1.4e9)
+        if _time.strftime('%Z%z', tm) != 'LHST+1030':
+            self.skipTest('Australia/Lord_Howe timezone is not supported on this platform')
+        # $ TZ=Australia/Lord_Howe date -r 1428158700
+        # Sun Apr  5 01:45:00 LHDT 2015
+        # $ TZ=Australia/Lord_Howe date -r 1428160500
+        # Sun Apr  5 01:45:00 LHST 2015
+        s = 1428158700
+        t0 = datetime.fromtimestamp(s)
+        t1 = datetime.fromtimestamp(s + 1800)
+        self.assertEqual(t0, t1)
+        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)
+        dt1 = dt0.replace(fold=1)
+        self.assertEqual(dt0.timestamp() + 3600,
+                         dt1.timestamp())
+
+    @support.run_with_tz('Australia/Lord_Howe')
+    def test_timestamp_lord_howe(self):
+        tm = _time.localtime(1.4e9)
+        if _time.strftime('%Z%z', tm) != 'LHST+1030':
+            self.skipTest('Australia/Lord_Howe timezone is not supported on this platform')
+        t = datetime(2015, 4, 5, 1, 45)
+        s0 = t.replace(fold=0).timestamp()
+        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)
+        dt1 = dt0.replace(fold=1)
+        # Convert both naive instances to aware.
+        adt0 = dt0.astimezone()
+        adt1 = dt1.astimezone()
+        # Check that the first instance in DST zone and the second in STD
+        self.assertEqual(adt0.tzname(), 'EDT')
+        self.assertEqual(adt1.tzname(), 'EST')
+        self.assertEqual(adt0 + HOUR, adt1)
+        # Aware instances with fixed offset tzinfo's always have fold=0
+        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)
+        for pickler, unpickler, proto in pickle_choices:
+            for x in [t, dt]:
+                s = pickler.dumps(x, proto)
+                y = unpickler.loads(s)
+                self.assertEqual(x, y)
+                self.assertEqual((0 if proto < 4 else x.fold), y.fold)
+
+    def test_repr(self):
+        t = time(fold=1)
+        dt = datetime(1, 1, 1, fold=1)
+        self.assertEqual(repr(t), 'datetime.time(0, 0, fold=1)')
+        self.assertEqual(repr(dt),
+                         'datetime.datetime(1, 1, 1, 0, 0, fold=1)')
+
+    def test_dst(self):
+        # Let's first establish that things work in regular times.
+        dt_summer = datetime(2002, 10, 27, 1, tzinfo=Eastern2) - timedelta.resolution
+        dt_winter = datetime(2002, 10, 27, 2, tzinfo=Eastern2)
+        self.assertEqual(dt_summer.dst(), HOUR)
+        self.assertEqual(dt_winter.dst(), ZERO)
+        # The disambiguation flag is ignored
+        self.assertEqual(dt_summer.replace(fold=1).dst(), HOUR)
+        self.assertEqual(dt_winter.replace(fold=1).dst(), ZERO)
+
+        # Pick local time in the fold.
+        for minute in [0, 30, 59]:
+            dt = datetime(2002, 10, 27, 1, minute, tzinfo=Eastern2)
+            # With fold=0 (the default) it is in DST.
+            self.assertEqual(dt.dst(), HOUR)
+            # With fold=1 it is in STD.
+            self.assertEqual(dt.replace(fold=1).dst(), ZERO)
+
+        # Pick local time in the gap.
+        for minute in [0, 30, 59]:
+            dt = datetime(2002, 4, 7, 2, minute, tzinfo=Eastern2)
+            # With fold=0 (the default) it is in STD.
+            self.assertEqual(dt.dst(), ZERO)
+            # With fold=1 it is in DST.
+            self.assertEqual(dt.replace(fold=1).dst(), HOUR)
+
+
+    def test_utcoffset(self):
+        # Let's first establish that things work in regular times.
+        dt_summer = datetime(2002, 10, 27, 1, tzinfo=Eastern2) - timedelta.resolution
+        dt_winter = datetime(2002, 10, 27, 2, tzinfo=Eastern2)
+        self.assertEqual(dt_summer.utcoffset(), -4 * HOUR)
+        self.assertEqual(dt_winter.utcoffset(), -5 * HOUR)
+        # The disambiguation flag is ignored
+        self.assertEqual(dt_summer.replace(fold=1).utcoffset(), -4 * HOUR)
+        self.assertEqual(dt_winter.replace(fold=1).utcoffset(), -5 * HOUR)
+
+    def test_fromutc(self):
+        # Let's first establish that things work in regular times.
+        u_summer = datetime(2002, 10, 27, 6, tzinfo=Eastern2) - timedelta.resolution
+        u_winter = datetime(2002, 10, 27, 7, tzinfo=Eastern2)
+        t_summer = Eastern2.fromutc(u_summer)
+        t_winter = Eastern2.fromutc(u_winter)
+        self.assertEqual(t_summer, u_summer - 4 * HOUR)
+        self.assertEqual(t_winter, u_winter - 5 * HOUR)
+        self.assertEqual(t_summer.fold, 0)
+        self.assertEqual(t_winter.fold, 0)
+
+        # What happens in the fall-back fold?
+        u = datetime(2002, 10, 27, 5, 30, tzinfo=Eastern2)
+        t0 = Eastern2.fromutc(u)
+        u += HOUR
+        t1 = Eastern2.fromutc(u)
+        self.assertEqual(t0, t1)
+        self.assertEqual(t0.fold, 0)
+        self.assertEqual(t1.fold, 1)
+        # The tricky part is when u is in the local fold:
+        u = datetime(2002, 10, 27, 1, 30, tzinfo=Eastern2)
+        t = Eastern2.fromutc(u)
+        self.assertEqual((t.day, t.hour), (26, 21))
+        # .. or gets into the local fold after a standard time adjustment
+        u = datetime(2002, 10, 27, 6, 30, tzinfo=Eastern2)
+        t = Eastern2.fromutc(u)
+        self.assertEqual((t.day, t.hour), (27, 1))
+
+        # What happens in the spring-forward gap?
+        u = datetime(2002, 4, 7, 2, 0, tzinfo=Eastern2)
+        t = Eastern2.fromutc(u)
+        self.assertEqual((t.day, t.hour), (6, 21))
+
+    def test_mixed_compare_regular(self):
+        t = datetime(2000, 1, 1, tzinfo=Eastern2)
+        self.assertEqual(t, t.astimezone(timezone.utc))
+        t = datetime(2000, 6, 1, tzinfo=Eastern2)
+        self.assertEqual(t, t.astimezone(timezone.utc))
+
+    def test_mixed_compare_fold(self):
+        t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2)
+        t_fold_utc = t_fold.astimezone(timezone.utc)
+        self.assertNotEqual(t_fold, t_fold_utc)
+
+    def test_mixed_compare_gap(self):
+        t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2)
+        t_gap_utc = t_gap.astimezone(timezone.utc)
+        self.assertNotEqual(t_gap, t_gap_utc)
+
+    def test_hash_aware(self):
+        t = datetime(2000, 1, 1, tzinfo=Eastern2)
+        self.assertEqual(hash(t), hash(t.replace(fold=1)))
+        t_fold = datetime(2002, 10, 27, 1, 45, tzinfo=Eastern2)
+        t_gap = datetime(2002, 4, 7, 2, 45, tzinfo=Eastern2)
+        self.assertEqual(hash(t_fold), hash(t_fold.replace(fold=1)))
+        self.assertEqual(hash(t_gap), hash(t_gap.replace(fold=1)))
+
+SEC = timedelta(0, 1)
+
+def pairs(iterable):
+    a, b = itertools.tee(iterable)
+    next(b, None)
+    return zip(a, b)
+
+class ZoneInfo(tzinfo):
+    zoneroot = '/usr/share/zoneinfo'
+    def __init__(self, ut, ti):
+        """
+
+        :param ut: array
+            Array of transition point timestamps
+        :param ti: list
+            A list of (offset, isdst, abbr) tuples
+        :return: None
+        """
+        self.ut = ut
+        self.ti = ti
+        self.lt = self.invert(ut, ti)
+
+    @staticmethod
+    def invert(ut, ti):
+        lt = (ut.__copy__(), ut.__copy__())
+        if ut:
+            offset = ti[0][0] // SEC
+            lt[0][0] = max(-2**31, lt[0][0] + offset)
+            lt[1][0] = max(-2**31, lt[1][0] + offset)
+            for i in range(1, len(ut)):
+                lt[0][i] += ti[i-1][0] // SEC
+                lt[1][i] += ti[i][0] // SEC
+        return lt
+
+    @classmethod
+    def fromfile(cls, fileobj):
+        if fileobj.read(4).decode() != "TZif":
+            raise ValueError("not a zoneinfo file")
+        fileobj.seek(32)
+        counts = array('i')
+        counts.fromfile(fileobj, 3)
+        if sys.byteorder != 'big':
+            counts.byteswap()
+
+        ut = array('i')
+        ut.fromfile(fileobj, counts[0])
+        if sys.byteorder != 'big':
+            ut.byteswap()
+
+        type_indices = array('B')
+        type_indices.fromfile(fileobj, counts[0])
+
+        ttis = []
+        for i in range(counts[1]):
+            ttis.append(struct.unpack(">lbb", fileobj.read(6)))
+
+        abbrs = fileobj.read(counts[2])
+
+        # Convert ttis
+        for i, (gmtoff, isdst, abbrind) in enumerate(ttis):
+            abbr = abbrs[abbrind:abbrs.find(0, abbrind)].decode()
+            ttis[i] = (timedelta(0, gmtoff), isdst, abbr)
+
+        ti = [None] * len(ut)
+        for i, idx in enumerate(type_indices):
+            ti[i] = ttis[idx]
+
+        self = cls(ut, ti)
+
+        return self
+
+    @classmethod
+    def fromname(cls, name):
+        path = os.path.join(cls.zoneroot, name)
+        with open(path, 'rb') as f:
+            return cls.fromfile(f)
+
+    EPOCHORDINAL = date(1970, 1, 1).toordinal()
+
+    def fromutc(self, dt):
+        """datetime in UTC -> datetime in local time."""
+
+        if not isinstance(dt, datetime):
+            raise TypeError("fromutc() requires a datetime argument")
+        if dt.tzinfo is not self:
+            raise ValueError("dt.tzinfo is not self")
+
+        timestamp = ((dt.toordinal() - self.EPOCHORDINAL) * 86400
+                     + dt.hour * 3600
+                     + dt.minute * 60
+                     + dt.second)
+
+        if timestamp < self.ut[1]:
+            tti = self.ti[0]
+            fold = 0
+        else:
+            idx = bisect.bisect_right(self.ut, timestamp)
+            assert self.ut[idx-1] <= timestamp
+            assert idx == len(self.ut) or timestamp < self.ut[idx]
+            tti_prev, tti = self.ti[idx-2:idx]
+            # Detect fold
+            shift = tti_prev[0] - tti[0]
+            fold = (shift > timedelta(0, timestamp - self.ut[idx-1]))
+        dt += tti[0]
+        if fold:
+            return dt.replace(fold=1)
+        else:
+            return dt
+
+    def _find_ti(self, dt, i):
+        timestamp = ((dt.toordinal() - self.EPOCHORDINAL) * 86400
+             + dt.hour * 3600
+             + dt.minute * 60
+             + dt.second)
+        lt = self.lt[dt.fold]
+        idx = bisect.bisect_right(lt, timestamp)
+
+        return self.ti[max(0, idx - 1)][i]
+
+    def utcoffset(self, dt):
+        return self._find_ti(dt, 0)
+
+    def dst(self, dt):
+        isdst = self._find_ti(dt, 1)
+        # XXX: We cannot accurately determine the "save" value,
+        # so let's return 1h whenever DST is in effect.  Since
+        # we don't use dst() in fromutc(), it is unlikely that
+        # it will be needed for anything more than bool(dst()).
+        return ZERO if isdst else HOUR
+
+    def tzname(self, dt):
+        return self._find_ti(dt, 2)
+
+    @classmethod
+    def zonenames(cls, zonedir=None):
+        if zonedir is None:
+            zonedir = cls.zoneroot
+        for root, _, files in os.walk(zonedir):
+            for f in files:
+                p = os.path.join(root, f)
+                with open(p, 'rb') as o:
+                    magic =  o.read(4)
+                if magic == b'TZif':
+                    yield p[len(zonedir) + 1:]
+
+    @classmethod
+    def stats(cls, start_year=1):
+        count = gap_count = fold_count = zeros_count = 0
+        min_gap = min_fold = timedelta.max
+        max_gap = max_fold = ZERO
+        min_gap_datetime = max_gap_datetime = datetime.min
+        min_gap_zone = max_gap_zone = None
+        min_fold_datetime = max_fold_datetime = datetime.min
+        min_fold_zone = max_fold_zone = None
+        stats_since = datetime(start_year, 1, 1) # Starting from 1970 eliminates a lot of noise
+        for zonename in cls.zonenames():
+            count += 1
+            tz = cls.fromname(zonename)
+            for dt, shift in tz.transitions():
+                if dt < stats_since:
+                    continue
+                if shift > ZERO:
+                    gap_count += 1
+                    if (shift, dt) > (max_gap, max_gap_datetime):
+                        max_gap = shift
+                        max_gap_zone = zonename
+                        max_gap_datetime = dt
+                    if (shift, datetime.max - dt) < (min_gap, datetime.max - min_gap_datetime):
+                        min_gap = shift
+                        min_gap_zone = zonename
+                        min_gap_datetime = dt
+                elif shift < ZERO:
+                    fold_count += 1
+                    shift = -shift
+                    if (shift, dt) > (max_fold, max_fold_datetime):
+                        max_fold = shift
+                        max_fold_zone = zonename
+                        max_fold_datetime = dt
+                    if (shift, datetime.max - dt) < (min_fold, datetime.max - min_fold_datetime):
+                        min_fold = shift
+                        min_fold_zone = zonename
+                        min_fold_datetime = dt
+                else:
+                    zeros_count += 1
+        trans_counts = (gap_count, fold_count, zeros_count)
+        print("Number of zones:       %5d" % count)
+        print("Number of transitions: %5d = %d (gaps) + %d (folds) + %d (zeros)" %
+              ((sum(trans_counts),) + trans_counts))
+        print("Min gap:         %16s at %s in %s" % (min_gap, min_gap_datetime, min_gap_zone))
+        print("Max gap:         %16s at %s in %s" % (max_gap, max_gap_datetime, max_gap_zone))
+        print("Min fold:        %16s at %s in %s" % (min_fold, min_fold_datetime, min_fold_zone))
+        print("Max fold:        %16s at %s in %s" % (max_fold, max_fold_datetime, max_fold_zone))
+
+
+    def transitions(self):
+        for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)):
+            shift = ti[0] - prev_ti[0]
+            yield datetime.utcfromtimestamp(t), shift
+
+    def nondst_folds(self):
+        """Find all folds with the same value of isdst on both sides of the transition."""
+        for (_, prev_ti), (t, ti) in pairs(zip(self.ut, self.ti)):
+            shift = ti[0] - prev_ti[0]
+            if shift < ZERO and ti[1] == prev_ti[1]:
+                yield datetime.utcfromtimestamp(t), -shift, prev_ti[2], ti[2]
+
+    @classmethod
+    def print_all_nondst_folds(cls, same_abbr=False, start_year=1):
+        count = 0
+        for zonename in cls.zonenames():
+            tz = cls.fromname(zonename)
+            for dt, shift, prev_abbr, abbr in tz.nondst_folds():
+                if dt.year < start_year or same_abbr and prev_abbr != abbr:
+                    continue
+                count += 1
+                print("%3d) %-30s %s %10s %5s -> %s" %
+                      (count, zonename, dt, shift, prev_abbr, abbr))
+
+    def folds(self):
+        for t, shift in self.transitions():
+            if shift < ZERO:
+                yield t, -shift
+
+    def gaps(self):
+        for t, shift in self.transitions():
+            if shift > ZERO:
+                yield t, shift
+
+    def zeros(self):
+        for t, shift in self.transitions():
+            if not shift:
+                yield t
+
+
+class ZoneInfoTest(unittest.TestCase):
+    zonename = 'America/New_York'
+
+    def setUp(self):
+        if sys.platform == "win32":
+            self.skipTest("Skipping zoneinfo tests on Windows")
+        self.tz = ZoneInfo.fromname(self.zonename)
+
+    def assertEquivDatetimes(self, a, b):
+        self.assertEqual((a.replace(tzinfo=None), a.fold, id(a.tzinfo)),
+                         (b.replace(tzinfo=None), b.fold, id(b.tzinfo)))
+
+    def test_folds(self):
+        tz = self.tz
+        for dt, shift in tz.folds():
+            for x in [0 * shift, 0.5 * shift, shift - timedelta.resolution]:
+                udt = dt + x
+                ldt = tz.fromutc(udt.replace(tzinfo=tz))
+                self.assertEqual(ldt.fold, 1)
+                adt = udt.replace(tzinfo=timezone.utc).astimezone(tz)
+                self.assertEquivDatetimes(adt, ldt)
+                utcoffset = ldt.utcoffset()
+                self.assertEqual(ldt.replace(tzinfo=None), udt + utcoffset)
+                # Round trip
+                self.assertEquivDatetimes(ldt.astimezone(timezone.utc),
+                                          udt.replace(tzinfo=timezone.utc))
+
+
+            for x in [-timedelta.resolution, shift]:
+                udt = dt + x
+                udt = udt.replace(tzinfo=tz)
+                ldt = tz.fromutc(udt)
+                self.assertEqual(ldt.fold, 0)
+
+    def test_gaps(self):
+        tz = self.tz
+        for dt, shift in tz.gaps():
+            for x in [0 * shift, 0.5 * shift, shift - timedelta.resolution]:
+                udt = dt + x
+                udt = udt.replace(tzinfo=tz)
+                ldt = tz.fromutc(udt)
+                self.assertEqual(ldt.fold, 0)
+                adt = udt.replace(tzinfo=timezone.utc).astimezone(tz)
+                self.assertEquivDatetimes(adt, ldt)
+                utcoffset = ldt.utcoffset()
+                self.assertEqual(ldt.replace(tzinfo=None), udt.replace(tzinfo=None) + utcoffset)
+                # Create a local time inside the gap
+                ldt = tz.fromutc(dt.replace(tzinfo=tz)) - shift + x
+                self.assertLess(ldt.replace(fold=1).utcoffset(),
+                                ldt.replace(fold=0).utcoffset(),
+                                "At %s." % ldt)
+
+            for x in [-timedelta.resolution, shift]:
+                udt = dt + x
+                ldt = tz.fromutc(udt.replace(tzinfo=tz))
+                self.assertEqual(ldt.fold, 0)
+
+    def test_system_transitions(self):
+        if ('Riyadh8' in self.zonename or
+            # From tzdata NEWS file:
+            # The files solar87, solar88, and solar89 are no longer distributed.
+            # They were a negative experiment - that is, a demonstration that
+            # tz data can represent solar time only with some difficulty and error.
+            # Their presence in the distribution caused confusion, as Riyadh
+            # civil time was generally not solar time in those years.
+                self.zonename.startswith('right/')):
+            self.skipTest("Skipping %s" % self.zonename)
+        tz = ZoneInfo.fromname(self.zonename)
+        TZ = os.environ.get('TZ')
+        os.environ['TZ'] = self.zonename
+        try:
+            _time.tzset()
+            for udt, shift in tz.transitions():
+                if self.zonename == 'Europe/Tallinn' and udt.date() == date(1999, 10, 31):
+                    print("Skip %s %s transition" % (self.zonename, udt))
+                    continue
+                s0 = (udt - datetime(1970, 1, 1)) // SEC
+                ss = shift // SEC   # shift seconds
+                for x in [-40 * 3600, -20*3600, -1, 0,
+                          ss - 1, ss + 20 * 3600, ss + 40 * 3600]:
+                    s = s0 + x
+                    sdt = datetime.fromtimestamp(s)
+                    tzdt = datetime.fromtimestamp(s, tz).replace(tzinfo=None)
+                    self.assertEquivDatetimes(sdt, tzdt)
+                    s1 = sdt.timestamp()
+                    self.assertEqual(s, s1)
+                if ss > 0:  # gap
+                    # Create local time inside the gap
+                    dt = datetime.fromtimestamp(s0) - shift / 2
+                    ts0 = dt.timestamp()
+                    ts1 = dt.replace(fold=1).timestamp()
+                    self.assertEqual(ts0, s0 + ss / 2)
+                    self.assertEqual(ts1, s0 - ss / 2)
+        finally:
+            if TZ is None:
+                del os.environ['TZ']
+            else:
+                os.environ['TZ'] = TZ
+            _time.tzset()
+
+
+class ZoneInfoCompleteTest(unittest.TestCase):
+    def test_all(self):
+        requires('tzdata', 'test requires tzdata and a long time to run')
+        for name in ZoneInfo.zonenames():
+            class Test(ZoneInfoTest):
+                zonename = name
+            for suffix in ['folds', 'gaps', 'system_transitions']:
+                test = Test('test_' + suffix)
+                result = test.run()
+                self.assertTrue(result.wasSuccessful(), name + ' ' + suffix)
+
+# Iran had a sub-minute UTC offset before 1946.
+class IranTest(ZoneInfoTest):
+    zonename = 'Iran'
+
 if __name__ == "__main__":
     unittest.main()
index de09a0122abb59d8663cc2dc6c9f3b89a50dcbc4..f2ec0bd4d237638cfbfbf1c1c23b4f0c04c650e9 100644 (file)
@@ -112,6 +112,8 @@ resources to test.  Currently only the following are defined:
 
     gui -       Run tests that require a running GUI.
 
+    tzdata -    Run tests that require timezone data.
+
 To enable all resources except one, use '-uall,-<resource>'.  For
 example, to run all the tests except for the gui tests, give the
 option '-uall,-gui'.
@@ -119,7 +121,7 @@ option '-uall,-gui'.
 
 
 RESOURCE_NAMES = ('audio', 'curses', 'largefile', 'network',
-                  'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui')
+                  'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui', 'tzdata')
 
 class _ArgParser(argparse.ArgumentParser):
 
index c56be7537d87fb534154588b548f278b84b54135..c4cc66024be4be6bc21cf1caab13dcbb4627d54f 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -26,6 +26,8 @@ Core and Builtins
 Library
 -------
 
+- Issue #24773: Implemented PEP 495 (Local Time Disambiguation).
+
 - Expose the EPOLLEXCLUSIVE constant (when it is defined) in the select module.
 
 - Issue #27567: Expose the EPOLLRDHUP and POLLRDHUP constants in the select
index 61b66a66cbea0bf22537b5feb68e1e085b02769c..1157859ae3f77156532308fe42418cb78091a7bd 100644 (file)
@@ -9,6 +9,12 @@
 
 #ifdef MS_WINDOWS
 #  include <winsock2.h>         /* struct timeval */
+static struct tm *localtime_r(const time_t *timep, struct tm *result)
+{
+    if (localtime_s(result, timep) == 0)
+        return result;
+    return NULL;
+}
 #endif
 
 /* Differentiate between building the core module and building extension
@@ -56,6 +62,7 @@ class datetime.datetime "PyDateTime_DateTime *" "&PyDateTime_DateTimeType"
 #define DATE_GET_MINUTE         PyDateTime_DATE_GET_MINUTE
 #define DATE_GET_SECOND         PyDateTime_DATE_GET_SECOND
 #define DATE_GET_MICROSECOND    PyDateTime_DATE_GET_MICROSECOND
+#define DATE_GET_FOLD           PyDateTime_DATE_GET_FOLD
 
 /* Date accessors for date and datetime. */
 #define SET_YEAR(o, v)          (((o)->data[0] = ((v) & 0xff00) >> 8), \
@@ -71,12 +78,14 @@ class datetime.datetime "PyDateTime_DateTime *" "&PyDateTime_DateTimeType"
     (((o)->data[7] = ((v) & 0xff0000) >> 16), \
      ((o)->data[8] = ((v) & 0x00ff00) >> 8), \
      ((o)->data[9] = ((v) & 0x0000ff)))
+#define DATE_SET_FOLD(o, v)   (PyDateTime_DATE_GET_FOLD(o) = (v))
 
 /* Time accessors for time. */
 #define TIME_GET_HOUR           PyDateTime_TIME_GET_HOUR
 #define TIME_GET_MINUTE         PyDateTime_TIME_GET_MINUTE
 #define TIME_GET_SECOND         PyDateTime_TIME_GET_SECOND
 #define TIME_GET_MICROSECOND    PyDateTime_TIME_GET_MICROSECOND
+#define TIME_GET_FOLD           PyDateTime_TIME_GET_FOLD
 #define TIME_SET_HOUR(o, v)     (PyDateTime_TIME_GET_HOUR(o) = (v))
 #define TIME_SET_MINUTE(o, v)   (PyDateTime_TIME_GET_MINUTE(o) = (v))
 #define TIME_SET_SECOND(o, v)   (PyDateTime_TIME_GET_SECOND(o) = (v))
@@ -84,6 +93,7 @@ class datetime.datetime "PyDateTime_DateTime *" "&PyDateTime_DateTimeType"
     (((o)->data[3] = ((v) & 0xff0000) >> 16), \
      ((o)->data[4] = ((v) & 0x00ff00) >> 8), \
      ((o)->data[5] = ((v) & 0x0000ff)))
+#define TIME_SET_FOLD(o, v)   (PyDateTime_TIME_GET_FOLD(o) = (v))
 
 /* Delta accessors for timedelta. */
 #define GET_TD_DAYS(o)          (((PyDateTime_Delta *)(o))->days)
@@ -674,8 +684,8 @@ new_date_ex(int year, int month, int day, PyTypeObject *type)
 
 /* Create a datetime instance with no range checking. */
 static PyObject *
-new_datetime_ex(int year, int month, int day, int hour, int minute,
-             int second, int usecond, PyObject *tzinfo, PyTypeObject *type)
+new_datetime_ex2(int year, int month, int day, int hour, int minute,
+                 int second, int usecond, PyObject *tzinfo, int fold, PyTypeObject *type)
 {
     PyDateTime_DateTime *self;
     char aware = tzinfo != Py_None;
@@ -692,18 +702,27 @@ new_datetime_ex(int year, int month, int day, int hour, int minute,
             Py_INCREF(tzinfo);
             self->tzinfo = tzinfo;
         }
+        DATE_SET_FOLD(self, fold);
     }
     return (PyObject *)self;
 }
 
-#define new_datetime(y, m, d, hh, mm, ss, us, tzinfo)           \
-    new_datetime_ex(y, m, d, hh, mm, ss, us, tzinfo,            \
+static PyObject *
+new_datetime_ex(int year, int month, int day, int hour, int minute,
+                int second, int usecond, PyObject *tzinfo, PyTypeObject *type)
+{
+    return new_datetime_ex2(year, month, day, hour, minute, second, usecond,
+                            tzinfo, 0, type);
+}
+
+#define new_datetime(y, m, d, hh, mm, ss, us, tzinfo, fold) \
+    new_datetime_ex2(y, m, d, hh, mm, ss, us, tzinfo, fold, \
                     &PyDateTime_DateTimeType)
 
 /* Create a time instance with no range checking. */
 static PyObject *
-new_time_ex(int hour, int minute, int second, int usecond,
-            PyObject *tzinfo, PyTypeObject *type)
+new_time_ex2(int hour, int minute, int second, int usecond,
+             PyObject *tzinfo, int fold, PyTypeObject *type)
 {
     PyDateTime_Time *self;
     char aware = tzinfo != Py_None;
@@ -720,12 +739,20 @@ new_time_ex(int hour, int minute, int second, int usecond,
             Py_INCREF(tzinfo);
             self->tzinfo = tzinfo;
         }
+        TIME_SET_FOLD(self, fold);
     }
     return (PyObject *)self;
 }
 
-#define new_time(hh, mm, ss, us, tzinfo)                \
-    new_time_ex(hh, mm, ss, us, tzinfo, &PyDateTime_TimeType)
+static PyObject *
+new_time_ex(int hour, int minute, int second, int usecond,
+            PyObject *tzinfo, PyTypeObject *type)
+{
+    return new_time_ex2(hour, minute, second, usecond, tzinfo, 0, type);
+}
+
+#define new_time(hh, mm, ss, us, tzinfo, fold)                       \
+    new_time_ex2(hh, mm, ss, us, tzinfo, fold, &PyDateTime_TimeType)
 
 /* Create a timedelta instance.  Normalize the members iff normalize is
  * true.  Passing false is a speed optimization, if you know for sure
@@ -887,10 +914,10 @@ 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 || GET_TD_SECONDS(offset) % 60 != 0) {
+        if (GET_TD_MICROSECONDS(offset) != 0) {
             Py_DECREF(offset);
             PyErr_Format(PyExc_ValueError, "offset must be a timedelta"
-                         " representing a whole number of minutes");
+                         " representing a whole number of seconds");
             return NULL;
         }
         if ((GET_TD_DAYS(offset) == -1 && GET_TD_SECONDS(offset) == 0) ||
@@ -1002,6 +1029,30 @@ append_keyword_tzinfo(PyObject *repr, PyObject *tzinfo)
     return repr;
 }
 
+/* repr is like "someclass(arg1, arg2)".  If fold isn't 0,
+ * stuff
+ *     ", fold=" + repr(tzinfo)
+ * before the closing ")".
+ */
+static PyObject *
+append_keyword_fold(PyObject *repr, int fold)
+{
+    PyObject *temp;
+
+    assert(PyUnicode_Check(repr));
+    if (fold == 0)
+        return repr;
+    /* Get rid of the trailing ')'. */
+    assert(PyUnicode_READ_CHAR(repr, PyUnicode_GET_LENGTH(repr)-1) == ')');
+    temp = PyUnicode_Substring(repr, 0, PyUnicode_GET_LENGTH(repr) - 1);
+    Py_DECREF(repr);
+    if (temp == NULL)
+        return NULL;
+    repr = PyUnicode_FromFormat("%U, fold=%d)", temp, fold);
+    Py_DECREF(temp);
+    return repr;
+}
+
 /* ---------------------------------------------------------------------------
  * String format helpers.
  */
@@ -1070,10 +1121,11 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
     Py_DECREF(offset);
     minutes = divmod(seconds, 60, &seconds);
     hours = divmod(minutes, 60, &minutes);
-    assert(seconds == 0);
-    /* XXX ignore sub-minute data, currently not allowed. */
-    PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes);
-
+    if (seconds == 0)
+        PyOS_snprintf(buf, buflen, "%c%02d%s%02d", sign, hours, sep, minutes);
+    else
+        PyOS_snprintf(buf, buflen, "%c%02d%s%02d%s%02d", sign, hours,
+                      sep, minutes, sep, seconds);
     return 0;
 }
 
@@ -3467,12 +3519,19 @@ time_tzinfo(PyDateTime_Time *self, void *unused)
     return result;
 }
 
+static PyObject *
+time_fold(PyDateTime_Time *self, void *unused)
+{
+    return PyLong_FromLong(TIME_GET_FOLD(self));
+}
+
 static PyGetSetDef time_getset[] = {
     {"hour",        (getter)time_hour},
     {"minute",      (getter)time_minute},
     {"second",      (getter)py_time_second},
     {"microsecond", (getter)time_microsecond},
-    {"tzinfo",          (getter)time_tzinfo},
+    {"tzinfo",      (getter)time_tzinfo},
+    {"fold",        (getter)time_fold},
     {NULL}
 };
 
@@ -3481,7 +3540,7 @@ static PyGetSetDef time_getset[] = {
  */
 
 static char *time_kws[] = {"hour", "minute", "second", "microsecond",
-                           "tzinfo", NULL};
+                           "tzinfo", "fold", NULL};
 
 static PyObject *
 time_new(PyTypeObject *type, PyObject *args, PyObject *kw)
@@ -3493,13 +3552,14 @@ time_new(PyTypeObject *type, PyObject *args, PyObject *kw)
     int second = 0;
     int usecond = 0;
     PyObject *tzinfo = Py_None;
+    int fold = 0;
 
     /* Check for invocation from pickle with __getstate__ state */
     if (PyTuple_GET_SIZE(args) >= 1 &&
         PyTuple_GET_SIZE(args) <= 2 &&
         PyBytes_Check(state = PyTuple_GET_ITEM(args, 0)) &&
         PyBytes_GET_SIZE(state) == _PyDateTime_TIME_DATASIZE &&
-        ((unsigned char) (PyBytes_AS_STRING(state)[0])) < 24)
+        (0x7F & ((unsigned char) (PyBytes_AS_STRING(state)[0]))) < 24)
     {
         PyDateTime_Time *me;
         char aware;
@@ -3524,19 +3584,26 @@ time_new(PyTypeObject *type, PyObject *args, PyObject *kw)
                 Py_INCREF(tzinfo);
                 me->tzinfo = tzinfo;
             }
+            if (pdata[0] & (1 << 7)) {
+                me->data[0] -= 128;
+                me->fold = 1;
+            }
+            else {
+                me->fold = 0;
+            }
         }
         return (PyObject *)me;
     }
 
-    if (PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO", time_kws,
+    if (PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO$i", time_kws,
                                     &hour, &minute, &second, &usecond,
-                                    &tzinfo)) {
+                                    &tzinfo, &fold)) {
         if (check_time_args(hour, minute, second, usecond) < 0)
             return NULL;
         if (check_tzinfo_subclass(tzinfo) < 0)
             return NULL;
-        self = new_time_ex(hour, minute, second, usecond, tzinfo,
-                           type);
+        self = new_time_ex2(hour, minute, second, usecond, tzinfo, fold,
+                            type);
     }
     return self;
 }
@@ -3586,6 +3653,7 @@ time_repr(PyDateTime_Time *self)
     int m = TIME_GET_MINUTE(self);
     int s = TIME_GET_SECOND(self);
     int us = TIME_GET_MICROSECOND(self);
+    int fold = TIME_GET_FOLD(self);
     PyObject *result = NULL;
 
     if (us)
@@ -3598,6 +3666,8 @@ time_repr(PyDateTime_Time *self)
         result = PyUnicode_FromFormat("%s(%d, %d)", type_name, h, m);
     if (result != NULL && HASTZINFO(self))
         result = append_keyword_tzinfo(result, self->tzinfo);
+    if (result != NULL && fold)
+        result = append_keyword_fold(result, fold);
     return result;
 }
 
@@ -3784,9 +3854,23 @@ static Py_hash_t
 time_hash(PyDateTime_Time *self)
 {
     if (self->hashcode == -1) {
-        PyObject *offset;
-
-        offset = time_utcoffset((PyObject *)self, NULL);
+        PyObject *offset, *self0;
+        if (DATE_GET_FOLD(self)) {
+            self0 = new_time_ex2(DATE_GET_HOUR(self),
+                                 DATE_GET_MINUTE(self),
+                                 DATE_GET_SECOND(self),
+                                 DATE_GET_MICROSECOND(self),
+                                 HASTZINFO(self) ? self->tzinfo : Py_None,
+                                 0, Py_TYPE(self));
+            if (self0 == NULL)
+                return -1;
+        }
+        else {
+            self0 = (PyObject *)self;
+            Py_INCREF(self0);
+        }
+        offset = time_utcoffset(self0, NULL);
+        Py_DECREF(self0);
 
         if (offset == NULL)
             return -1;
@@ -3832,15 +3916,18 @@ time_replace(PyDateTime_Time *self, PyObject *args, PyObject *kw)
     int ss = TIME_GET_SECOND(self);
     int us = TIME_GET_MICROSECOND(self);
     PyObject *tzinfo = HASTZINFO(self) ? self->tzinfo : Py_None;
+    int fold = TIME_GET_FOLD(self);
 
-    if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO:replace",
+    if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiO$i:replace",
                                       time_kws,
-                                      &hh, &mm, &ss, &us, &tzinfo))
+                                      &hh, &mm, &ss, &us, &tzinfo, &fold))
         return NULL;
     tuple = Py_BuildValue("iiiiO", hh, mm, ss, us, tzinfo);
     if (tuple == NULL)
         return NULL;
     clone = time_new(Py_TYPE(self), tuple, NULL);
+    if (clone != NULL)
+        TIME_SET_FOLD(clone, fold);
     Py_DECREF(tuple);
     return clone;
 }
@@ -3853,7 +3940,7 @@ time_replace(PyDateTime_Time *self, PyObject *args, PyObject *kw)
  * __getstate__ isn't exposed.
  */
 static PyObject *
-time_getstate(PyDateTime_Time *self)
+time_getstate(PyDateTime_Time *self, int proto)
 {
     PyObject *basestate;
     PyObject *result = NULL;
@@ -3861,6 +3948,9 @@ time_getstate(PyDateTime_Time *self)
     basestate =  PyBytes_FromStringAndSize((char *)self->data,
                                             _PyDateTime_TIME_DATASIZE);
     if (basestate != NULL) {
+        if (proto > 3 && TIME_GET_FOLD(self))
+            /* Set the first bit of the first byte */
+            PyBytes_AS_STRING(basestate)[0] |= (1 << 7);
         if (! HASTZINFO(self) || self->tzinfo == Py_None)
             result = PyTuple_Pack(1, basestate);
         else
@@ -3871,9 +3961,13 @@ time_getstate(PyDateTime_Time *self)
 }
 
 static PyObject *
-time_reduce(PyDateTime_Time *self, PyObject *arg)
+time_reduce(PyDateTime_Time *self, PyObject *args)
 {
-    return Py_BuildValue("(ON)", Py_TYPE(self), time_getstate(self));
+    int proto = 0;
+    if (!PyArg_ParseTuple(args, "|i:__reduce_ex__", &proto))
+        return NULL;
+
+    return Py_BuildValue("(ON)", Py_TYPE(self), time_getstate(self, proto));
 }
 
 static PyMethodDef time_methods[] = {
@@ -3901,8 +3995,8 @@ static PyMethodDef time_methods[] = {
     {"replace",     (PyCFunction)time_replace,          METH_VARARGS | METH_KEYWORDS,
      PyDoc_STR("Return time with new specified fields.")},
 
-    {"__reduce__", (PyCFunction)time_reduce,        METH_NOARGS,
-     PyDoc_STR("__reduce__() -> (cls, state)")},
+    {"__reduce_ex__", (PyCFunction)time_reduce,        METH_VARARGS,
+     PyDoc_STR("__reduce_ex__(proto) -> (cls, state)")},
 
     {NULL,      NULL}
 };
@@ -3995,12 +4089,19 @@ datetime_tzinfo(PyDateTime_DateTime *self, void *unused)
     return result;
 }
 
+static PyObject *
+datetime_fold(PyDateTime_DateTime *self, void *unused)
+{
+    return PyLong_FromLong(DATE_GET_FOLD(self));
+}
+
 static PyGetSetDef datetime_getset[] = {
     {"hour",        (getter)datetime_hour},
     {"minute",      (getter)datetime_minute},
     {"second",      (getter)datetime_second},
     {"microsecond", (getter)datetime_microsecond},
-    {"tzinfo",          (getter)datetime_tzinfo},
+    {"tzinfo",      (getter)datetime_tzinfo},
+    {"fold",        (getter)datetime_fold},
     {NULL}
 };
 
@@ -4010,7 +4111,7 @@ static PyGetSetDef datetime_getset[] = {
 
 static char *datetime_kws[] = {
     "year", "month", "day", "hour", "minute", "second",
-    "microsecond", "tzinfo", NULL
+    "microsecond", "tzinfo", "fold", NULL
 };
 
 static PyObject *
@@ -4025,6 +4126,7 @@ datetime_new(PyTypeObject *type, PyObject *args, PyObject *kw)
     int minute = 0;
     int second = 0;
     int usecond = 0;
+    int fold = 0;
     PyObject *tzinfo = Py_None;
 
     /* Check for invocation from pickle with __getstate__ state */
@@ -4032,7 +4134,7 @@ datetime_new(PyTypeObject *type, PyObject *args, PyObject *kw)
         PyTuple_GET_SIZE(args) <= 2 &&
         PyBytes_Check(state = PyTuple_GET_ITEM(args, 0)) &&
         PyBytes_GET_SIZE(state) == _PyDateTime_DATETIME_DATASIZE &&
-        MONTH_IS_SANE(PyBytes_AS_STRING(state)[2]))
+        MONTH_IS_SANE(PyBytes_AS_STRING(state)[2] & 0x7F))
     {
         PyDateTime_DateTime *me;
         char aware;
@@ -4057,22 +4159,29 @@ datetime_new(PyTypeObject *type, PyObject *args, PyObject *kw)
                 Py_INCREF(tzinfo);
                 me->tzinfo = tzinfo;
             }
+            if (pdata[2] & (1 << 7)) {
+                me->data[2] -= 128;
+                me->fold = 1;
+            }
+            else {
+                me->fold = 0;
+            }
         }
         return (PyObject *)me;
     }
 
-    if (PyArg_ParseTupleAndKeywords(args, kw, "iii|iiiiO", datetime_kws,
+    if (PyArg_ParseTupleAndKeywords(args, kw, "iii|iiiiO$i", datetime_kws,
                                     &year, &month, &day, &hour, &minute,
-                                    &second, &usecond, &tzinfo)) {
+                                    &second, &usecond, &tzinfo, &fold)) {
         if (check_date_args(year, month, day) < 0)
             return NULL;
         if (check_time_args(hour, minute, second, usecond) < 0)
             return NULL;
         if (check_tzinfo_subclass(tzinfo) < 0)
             return NULL;
-        self = new_datetime_ex(year, month, day,
+        self = new_datetime_ex2(year, month, day,
                                 hour, minute, second, usecond,
-                                tzinfo, type);
+                                tzinfo, fold, type);
     }
     return self;
 }
@@ -4080,6 +4189,38 @@ datetime_new(PyTypeObject *type, PyObject *args, PyObject *kw)
 /* TM_FUNC is the shared type of localtime() and gmtime(). */
 typedef struct tm *(*TM_FUNC)(const time_t *timer);
 
+/* As of version 2015f max fold in IANA database is
+ * 23 hours at 1969-09-30 13:00:00 in Kwajalein. */
+static PY_LONG_LONG max_fold_seconds = 24 * 3600;
+/* NB: date(1970,1,1).toordinal() == 719163 */
+static PY_LONG_LONG epoch = Py_LL(719163) * 24 * 60 * 60;
+
+static PY_LONG_LONG
+utc_to_seconds(int year, int month, int day,
+               int hour, int minute, int second)
+{
+    PY_LONG_LONG ordinal = ymd_to_ord(year, month, day);
+    return ((ordinal * 24 + hour) * 60 + minute) * 60 + second;
+}
+
+static PY_LONG_LONG
+local(PY_LONG_LONG u)
+{
+    struct tm local_time;
+    time_t t = u - epoch;
+    /* XXX: add bounds checking */
+    if (localtime_r(&t, &local_time) == NULL) {
+        PyErr_SetFromErrno(PyExc_OSError);
+        return -1;
+    }
+    return utc_to_seconds(local_time.tm_year + 1900,
+                          local_time.tm_mon + 1,
+                          local_time.tm_mday,
+                          local_time.tm_hour,
+                          local_time.tm_min,
+                          local_time.tm_sec);
+}
+
 /* Internal helper.
  * Build datetime from a time_t and a distinct count of microseconds.
  * Pass localtime or gmtime for f, to control the interpretation of timet.
@@ -4089,6 +4230,7 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
                            PyObject *tzinfo)
 {
     struct tm *tm;
+    int year, month, day, hour, minute, second, fold = 0;
 
     tm = f(&timet);
     if (tm == NULL) {
@@ -4099,23 +4241,40 @@ datetime_from_timet_and_us(PyObject *cls, TM_FUNC f, time_t timet, int us,
         return PyErr_SetFromErrno(PyExc_OSError);
     }
 
+    year = tm->tm_year + 1900;
+    month = tm->tm_mon + 1;
+    day = tm->tm_mday;
+    hour = tm->tm_hour;
+    minute = tm->tm_min;
     /* The platform localtime/gmtime may insert leap seconds,
      * indicated by tm->tm_sec > 59.  We don't care about them,
      * except to the extent that passing them on to the datetime
      * constructor would raise ValueError for a reason that
      * made no sense to the user.
      */
-    if (tm->tm_sec > 59)
-        tm->tm_sec = 59;
-    return PyObject_CallFunction(cls, "iiiiiiiO",
-                                 tm->tm_year + 1900,
-                                 tm->tm_mon + 1,
-                                 tm->tm_mday,
-                                 tm->tm_hour,
-                                 tm->tm_min,
-                                 tm->tm_sec,
-                                 us,
-                                 tzinfo);
+    second = Py_MIN(59, tm->tm_sec);
+
+    if (tzinfo == Py_None && f == localtime) {
+        PY_LONG_LONG probe_seconds, result_seconds, transition;
+
+        result_seconds = utc_to_seconds(year, month, day,
+                                        hour, minute, second);
+        /* Probe max_fold_seconds to detect a fold. */
+        probe_seconds = local(epoch + timet - max_fold_seconds);
+        if (probe_seconds == -1)
+            return NULL;
+        transition = result_seconds - probe_seconds - max_fold_seconds;
+        if (transition < 0) {
+            probe_seconds = local(epoch + timet + transition);
+            if (probe_seconds == -1)
+                return NULL;
+            if (probe_seconds == result_seconds)
+                fold = 1;
+        }
+    }
+    return new_datetime_ex2(year, month, day, hour,
+                            minute, second, us, tzinfo, fold,
+                            (PyTypeObject *)cls);
 }
 
 /* Internal helper.
@@ -4285,6 +4444,7 @@ datetime_combine(PyObject *cls, PyObject *args, PyObject *kw)
                                         TIME_GET_SECOND(time),
                                         TIME_GET_MICROSECOND(time),
                                         tzinfo);
+        DATE_SET_FOLD(result, TIME_GET_FOLD(time));
     }
     return result;
 }
@@ -4352,7 +4512,7 @@ add_datetime_timedelta(PyDateTime_DateTime *date, PyDateTime_Delta *delta,
     else
         return new_datetime(year, month, day,
                             hour, minute, second, microsecond,
-                            HASTZINFO(date) ? date->tzinfo : Py_None);
+                            HASTZINFO(date) ? date->tzinfo : Py_None, 0);
 }
 
 static PyObject *
@@ -4495,6 +4655,8 @@ datetime_repr(PyDateTime_DateTime *self)
                       GET_YEAR(self), GET_MONTH(self), GET_DAY(self),
                       DATE_GET_HOUR(self), DATE_GET_MINUTE(self));
     }
+    if (baserepr != NULL && DATE_GET_FOLD(self) != 0)
+        baserepr = append_keyword_fold(baserepr, DATE_GET_FOLD(self));
     if (baserepr == NULL || ! HASTZINFO(self))
         return baserepr;
     return append_keyword_tzinfo(baserepr, self->tzinfo);
@@ -4584,6 +4746,70 @@ datetime_ctime(PyDateTime_DateTime *self)
 
 /* Miscellaneous methods. */
 
+static PyObject *
+flip_fold(PyObject *dt)
+{
+    return new_datetime_ex2(GET_YEAR(dt),
+                            GET_MONTH(dt),
+                            GET_DAY(dt),
+                            DATE_GET_HOUR(dt),
+                            DATE_GET_MINUTE(dt),
+                            DATE_GET_SECOND(dt),
+                            DATE_GET_MICROSECOND(dt),
+                            HASTZINFO(dt) ?
+                             ((PyDateTime_DateTime *)dt)->tzinfo : Py_None,
+                            !DATE_GET_FOLD(dt),
+                            Py_TYPE(dt));
+}
+
+static PyObject *
+get_flip_fold_offset(PyObject *dt)
+{
+    PyObject *result, *flip_dt;
+
+    flip_dt = flip_fold(dt);
+    if (flip_dt == NULL)
+        return NULL;
+    result = datetime_utcoffset(flip_dt, NULL);
+    Py_DECREF(flip_dt);
+    return result;
+}
+
+/* PEP 495 exception: Whenever one or both of the operands in
+ * inter-zone comparison is such that its utcoffset() depends
+ * on the value of its fold fold attribute, the result is False.
+ *
+ * Return 1 if exception applies, 0 if not,  and -1 on error.
+ */
+static int
+pep495_eq_exception(PyObject *self, PyObject *other,
+                    PyObject *offset_self, PyObject *offset_other)
+{
+    int result = 0;
+    PyObject *flip_offset;
+
+    flip_offset = get_flip_fold_offset(self);
+    if (flip_offset == NULL)
+        return -1;
+    if (flip_offset != offset_self &&
+        delta_cmp(flip_offset, offset_self))
+    {
+        result = 1;
+        goto done;
+    }
+    Py_DECREF(flip_offset);
+
+    flip_offset = get_flip_fold_offset(other);
+    if (flip_offset == NULL)
+        return -1;
+    if (flip_offset != offset_other &&
+        delta_cmp(flip_offset, offset_other))
+        result = 1;
+ done:
+    Py_DECREF(flip_offset);
+    return result;
+}
+
 static PyObject *
 datetime_richcompare(PyObject *self, PyObject *other, int op)
 {
@@ -4631,6 +4857,13 @@ datetime_richcompare(PyObject *self, PyObject *other, int op)
         diff = memcmp(((PyDateTime_DateTime *)self)->data,
                       ((PyDateTime_DateTime *)other)->data,
                       _PyDateTime_DATETIME_DATASIZE);
+        if ((op == Py_EQ || op == Py_NE) && diff == 0) {
+            int ex = pep495_eq_exception(self, other, offset1, offset2);
+            if (ex == -1)
+                goto done;
+            if (ex)
+                diff = 1;
+        }
         result = diff_to_bool(diff, op);
     }
     else if (offset1 != Py_None && offset2 != Py_None) {
@@ -4646,6 +4879,13 @@ datetime_richcompare(PyObject *self, PyObject *other, int op)
             diff = GET_TD_SECONDS(delta) |
                    GET_TD_MICROSECONDS(delta);
         Py_DECREF(delta);
+        if ((op == Py_EQ || op == Py_NE) && diff == 0) {
+            int ex = pep495_eq_exception(self, other, offset1, offset2);
+            if (ex == -1)
+                goto done;
+            if (ex)
+                diff = 1;
+        }
         result = diff_to_bool(diff, op);
     }
     else if (op == Py_EQ) {
@@ -4671,9 +4911,26 @@ static Py_hash_t
 datetime_hash(PyDateTime_DateTime *self)
 {
     if (self->hashcode == -1) {
-        PyObject *offset;
-
-        offset = datetime_utcoffset((PyObject *)self, NULL);
+        PyObject *offset, *self0;
+        if (DATE_GET_FOLD(self)) {
+            self0 = new_datetime_ex2(GET_YEAR(self),
+                                     GET_MONTH(self),
+                                     GET_DAY(self),
+                                     DATE_GET_HOUR(self),
+                                     DATE_GET_MINUTE(self),
+                                     DATE_GET_SECOND(self),
+                                     DATE_GET_MICROSECOND(self),
+                                     HASTZINFO(self) ? self->tzinfo : Py_None,
+                                     0, Py_TYPE(self));
+            if (self0 == NULL)
+                return -1;
+        }
+        else {
+            self0 = (PyObject *)self;
+            Py_INCREF(self0);
+        }
+        offset = datetime_utcoffset(self0, NULL);
+        Py_DECREF(self0);
 
         if (offset == NULL)
             return -1;
@@ -4727,76 +4984,71 @@ datetime_replace(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
     int ss = DATE_GET_SECOND(self);
     int us = DATE_GET_MICROSECOND(self);
     PyObject *tzinfo = HASTZINFO(self) ? self->tzinfo : Py_None;
+    int fold = DATE_GET_FOLD(self);
 
-    if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiiiiO:replace",
+    if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiiiiO$i:replace",
                                       datetime_kws,
                                       &y, &m, &d, &hh, &mm, &ss, &us,
-                                      &tzinfo))
+                                      &tzinfo, &fold))
         return NULL;
     tuple = Py_BuildValue("iiiiiiiO", y, m, d, hh, mm, ss, us, tzinfo);
     if (tuple == NULL)
         return NULL;
     clone = datetime_new(Py_TYPE(self), tuple, NULL);
+    if (clone != NULL)
+        DATE_SET_FOLD(clone, fold);
     Py_DECREF(tuple);
     return clone;
 }
 
 static PyObject *
-local_timezone(PyDateTime_DateTime *utc_time)
+local_timezone_from_timestamp(time_t timestamp)
 {
     PyObject *result = NULL;
-    struct tm *timep;
-    time_t timestamp;
     PyObject *delta;
-    PyObject *one_second;
-    PyObject *seconds;
+    struct tm *local_time_tm;
     PyObject *nameo = NULL;
     const char *zone = NULL;
 
-    delta = new_delta(ymd_to_ord(GET_YEAR(utc_time), GET_MONTH(utc_time),
-                                 GET_DAY(utc_time)) - 719163,
-                      60 * (60 * DATE_GET_HOUR(utc_time) +
-                            DATE_GET_MINUTE(utc_time)) +
-                      DATE_GET_SECOND(utc_time),
-                      0, 0);
-    if (delta == NULL)
-        return NULL;
-    one_second = new_delta(0, 1, 0, 0);
-    if (one_second == NULL)
-        goto error;
-    seconds = divide_timedelta_timedelta((PyDateTime_Delta *)delta,
-                                         (PyDateTime_Delta *)one_second);
-    Py_DECREF(one_second);
-    if (seconds == NULL)
-        goto error;
-    Py_DECREF(delta);
-    timestamp = _PyLong_AsTime_t(seconds);
-    Py_DECREF(seconds);
-    if (timestamp == -1 && PyErr_Occurred())
-        return NULL;
-    timep = localtime(&timestamp);
+    local_time_tm = localtime(&timestamp);
 #ifdef HAVE_STRUCT_TM_TM_ZONE
-    zone = timep->tm_zone;
-    delta = new_delta(0, timep->tm_gmtoff, 0, 1);
+    zone = local_time_tm->tm_zone;
+    delta = new_delta(0, local_time_tm->tm_gmtoff, 0, 1);
 #else /* HAVE_STRUCT_TM_TM_ZONE */
     {
-        PyObject *local_time;
-        local_time = new_datetime(timep->tm_year + 1900, timep->tm_mon + 1,
-                                  timep->tm_mday, timep->tm_hour, timep->tm_min,
-                                  timep->tm_sec, DATE_GET_MICROSECOND(utc_time),
-                                  utc_time->tzinfo);
-        if (local_time == NULL)
-            goto error;
-        delta = datetime_subtract(local_time, (PyObject*)utc_time);
-        /* XXX: before relying on tzname, we should compare delta
-           to the offset implied by timezone/altzone */
-        if (daylight && timep->tm_isdst >= 0)
-            zone = tzname[timep->tm_isdst % 2];
-        else
-            zone = tzname[0];
+        PyObject *local_time, *utc_time;
+        struct tm *utc_time_tm;
+        char buf[100];
+        strftime(buf, sizeof(buf), "%Z", local_time_tm);
+        zone = buf;
+        local_time = new_datetime(local_time_tm->tm_year + 1900,
+                                  local_time_tm->tm_mon + 1,
+                                  local_time_tm->tm_mday,
+                                  local_time_tm->tm_hour,
+                                  local_time_tm->tm_min,
+                                  local_time_tm->tm_sec, 0, Py_None, 0);
+        if (local_time == NULL) {
+            return NULL;
+        }
+        utc_time_tm = gmtime(&timestamp);
+        utc_time = new_datetime(utc_time_tm->tm_year + 1900,
+                                utc_time_tm->tm_mon + 1,
+                                utc_time_tm->tm_mday,
+                                utc_time_tm->tm_hour,
+                                utc_time_tm->tm_min,
+                                utc_time_tm->tm_sec, 0, Py_None, 0);
+        if (utc_time == NULL) {
+            Py_DECREF(local_time);
+            return NULL;
+        }
+        delta = datetime_subtract(local_time, utc_time);
         Py_DECREF(local_time);
+        Py_DECREF(utc_time);
     }
 #endif /* HAVE_STRUCT_TM_TM_ZONE */
+    if (delta == NULL) {
+            return NULL;
+    }
     if (zone != NULL) {
         nameo = PyUnicode_DecodeLocale(zone, "surrogateescape");
         if (nameo == NULL)
@@ -4809,12 +5061,65 @@ local_timezone(PyDateTime_DateTime *utc_time)
     return result;
 }
 
+static PyObject *
+local_timezone(PyDateTime_DateTime *utc_time)
+{
+    time_t timestamp;
+    PyObject *delta;
+    PyObject *one_second;
+    PyObject *seconds;
+
+    delta = datetime_subtract((PyObject *)utc_time, PyDateTime_Epoch);
+    if (delta == NULL)
+        return NULL;
+    one_second = new_delta(0, 1, 0, 0);
+    if (one_second == NULL) {
+        Py_DECREF(delta);
+        return NULL;
+    }
+    seconds = divide_timedelta_timedelta((PyDateTime_Delta *)delta,
+                                         (PyDateTime_Delta *)one_second);
+    Py_DECREF(one_second);
+    Py_DECREF(delta);
+    if (seconds == NULL)
+        return NULL;
+    timestamp = _PyLong_AsTime_t(seconds);
+    Py_DECREF(seconds);
+    if (timestamp == -1 && PyErr_Occurred())
+        return NULL;
+    return local_timezone_from_timestamp(timestamp);
+}
+
+static PY_LONG_LONG
+local_to_seconds(int year, int month, int day,
+                 int hour, int minute, int second, int fold);
+
+static PyObject *
+local_timezone_from_local(PyDateTime_DateTime *local_dt)
+{
+    PY_LONG_LONG seconds;
+    time_t timestamp;
+    seconds = local_to_seconds(GET_YEAR(local_dt),
+                               GET_MONTH(local_dt),
+                               GET_DAY(local_dt),
+                               DATE_GET_HOUR(local_dt),
+                               DATE_GET_MINUTE(local_dt),
+                               DATE_GET_SECOND(local_dt),
+                               DATE_GET_FOLD(local_dt));
+    if (seconds == -1)
+        return NULL;
+    /* XXX: add bounds check */
+    timestamp = seconds - epoch;
+    return local_timezone_from_timestamp(timestamp);
+}
+
 static PyDateTime_DateTime *
 datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
 {
     PyDateTime_DateTime *result;
     PyObject *offset;
     PyObject *temp;
+    PyObject *self_tzinfo;
     PyObject *tzinfo = Py_None;
     static char *keywords[] = {"tz", NULL};
 
@@ -4825,27 +5130,27 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
     if (check_tzinfo_subclass(tzinfo) == -1)
         return NULL;
 
-    if (!HASTZINFO(self) || self->tzinfo == Py_None)
-        goto NeedAware;
+    if (!HASTZINFO(self) || self->tzinfo == Py_None) {
+        self_tzinfo = local_timezone_from_local(self);
+        if (self_tzinfo == NULL)
+            return NULL;
+    } else {
+        self_tzinfo = self->tzinfo;
+        Py_INCREF(self_tzinfo);
+    }
 
     /* Conversion to self's own time zone is a NOP. */
-    if (self->tzinfo == tzinfo) {
+    if (self_tzinfo == tzinfo) {
+        Py_DECREF(self_tzinfo);
         Py_INCREF(self);
         return self;
     }
 
     /* Convert self to UTC. */
-    offset = datetime_utcoffset((PyObject *)self, NULL);
+    offset = call_utcoffset(self_tzinfo, (PyObject *)self);
+    Py_DECREF(self_tzinfo);
     if (offset == NULL)
         return NULL;
-    if (offset == Py_None) {
-        Py_DECREF(offset);
-      NeedAware:
-        PyErr_SetString(PyExc_ValueError, "astimezone() cannot be applied to "
-                        "a naive datetime");
-        return NULL;
-    }
-
     /* result = self - offset */
     result = (PyDateTime_DateTime *)add_datetime_timedelta(self,
                                        (PyDateTime_Delta *)offset, -1);
@@ -4853,6 +5158,32 @@ datetime_astimezone(PyDateTime_DateTime *self, PyObject *args, PyObject *kw)
     if (result == NULL)
         return NULL;
 
+    /* Make sure result is aware and UTC. */
+    if (!HASTZINFO(result)) {
+        temp = (PyObject *)result;
+        result = (PyDateTime_DateTime *)
+                   new_datetime_ex2(GET_YEAR(result),
+                                    GET_MONTH(result),
+                                    GET_DAY(result),
+                                    DATE_GET_HOUR(result),
+                                    DATE_GET_MINUTE(result),
+                                    DATE_GET_SECOND(result),
+                                    DATE_GET_MICROSECOND(result),
+                                    PyDateTime_TimeZone_UTC,
+                                    DATE_GET_FOLD(result),
+                                    Py_TYPE(result));
+        Py_DECREF(temp);
+        if (result == NULL)
+            return NULL;
+    }
+    else {
+        /* Result is already aware - just replace tzinfo. */
+        temp = result->tzinfo;
+        result->tzinfo = PyDateTime_TimeZone_UTC;
+        Py_INCREF(result->tzinfo);
+        Py_DECREF(temp);
+    }
+
     /* Attach new tzinfo and let fromutc() do the rest. */
     temp = result->tzinfo;
     if (tzinfo == Py_None) {
@@ -4900,6 +5231,56 @@ datetime_timetuple(PyDateTime_DateTime *self)
                              dstflag);
 }
 
+static PY_LONG_LONG
+local_to_seconds(int year, int month, int day,
+                 int hour, int minute, int second, int fold)
+{
+    PY_LONG_LONG t, a, b, u1, u2, t1, t2, lt;
+    t = utc_to_seconds(year, month, day, hour, minute, second);
+    /* Our goal is to solve t = local(u) for u. */
+    lt = local(t);
+    if (lt == -1)
+        return -1;
+    a = lt - t;
+    u1 = t - a;
+    t1 = local(u1);
+    if (t1 == -1)
+        return -1;
+    if (t1 == t) {
+        /* We found one solution, but it may not be the one we need.
+         * Look for an earlier solution (if `fold` is 0), or a
+         * later one (if `fold` is 1). */
+        if (fold)
+            u2 = u1 + max_fold_seconds;
+        else
+            u2 = u1 - max_fold_seconds;
+        lt = local(u2);
+        if (lt == -1)
+            return -1;
+        b = lt - u2;
+        if (a == b)
+            return u1;
+    }
+    else {
+        b = t1 - u1;
+        assert(a != b);
+    }
+    u2 = t - b;
+    t2 = local(u2);
+    if (t2 == -1)
+        return -1;
+    if (t2 == t)
+        return u2;
+    if (t1 == t)
+        return u1;
+    /* We have found both offsets a and b, but neither t - a nor t - b is
+     * a solution.  This means t is in the gap. */
+    return fold?Py_MIN(u1, u2):Py_MAX(u1, u2);
+}
+
+/* date(1970,1,1).toordinal() == 719163 */
+#define EPOCH_SECONDS (719163LL * 24 * 60 * 60)
+
 static PyObject *
 datetime_timestamp(PyDateTime_DateTime *self)
 {
@@ -4914,33 +5295,18 @@ datetime_timestamp(PyDateTime_DateTime *self)
         Py_DECREF(delta);
     }
     else {
-        struct tm time;
-        time_t timestamp;
-        memset((void *) &time, '\0', sizeof(struct tm));
-        time.tm_year = GET_YEAR(self) - 1900;
-        time.tm_mon = GET_MONTH(self) - 1;
-        time.tm_mday = GET_DAY(self);
-        time.tm_hour = DATE_GET_HOUR(self);
-        time.tm_min = DATE_GET_MINUTE(self);
-        time.tm_sec = DATE_GET_SECOND(self);
-        time.tm_wday = -1;
-        time.tm_isdst = -1;
-        timestamp = mktime(&time);
-        if (timestamp == (time_t)(-1)
-#ifndef _AIX
-            /* Return value of -1 does not necessarily mean an error,
-             * but tm_wday cannot remain set to -1 if mktime succeeded. */
-            && time.tm_wday == -1
-#else
-            /* on AIX, tm_wday is always sets, even on error */
-#endif
-          )
-        {
-            PyErr_SetString(PyExc_OverflowError,
-                            "timestamp out of range");
+        PY_LONG_LONG seconds;
+        seconds = local_to_seconds(GET_YEAR(self),
+                                   GET_MONTH(self),
+                                   GET_DAY(self),
+                                   DATE_GET_HOUR(self),
+                                   DATE_GET_MINUTE(self),
+                                   DATE_GET_SECOND(self),
+                                   DATE_GET_FOLD(self));
+        if (seconds == -1)
             return NULL;
-        }
-        result = PyFloat_FromDouble(timestamp + DATE_GET_MICROSECOND(self) / 1e6);
+        result = PyFloat_FromDouble(seconds - EPOCH_SECONDS +
+                                    DATE_GET_MICROSECOND(self) / 1e6);
     }
     return result;
 }
@@ -4960,7 +5326,8 @@ datetime_gettime(PyDateTime_DateTime *self)
                     DATE_GET_MINUTE(self),
                     DATE_GET_SECOND(self),
                     DATE_GET_MICROSECOND(self),
-                    Py_None);
+                    Py_None,
+                    DATE_GET_FOLD(self));
 }
 
 static PyObject *
@@ -4970,7 +5337,8 @@ datetime_gettimetz(PyDateTime_DateTime *self)
                     DATE_GET_MINUTE(self),
                     DATE_GET_SECOND(self),
                     DATE_GET_MICROSECOND(self),
-                    GET_DT_TZINFO(self));
+                    GET_DT_TZINFO(self),
+                    DATE_GET_FOLD(self));
 }
 
 static PyObject *
@@ -5022,7 +5390,7 @@ datetime_utctimetuple(PyDateTime_DateTime *self)
  * __getstate__ isn't exposed.
  */
 static PyObject *
-datetime_getstate(PyDateTime_DateTime *self)
+datetime_getstate(PyDateTime_DateTime *self, int proto)
 {
     PyObject *basestate;
     PyObject *result = NULL;
@@ -5030,6 +5398,9 @@ datetime_getstate(PyDateTime_DateTime *self)
     basestate = PyBytes_FromStringAndSize((char *)self->data,
                                            _PyDateTime_DATETIME_DATASIZE);
     if (basestate != NULL) {
+        if (proto > 3 && DATE_GET_FOLD(self))
+            /* Set the first bit of the third byte */
+            PyBytes_AS_STRING(basestate)[2] |= (1 << 7);
         if (! HASTZINFO(self) || self->tzinfo == Py_None)
             result = PyTuple_Pack(1, basestate);
         else
@@ -5040,9 +5411,13 @@ datetime_getstate(PyDateTime_DateTime *self)
 }
 
 static PyObject *
-datetime_reduce(PyDateTime_DateTime *self, PyObject *arg)
+datetime_reduce(PyDateTime_DateTime *self, PyObject *args)
 {
-    return Py_BuildValue("(ON)", Py_TYPE(self), datetime_getstate(self));
+    int proto = 0;
+    if (!PyArg_ParseTuple(args, "|i:__reduce_ex__", &proto))
+        return NULL;
+
+    return Py_BuildValue("(ON)", Py_TYPE(self), datetime_getstate(self, proto));
 }
 
 static PyMethodDef datetime_methods[] = {
@@ -5119,8 +5494,8 @@ static PyMethodDef datetime_methods[] = {
     {"astimezone",  (PyCFunction)datetime_astimezone, METH_VARARGS | METH_KEYWORDS,
      PyDoc_STR("tz -> convert to local time in new timezone tz\n")},
 
-    {"__reduce__", (PyCFunction)datetime_reduce,     METH_NOARGS,
-     PyDoc_STR("__reduce__() -> (cls, state)")},
+    {"__reduce_ex__", (PyCFunction)datetime_reduce,     METH_VARARGS,
+     PyDoc_STR("__reduce_ex__(proto) -> (cls, state)")},
 
     {NULL,      NULL}
 };
@@ -5208,7 +5583,9 @@ static PyDateTime_CAPI CAPI = {
     new_time_ex,
     new_delta_ex,
     datetime_fromtimestamp,
-    date_fromtimestamp
+    date_fromtimestamp,
+    new_datetime_ex2,
+    new_time_ex2
 };
 
 
@@ -5289,12 +5666,12 @@ PyInit__datetime(void)
     /* time values */
     d = PyDateTime_TimeType.tp_dict;
 
-    x = new_time(0, 0, 0, 0, Py_None);
+    x = new_time(0, 0, 0, 0, Py_None, 0);
     if (x == NULL || PyDict_SetItemString(d, "min", x) < 0)
         return NULL;
     Py_DECREF(x);
 
-    x = new_time(23, 59, 59, 999999, Py_None);
+    x = new_time(23, 59, 59, 999999, Py_None, 0);
     if (x == NULL || PyDict_SetItemString(d, "max", x) < 0)
         return NULL;
     Py_DECREF(x);
@@ -5307,12 +5684,12 @@ PyInit__datetime(void)
     /* datetime values */
     d = PyDateTime_DateTimeType.tp_dict;
 
-    x = new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None);
+    x = new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0);
     if (x == NULL || PyDict_SetItemString(d, "min", x) < 0)
         return NULL;
     Py_DECREF(x);
 
-    x = new_datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, Py_None);
+    x = new_datetime(MAXYEAR, 12, 31, 23, 59, 59, 999999, Py_None, 0);
     if (x == NULL || PyDict_SetItemString(d, "max", x) < 0)
         return NULL;
     Py_DECREF(x);
@@ -5354,7 +5731,7 @@ PyInit__datetime(void)
 
     /* Epoch */
     PyDateTime_Epoch = new_datetime(1970, 1, 1, 0, 0, 0, 0,
-                                    PyDateTime_TimeZone_UTC);
+                                    PyDateTime_TimeZone_UTC, 0);
     if (PyDateTime_Epoch == NULL)
       return NULL;
 
diff --git a/Tools/tz/zdump.py b/Tools/tz/zdump.py
new file mode 100644 (file)
index 0000000..f94b483
--- /dev/null
@@ -0,0 +1,81 @@
+import sys
+import os
+import struct
+from array import array
+from collections import namedtuple
+from datetime import datetime, timedelta
+
+ttinfo = namedtuple('ttinfo', ['tt_gmtoff', 'tt_isdst', 'tt_abbrind'])
+
+class TZInfo:
+    def __init__(self, transitions, type_indices, ttis, abbrs):
+        self.transitions = transitions
+        self.type_indices = type_indices
+        self.ttis = ttis
+        self.abbrs = abbrs
+
+    @classmethod
+    def fromfile(cls, fileobj):
+        if fileobj.read(4).decode() != "TZif":
+            raise ValueError("not a zoneinfo file")
+        fileobj.seek(20)
+        header = fileobj.read(24)
+        tzh = (tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt,
+               tzh_timecnt, tzh_typecnt, tzh_charcnt) = struct.unpack(">6l", header)
+        transitions = array('i')
+        transitions.fromfile(fileobj, tzh_timecnt)
+        if sys.byteorder != 'big':
+            transitions.byteswap()
+
+        type_indices = array('B')
+        type_indices.fromfile(fileobj, tzh_timecnt)
+
+        ttis = []
+        for i in range(tzh_typecnt):
+            ttis.append(ttinfo._make(struct.unpack(">lbb", fileobj.read(6))))
+
+        abbrs = fileobj.read(tzh_charcnt)
+
+        self = cls(transitions, type_indices, ttis, abbrs)
+        self.tzh = tzh
+
+        return self
+
+    def dump(self, stream, start=None, end=None):
+        for j, (trans, i) in enumerate(zip(self.transitions, self.type_indices)):
+            utc = datetime.utcfromtimestamp(trans)
+            tti = self.ttis[i]
+            lmt = datetime.utcfromtimestamp(trans + tti.tt_gmtoff)
+            abbrind = tti.tt_abbrind
+            abbr = self.abbrs[abbrind:self.abbrs.find(0, abbrind)].decode()
+            if j > 0:
+                prev_tti = self.ttis[self.type_indices[j - 1]]
+                shift = " %+g" % ((tti.tt_gmtoff - prev_tti.tt_gmtoff) / 3600)
+            else:
+                shift = ''
+            print("%s UTC = %s %-5s isdst=%d" % (utc, lmt, abbr, tti[1]) + shift, file=stream)
+
+    @classmethod
+    def zonelist(cls, zonedir='/usr/share/zoneinfo'):
+        zones = []
+        for root, _, files in os.walk(zonedir):
+            for f in files:
+                p = os.path.join(root, f)
+                with open(p, 'rb') as o:
+                    magic =  o.read(4)
+                if magic == b'TZif':
+                    zones.append(p[len(zonedir) + 1:])
+        return zones
+
+if __name__ == '__main__':
+    if len(sys.argv) < 2:
+        zones = TZInfo.zonelist()
+        for z in zones:
+            print(z)
+        sys.exit()
+    filepath = sys.argv[1]
+    if not filepath.startswith('/'):
+        filepath = os.path.join('/usr/share/zoneinfo', filepath)
+    with open(filepath, 'rb') as fileobj:
+        tzi = TZInfo.fromfile(fileobj)
+    tzi.dump(sys.stdout)