Closes issue #24773: Implement PEP 495 (Local Time Disambiguation).

This commit is contained in:
Alexander Belopolsky 2016-07-22 18:47:04 -04:00
parent 638e622055
commit 5d0c598382
7 changed files with 1601 additions and 227 deletions

View 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)

View 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 @@ def isocalendar(self):
# 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 @@ def __setstate(self, string):
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 @@ def __reduce__(self):
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 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None):
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 @@ def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None):
self._microsecond = microsecond
self._tzinfo = tzinfo
self._hashcode = -1
self._fold = fold
return self
# Read-only field accessors
@ -1095,6 +1098,10 @@ def tzinfo(self):
"""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 @@ def _cmp(self, other, allow_mixed=False):
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 @@ def _tzstr(self, sep=":"):
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 @@ def __repr__(self):
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 @@ def dst(self):
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 @@ def replace(self, hour=None, minute=None, second=None, microsecond=None,
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 @@ def _getstate(self):
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 @@ def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
self._microsecond = microsecond
self._tzinfo = tzinfo
self._hashcode = -1
self._fold = fold
return self
# Read-only field accessors
@ -1384,6 +1411,10 @@ def tzinfo(self):
"""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 @@ def _fromtimestamp(cls, t, utc, tz):
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 @@ def fromtimestamp(cls, t, tz=None):
"""
_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 @@ def combine(cls, date, time):
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 @@ def timetuple(self):
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 @@ def date(self):
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 @@ def replace(self, year=None, month=None, day=None, hour=None,
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 @@ def isoformat(self, sep='T', timespec='auto'):
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 @@ def __repr__(self):
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 @@ def _cmp(self, other, allow_mixed=False):
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 @@ def __sub__(self, other):
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 @@ def __hash__(self):
# 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 @@ def _getstate(self):
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)

View 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 @@ def test_isoformat(self):
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 @@ def test_bad_constructor_arguments(self):
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 @@ def test_timestamp_naive(self):
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 @@ def test_timestamp_aware(self):
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 @@ def test_replace(self):
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 @@ def dst(self, dt): return timedelta(hours=24)
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 Float(float):
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()

View file

@ -112,6 +112,8 @@
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 @@
RESOURCE_NAMES = ('audio', 'curses', 'largefile', 'network',
'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui')
'decimal', 'cpu', 'subprocess', 'urlfetch', 'gui', 'tzdata')
class _ArgParser(argparse.ArgumentParser):

View file

@ -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

File diff suppressed because it is too large Load diff

81
Tools/tz/zdump.py Normal file
View file

@ -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)