Closes issue #24773: Implement PEP 495 (Local Time Disambiguation).
This commit is contained in:
parent
638e622055
commit
5d0c598382
|
@ -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)
|
||||
|
|
242
Lib/datetime.py
242
Lib/datetime.py
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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
|
@ -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)
|
Loading…
Reference in New Issue