mirror of
https://github.com/python/cpython.git
synced 2025-10-09 16:34:44 +00:00
Closes issue #24773: Implement PEP 495 (Local Time Disambiguation).
This commit is contained in:
parent
638e622055
commit
5d0c598382
7 changed files with 1601 additions and 227 deletions
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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue