Kaydet (Commit) 6c7a4182 authored tarafından Alexander Belopolsky's avatar Alexander Belopolsky

Closes issue #20858: Enhancements/fixes to pure-python datetime module

This patch brings the pure-python datetime more in-line with the C
module.  Patch contributed by Brian Kearns, a PyPy developer.  PyPy
project has been running these modifications in PyPy2 stdlib.

This commit includes:

- General PEP8/cleanups;
- Better testing of argument types passed to constructors;
- Removal of duplicate operations;
- Optimization of timedelta creation;
- Caching the result of __hash__ like the C accelerator;
- Enhancements/bug fixes in tests.
üst a2f93885
...@@ -12,7 +12,7 @@ def _cmp(x, y): ...@@ -12,7 +12,7 @@ def _cmp(x, y):
MINYEAR = 1 MINYEAR = 1
MAXYEAR = 9999 MAXYEAR = 9999
_MAXORDINAL = 3652059 # date.max.toordinal() _MAXORDINAL = 3652059 # date.max.toordinal()
# Utility functions, adapted from Python's Demo/classes/Dates.py, which # Utility functions, adapted from Python's Demo/classes/Dates.py, which
# also assumes the current Gregorian calendar indefinitely extended in # also assumes the current Gregorian calendar indefinitely extended in
...@@ -26,7 +26,7 @@ _MAXORDINAL = 3652059 # date.max.toordinal() ...@@ -26,7 +26,7 @@ _MAXORDINAL = 3652059 # date.max.toordinal()
# -1 is a placeholder for indexing purposes. # -1 is a placeholder for indexing purposes.
_DAYS_IN_MONTH = [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] _DAYS_IN_MONTH = [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
_DAYS_BEFORE_MONTH = [-1] # -1 is a placeholder for indexing purposes. _DAYS_BEFORE_MONTH = [-1] # -1 is a placeholder for indexing purposes.
dbm = 0 dbm = 0
for dim in _DAYS_IN_MONTH[1:]: for dim in _DAYS_IN_MONTH[1:]:
_DAYS_BEFORE_MONTH.append(dbm) _DAYS_BEFORE_MONTH.append(dbm)
...@@ -162,9 +162,9 @@ def _format_time(hh, mm, ss, us): ...@@ -162,9 +162,9 @@ def _format_time(hh, mm, ss, us):
# Correctly substitute for %z and %Z escapes in strftime formats. # Correctly substitute for %z and %Z escapes in strftime formats.
def _wrap_strftime(object, format, timetuple): def _wrap_strftime(object, format, timetuple):
# Don't call utcoffset() or tzname() unless actually needed. # Don't call utcoffset() or tzname() unless actually needed.
freplace = None # the string to use for %f freplace = None # the string to use for %f
zreplace = None # the string to use for %z zreplace = None # the string to use for %z
Zreplace = None # the string to use for %Z Zreplace = None # the string to use for %Z
# Scan format for %z and %Z escapes, replacing as needed. # Scan format for %z and %Z escapes, replacing as needed.
newformat = [] newformat = []
...@@ -217,11 +217,6 @@ def _wrap_strftime(object, format, timetuple): ...@@ -217,11 +217,6 @@ def _wrap_strftime(object, format, timetuple):
newformat = "".join(newformat) newformat = "".join(newformat)
return _time.strftime(newformat, timetuple) return _time.strftime(newformat, timetuple)
def _call_tzinfo_method(tzinfo, methname, tzinfoarg):
if tzinfo is None:
return None
return getattr(tzinfo, methname)(tzinfoarg)
# Just raise TypeError if the arg isn't None or a string. # Just raise TypeError if the arg isn't None or a string.
def _check_tzname(name): def _check_tzname(name):
if name is not None and not isinstance(name, str): if name is not None and not isinstance(name, str):
...@@ -245,13 +240,31 @@ def _check_utc_offset(name, offset): ...@@ -245,13 +240,31 @@ def _check_utc_offset(name, offset):
raise ValueError("tzinfo.%s() must return a whole number " raise ValueError("tzinfo.%s() must return a whole number "
"of minutes, got %s" % (name, offset)) "of minutes, got %s" % (name, offset))
if not -timedelta(1) < offset < timedelta(1): if not -timedelta(1) < offset < timedelta(1):
raise ValueError("%s()=%s, must be must be strictly between" raise ValueError("%s()=%s, must be must be strictly between "
" -timedelta(hours=24) and timedelta(hours=24)" "-timedelta(hours=24) and timedelta(hours=24)" %
% (name, offset)) (name, offset))
def _check_int_field(value):
if isinstance(value, int):
return value
if not isinstance(value, float):
try:
value = value.__int__()
except AttributeError:
pass
else:
if isinstance(value, int):
return value
raise TypeError('__int__ returned non-int (type %s)' %
type(value).__name__)
raise TypeError('an integer is required (got type %s)' %
type(value).__name__)
raise TypeError('integer argument expected, got float')
def _check_date_fields(year, month, day): def _check_date_fields(year, month, day):
if not isinstance(year, int): year = _check_int_field(year)
raise TypeError('int expected') month = _check_int_field(month)
day = _check_int_field(day)
if not MINYEAR <= year <= MAXYEAR: if not MINYEAR <= year <= MAXYEAR:
raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year) raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year)
if not 1 <= month <= 12: if not 1 <= month <= 12:
...@@ -259,10 +272,13 @@ def _check_date_fields(year, month, day): ...@@ -259,10 +272,13 @@ def _check_date_fields(year, month, day):
dim = _days_in_month(year, month) dim = _days_in_month(year, month)
if not 1 <= day <= dim: if not 1 <= day <= dim:
raise ValueError('day must be in 1..%d' % dim, day) raise ValueError('day must be in 1..%d' % dim, day)
return year, month, day
def _check_time_fields(hour, minute, second, microsecond): def _check_time_fields(hour, minute, second, microsecond):
if not isinstance(hour, int): hour = _check_int_field(hour)
raise TypeError('int expected') minute = _check_int_field(minute)
second = _check_int_field(second)
microsecond = _check_int_field(microsecond)
if not 0 <= hour <= 23: if not 0 <= hour <= 23:
raise ValueError('hour must be in 0..23', hour) raise ValueError('hour must be in 0..23', hour)
if not 0 <= minute <= 59: if not 0 <= minute <= 59:
...@@ -271,6 +287,7 @@ def _check_time_fields(hour, minute, second, microsecond): ...@@ -271,6 +287,7 @@ def _check_time_fields(hour, minute, second, microsecond):
raise ValueError('second must be in 0..59', second) raise ValueError('second must be in 0..59', second)
if not 0 <= microsecond <= 999999: if not 0 <= microsecond <= 999999:
raise ValueError('microsecond must be in 0..999999', microsecond) raise ValueError('microsecond must be in 0..999999', microsecond)
return hour, minute, second, microsecond
def _check_tzinfo_arg(tz): def _check_tzinfo_arg(tz):
if tz is not None and not isinstance(tz, tzinfo): if tz is not None and not isinstance(tz, tzinfo):
...@@ -297,7 +314,7 @@ class timedelta: ...@@ -297,7 +314,7 @@ class timedelta:
Representation: (days, seconds, microseconds). Why? Because I Representation: (days, seconds, microseconds). Why? Because I
felt like it. felt like it.
""" """
__slots__ = '_days', '_seconds', '_microseconds' __slots__ = '_days', '_seconds', '_microseconds', '_hashcode'
def __new__(cls, days=0, seconds=0, microseconds=0, def __new__(cls, days=0, seconds=0, microseconds=0,
milliseconds=0, minutes=0, hours=0, weeks=0): milliseconds=0, minutes=0, hours=0, weeks=0):
...@@ -363,38 +380,26 @@ class timedelta: ...@@ -363,38 +380,26 @@ class timedelta:
# secondsfrac isn't referenced again # secondsfrac isn't referenced again
if isinstance(microseconds, float): if isinstance(microseconds, float):
microseconds += usdouble microseconds = round(microseconds + usdouble)
microseconds = round(microseconds, 0) seconds, microseconds = divmod(microseconds, 1000000)
seconds, microseconds = divmod(microseconds, 1e6) days, seconds = divmod(seconds, 24*3600)
assert microseconds == int(microseconds) d += days
assert seconds == int(seconds) s += seconds
days, seconds = divmod(seconds, 24.*3600.)
assert days == int(days)
assert seconds == int(seconds)
d += int(days)
s += int(seconds) # can't overflow
assert isinstance(s, int)
assert abs(s) <= 3 * 24 * 3600
else: else:
microseconds = int(microseconds)
seconds, microseconds = divmod(microseconds, 1000000) seconds, microseconds = divmod(microseconds, 1000000)
days, seconds = divmod(seconds, 24*3600) days, seconds = divmod(seconds, 24*3600)
d += days d += days
s += int(seconds) # can't overflow s += seconds
assert isinstance(s, int) microseconds = round(microseconds + usdouble)
assert abs(s) <= 3 * 24 * 3600 assert isinstance(s, int)
microseconds = float(microseconds) assert isinstance(microseconds, int)
microseconds += usdouble
microseconds = round(microseconds, 0)
assert abs(s) <= 3 * 24 * 3600 assert abs(s) <= 3 * 24 * 3600
assert abs(microseconds) < 3.1e6 assert abs(microseconds) < 3.1e6
# Just a little bit of carrying possible for microseconds and seconds. # Just a little bit of carrying possible for microseconds and seconds.
assert isinstance(microseconds, float) seconds, us = divmod(microseconds, 1000000)
assert int(microseconds) == microseconds s += seconds
us = int(microseconds)
seconds, us = divmod(us, 1000000)
s += seconds # cant't overflow
assert isinstance(s, int)
days, s = divmod(s, 24*3600) days, s = divmod(s, 24*3600)
d += days d += days
...@@ -402,14 +407,14 @@ class timedelta: ...@@ -402,14 +407,14 @@ class timedelta:
assert isinstance(s, int) and 0 <= s < 24*3600 assert isinstance(s, int) and 0 <= s < 24*3600
assert isinstance(us, int) and 0 <= us < 1000000 assert isinstance(us, int) and 0 <= us < 1000000
self = object.__new__(cls) if abs(d) > 999999999:
raise OverflowError("timedelta # of days is too large: %d" % d)
self = object.__new__(cls)
self._days = d self._days = d
self._seconds = s self._seconds = s
self._microseconds = us self._microseconds = us
if abs(d) > 999999999: self._hashcode = -1
raise OverflowError("timedelta # of days is too large: %d" % d)
return self return self
def __repr__(self): def __repr__(self):
...@@ -442,7 +447,7 @@ class timedelta: ...@@ -442,7 +447,7 @@ class timedelta:
def total_seconds(self): def total_seconds(self):
"""Total seconds in the duration.""" """Total seconds in the duration."""
return ((self.days * 86400 + self.seconds)*10**6 + return ((self.days * 86400 + self.seconds) * 10**6 +
self.microseconds) / 10**6 self.microseconds) / 10**6
# Read-only field accessors # Read-only field accessors
...@@ -597,7 +602,9 @@ class timedelta: ...@@ -597,7 +602,9 @@ class timedelta:
return _cmp(self._getstate(), other._getstate()) return _cmp(self._getstate(), other._getstate())
def __hash__(self): def __hash__(self):
return hash(self._getstate()) if self._hashcode == -1:
self._hashcode = hash(self._getstate())
return self._hashcode
def __bool__(self): def __bool__(self):
return (self._days != 0 or return (self._days != 0 or
...@@ -645,7 +652,7 @@ class date: ...@@ -645,7 +652,7 @@ class date:
Properties (readonly): Properties (readonly):
year, month, day year, month, day
""" """
__slots__ = '_year', '_month', '_day' __slots__ = '_year', '_month', '_day', '_hashcode'
def __new__(cls, year, month=None, day=None): def __new__(cls, year, month=None, day=None):
"""Constructor. """Constructor.
...@@ -654,17 +661,19 @@ class date: ...@@ -654,17 +661,19 @@ class date:
year, month, day (required, base 1) year, month, day (required, base 1)
""" """
if (isinstance(year, bytes) and len(year) == 4 and if month is None and isinstance(year, bytes) and len(year) == 4 and \
1 <= year[2] <= 12 and month is None): # Month is sane 1 <= year[2] <= 12:
# Pickle support # Pickle support
self = object.__new__(cls) self = object.__new__(cls)
self.__setstate(year) self.__setstate(year)
self._hashcode = -1
return self return self
_check_date_fields(year, month, day) year, month, day = _check_date_fields(year, month, day)
self = object.__new__(cls) self = object.__new__(cls)
self._year = year self._year = year
self._month = month self._month = month
self._day = day self._day = day
self._hashcode = -1
return self return self
# Additional constructors # Additional constructors
...@@ -728,6 +737,8 @@ class date: ...@@ -728,6 +737,8 @@ class date:
return _wrap_strftime(self, fmt, self.timetuple()) return _wrap_strftime(self, fmt, self.timetuple())
def __format__(self, fmt): def __format__(self, fmt):
if not isinstance(fmt, str):
raise TypeError("must be str, not %s" % type(fmt).__name__)
if len(fmt) != 0: if len(fmt) != 0:
return self.strftime(fmt) return self.strftime(fmt)
return str(self) return str(self)
...@@ -784,7 +795,6 @@ class date: ...@@ -784,7 +795,6 @@ class date:
month = self._month month = self._month
if day is None: if day is None:
day = self._day day = self._day
_check_date_fields(year, month, day)
return date(year, month, day) return date(year, month, day)
# Comparisons of date objects with other. # Comparisons of date objects with other.
...@@ -827,7 +837,9 @@ class date: ...@@ -827,7 +837,9 @@ class date:
def __hash__(self): def __hash__(self):
"Hash." "Hash."
return hash(self._getstate()) if self._hashcode == -1:
self._hashcode = hash(self._getstate())
return self._hashcode
# Computations # Computations
...@@ -897,8 +909,6 @@ class date: ...@@ -897,8 +909,6 @@ class date:
return bytes([yhi, ylo, self._month, self._day]), return bytes([yhi, ylo, self._month, self._day]),
def __setstate(self, string): def __setstate(self, string):
if len(string) != 4 or not (1 <= string[2] <= 12):
raise TypeError("not enough arguments")
yhi, ylo, self._month, self._day = string yhi, ylo, self._month, self._day = string
self._year = yhi * 256 + ylo self._year = yhi * 256 + ylo
...@@ -917,6 +927,7 @@ class tzinfo: ...@@ -917,6 +927,7 @@ class tzinfo:
Subclasses must override the name(), utcoffset() and dst() methods. Subclasses must override the name(), utcoffset() and dst() methods.
""" """
__slots__ = () __slots__ = ()
def tzname(self, dt): def tzname(self, dt):
"datetime -> string name of time zone." "datetime -> string name of time zone."
raise NotImplementedError("tzinfo subclass must override tzname()") raise NotImplementedError("tzinfo subclass must override tzname()")
...@@ -1003,6 +1014,7 @@ class time: ...@@ -1003,6 +1014,7 @@ class time:
Properties (readonly): Properties (readonly):
hour, minute, second, microsecond, tzinfo hour, minute, second, microsecond, tzinfo
""" """
__slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode'
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):
"""Constructor. """Constructor.
...@@ -1013,18 +1025,22 @@ class time: ...@@ -1013,18 +1025,22 @@ class time:
second, microsecond (default to zero) second, microsecond (default to zero)
tzinfo (default to None) tzinfo (default to None)
""" """
self = object.__new__(cls) if isinstance(hour, bytes) and len(hour) == 6 and hour[0] < 24:
if isinstance(hour, bytes) and len(hour) == 6:
# Pickle support # Pickle support
self = object.__new__(cls)
self.__setstate(hour, minute or None) self.__setstate(hour, minute or None)
self._hashcode = -1
return self return self
hour, minute, second, microsecond = _check_time_fields(
hour, minute, second, microsecond)
_check_tzinfo_arg(tzinfo) _check_tzinfo_arg(tzinfo)
_check_time_fields(hour, minute, second, microsecond) self = object.__new__(cls)
self._hour = hour self._hour = hour
self._minute = minute self._minute = minute
self._second = second self._second = second
self._microsecond = microsecond self._microsecond = microsecond
self._tzinfo = tzinfo self._tzinfo = tzinfo
self._hashcode = -1
return self return self
# Read-only field accessors # Read-only field accessors
...@@ -1109,8 +1125,8 @@ class time: ...@@ -1109,8 +1125,8 @@ class time:
if base_compare: if base_compare:
return _cmp((self._hour, self._minute, self._second, return _cmp((self._hour, self._minute, self._second,
self._microsecond), self._microsecond),
(other._hour, other._minute, other._second, (other._hour, other._minute, other._second,
other._microsecond)) other._microsecond))
if myoff is None or otoff is None: if myoff is None or otoff is None:
if allow_mixed: if allow_mixed:
return 2 # arbitrary non-zero value return 2 # arbitrary non-zero value
...@@ -1123,16 +1139,20 @@ class time: ...@@ -1123,16 +1139,20 @@ class time:
def __hash__(self): def __hash__(self):
"""Hash.""" """Hash."""
tzoff = self.utcoffset() if self._hashcode == -1:
if not tzoff: # zero or None tzoff = self.utcoffset()
return hash(self._getstate()[0]) if not tzoff: # zero or None
h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff, self._hashcode = hash(self._getstate()[0])
timedelta(hours=1)) else:
assert not m % timedelta(minutes=1), "whole minute" h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff,
m //= timedelta(minutes=1) timedelta(hours=1))
if 0 <= h < 24: assert not m % timedelta(minutes=1), "whole minute"
return hash(time(h, m, self.second, self.microsecond)) m //= timedelta(minutes=1)
return hash((h, m, self.second, self.microsecond)) if 0 <= h < 24:
self._hashcode = hash(time(h, m, self.second, self.microsecond))
else:
self._hashcode = hash((h, m, self.second, self.microsecond))
return self._hashcode
# Conversion to string # Conversion to string
...@@ -1195,6 +1215,8 @@ class time: ...@@ -1195,6 +1215,8 @@ class time:
return _wrap_strftime(self, fmt, timetuple) return _wrap_strftime(self, fmt, timetuple)
def __format__(self, fmt): def __format__(self, fmt):
if not isinstance(fmt, str):
raise TypeError("must be str, not %s" % type(fmt).__name__)
if len(fmt) != 0: if len(fmt) != 0:
return self.strftime(fmt) return self.strftime(fmt)
return str(self) return str(self)
...@@ -1251,8 +1273,6 @@ class time: ...@@ -1251,8 +1273,6 @@ class time:
microsecond = self.microsecond microsecond = self.microsecond
if tzinfo is True: if tzinfo is True:
tzinfo = self.tzinfo tzinfo = self.tzinfo
_check_time_fields(hour, minute, second, microsecond)
_check_tzinfo_arg(tzinfo)
return time(hour, minute, second, microsecond, tzinfo) return time(hour, minute, second, microsecond, tzinfo)
# Pickle support. # Pickle support.
...@@ -1268,15 +1288,11 @@ class time: ...@@ -1268,15 +1288,11 @@ class time:
return (basestate, self._tzinfo) return (basestate, self._tzinfo)
def __setstate(self, string, tzinfo): def __setstate(self, string, tzinfo):
if len(string) != 6 or string[0] >= 24: if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class):
raise TypeError("an integer is required") raise TypeError("bad tzinfo state arg")
(self._hour, self._minute, self._second, self._hour, self._minute, self._second, us1, us2, us3 = string
us1, us2, us3) = string
self._microsecond = (((us1 << 8) | us2) << 8) | us3 self._microsecond = (((us1 << 8) | us2) << 8) | us3
if tzinfo is None or isinstance(tzinfo, _tzinfo_class): self._tzinfo = tzinfo
self._tzinfo = tzinfo
else:
raise TypeError("bad tzinfo state arg %r" % tzinfo)
def __reduce__(self): def __reduce__(self):
return (time, self._getstate()) return (time, self._getstate())
...@@ -1293,25 +1309,30 @@ class datetime(date): ...@@ -1293,25 +1309,30 @@ class datetime(date):
The year, month and day arguments are required. tzinfo may be None, or an The year, month and day arguments are required. tzinfo may be None, or an
instance of a tzinfo subclass. The remaining arguments may be ints. instance of a tzinfo subclass. The remaining arguments may be ints.
""" """
__slots__ = date.__slots__ + time.__slots__
__slots__ = date.__slots__ + (
'_hour', '_minute', '_second',
'_microsecond', '_tzinfo')
def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0, def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0,
microsecond=0, tzinfo=None): microsecond=0, tzinfo=None):
if isinstance(year, bytes) and len(year) == 10: if isinstance(year, bytes) and len(year) == 10 and 1 <= year[2] <= 12:
# Pickle support # Pickle support
self = date.__new__(cls, year[:4]) self = object.__new__(cls)
self.__setstate(year, month) self.__setstate(year, month)
self._hashcode = -1
return self return self
year, month, day = _check_date_fields(year, month, day)
hour, minute, second, microsecond = _check_time_fields(
hour, minute, second, microsecond)
_check_tzinfo_arg(tzinfo) _check_tzinfo_arg(tzinfo)
_check_time_fields(hour, minute, second, microsecond) self = object.__new__(cls)
self = date.__new__(cls, year, month, day) self._year = year
self._month = month
self._day = day
self._hour = hour self._hour = hour
self._minute = minute self._minute = minute
self._second = second self._second = second
self._microsecond = microsecond self._microsecond = microsecond
self._tzinfo = tzinfo self._tzinfo = tzinfo
self._hashcode = -1
return self return self
# Read-only field accessors # Read-only field accessors
...@@ -1346,7 +1367,6 @@ class datetime(date): ...@@ -1346,7 +1367,6 @@ class datetime(date):
A timezone info object may be passed in as well. A timezone info object may be passed in as well.
""" """
_check_tzinfo_arg(tz) _check_tzinfo_arg(tz)
converter = _time.localtime if tz is None else _time.gmtime converter = _time.localtime if tz is None else _time.gmtime
...@@ -1385,11 +1405,6 @@ class datetime(date): ...@@ -1385,11 +1405,6 @@ class datetime(date):
ss = min(ss, 59) # clamp out leap seconds if the platform has them ss = min(ss, 59) # clamp out leap seconds if the platform has them
return cls(y, m, d, hh, mm, ss, us) return cls(y, m, d, hh, mm, ss, us)
# XXX This is supposed to do better than we *can* do by using time.time(),
# XXX if the platform supports a more accurate way. The C implementation
# XXX uses gettimeofday on platforms that have it, but that isn't
# XXX available from Python. So now() may return different results
# XXX across the implementations.
@classmethod @classmethod
def now(cls, tz=None): def now(cls, tz=None):
"Construct a datetime from time.time() and optional time zone info." "Construct a datetime from time.time() and optional time zone info."
...@@ -1476,11 +1491,8 @@ class datetime(date): ...@@ -1476,11 +1491,8 @@ class datetime(date):
microsecond = self.microsecond microsecond = self.microsecond
if tzinfo is True: if tzinfo is True:
tzinfo = self.tzinfo tzinfo = self.tzinfo
_check_date_fields(year, month, day) return datetime(year, month, day, hour, minute, second, microsecond,
_check_time_fields(hour, minute, second, microsecond) tzinfo)
_check_tzinfo_arg(tzinfo)
return datetime(year, month, day, hour, minute, second,
microsecond, tzinfo)
def astimezone(self, tz=None): def astimezone(self, tz=None):
if tz is None: if tz is None:
...@@ -1550,10 +1562,9 @@ class datetime(date): ...@@ -1550,10 +1562,9 @@ class datetime(date):
Optional argument sep specifies the separator between date and Optional argument sep specifies the separator between date and
time, default 'T'. time, default 'T'.
""" """
s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) +
sep) + _format_time(self._hour, self._minute, self._second,
_format_time(self._hour, self._minute, self._second, self._microsecond))
self._microsecond))
off = self.utcoffset() off = self.utcoffset()
if off is not None: if off is not None:
if off.days < 0: if off.days < 0:
...@@ -1569,7 +1580,7 @@ class datetime(date): ...@@ -1569,7 +1580,7 @@ class datetime(date):
def __repr__(self): def __repr__(self):
"""Convert to formal string, for repr().""" """Convert to formal string, for repr()."""
L = [self._year, self._month, self._day, # These are never zero L = [self._year, self._month, self._day, # These are never zero
self._hour, self._minute, self._second, self._microsecond] self._hour, self._minute, self._second, self._microsecond]
if L[-1] == 0: if L[-1] == 0:
del L[-1] del L[-1]
...@@ -1609,7 +1620,9 @@ class datetime(date): ...@@ -1609,7 +1620,9 @@ class datetime(date):
it mean anything in particular. For example, "GMT", "UTC", "-500", it mean anything in particular. For example, "GMT", "UTC", "-500",
"-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies. "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies.
""" """
name = _call_tzinfo_method(self._tzinfo, "tzname", self) if self._tzinfo is None:
return None
name = self._tzinfo.tzname(self)
_check_tzname(name) _check_tzname(name)
return name return name
...@@ -1695,9 +1708,9 @@ class datetime(date): ...@@ -1695,9 +1708,9 @@ class datetime(date):
return _cmp((self._year, self._month, self._day, return _cmp((self._year, self._month, self._day,
self._hour, self._minute, self._second, self._hour, self._minute, self._second,
self._microsecond), self._microsecond),
(other._year, other._month, other._day, (other._year, other._month, other._day,
other._hour, other._minute, other._second, other._hour, other._minute, other._second,
other._microsecond)) other._microsecond))
if myoff is None or otoff is None: if myoff is None or otoff is None:
if allow_mixed: if allow_mixed:
return 2 # arbitrary non-zero value return 2 # arbitrary non-zero value
...@@ -1755,12 +1768,15 @@ class datetime(date): ...@@ -1755,12 +1768,15 @@ class datetime(date):
return base + otoff - myoff return base + otoff - myoff
def __hash__(self): def __hash__(self):
tzoff = self.utcoffset() if self._hashcode == -1:
if tzoff is None: tzoff = self.utcoffset()
return hash(self._getstate()[0]) if tzoff is None:
days = _ymd2ord(self.year, self.month, self.day) self._hashcode = hash(self._getstate()[0])
seconds = self.hour * 3600 + self.minute * 60 + self.second else:
return hash(timedelta(days, seconds, self.microsecond) - tzoff) days = _ymd2ord(self.year, self.month, self.day)
seconds = self.hour * 3600 + self.minute * 60 + self.second
self._hashcode = hash(timedelta(days, seconds, self.microsecond) - tzoff)
return self._hashcode
# Pickle support. # Pickle support.
...@@ -1777,14 +1793,13 @@ class datetime(date): ...@@ -1777,14 +1793,13 @@ class datetime(date):
return (basestate, self._tzinfo) return (basestate, self._tzinfo)
def __setstate(self, string, tzinfo): 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, self._month, self._day, self._hour,
self._minute, self._second, us1, us2, us3) = string self._minute, self._second, us1, us2, us3) = string
self._year = yhi * 256 + ylo self._year = yhi * 256 + ylo
self._microsecond = (((us1 << 8) | us2) << 8) | us3 self._microsecond = (((us1 << 8) | us2) << 8) | us3
if tzinfo is None or isinstance(tzinfo, _tzinfo_class): self._tzinfo = tzinfo
self._tzinfo = tzinfo
else:
raise TypeError("bad tzinfo state arg %r" % tzinfo)
def __reduce__(self): def __reduce__(self):
return (self.__class__, self._getstate()) return (self.__class__, self._getstate())
...@@ -1800,7 +1815,7 @@ def _isoweek1monday(year): ...@@ -1800,7 +1815,7 @@ def _isoweek1monday(year):
# XXX This could be done more efficiently # XXX This could be done more efficiently
THURSDAY = 3 THURSDAY = 3
firstday = _ymd2ord(year, 1, 1) firstday = _ymd2ord(year, 1, 1)
firstweekday = (firstday + 6) % 7 # See weekday() above firstweekday = (firstday + 6) % 7 # See weekday() above
week1monday = firstday - firstweekday week1monday = firstday - firstweekday
if firstweekday > THURSDAY: if firstweekday > THURSDAY:
week1monday += 7 week1monday += 7
...@@ -1821,13 +1836,12 @@ class timezone(tzinfo): ...@@ -1821,13 +1836,12 @@ class timezone(tzinfo):
elif not isinstance(name, str): elif not isinstance(name, str):
raise TypeError("name must be a string") raise TypeError("name must be a string")
if not cls._minoffset <= offset <= cls._maxoffset: if not cls._minoffset <= offset <= cls._maxoffset:
raise ValueError("offset must be a timedelta" raise ValueError("offset must be a timedelta "
" strictly between -timedelta(hours=24) and" "strictly between -timedelta(hours=24) and "
" timedelta(hours=24).") "timedelta(hours=24).")
if (offset.microseconds != 0 or if (offset.microseconds != 0 or offset.seconds % 60 != 0):
offset.seconds % 60 != 0): raise ValueError("offset must be a timedelta "
raise ValueError("offset must be a timedelta" "representing a whole number of minutes")
" representing a whole number of minutes")
return cls._create(offset, name) return cls._create(offset, name)
@classmethod @classmethod
...@@ -2124,14 +2138,13 @@ except ImportError: ...@@ -2124,14 +2138,13 @@ except ImportError:
pass pass
else: else:
# Clean up unused names # Clean up unused names
del (_DAYNAMES, _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH, del (_DAYNAMES, _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH, _DI100Y, _DI400Y,
_DI100Y, _DI400Y, _DI4Y, _MAXORDINAL, _MONTHNAMES, _DI4Y, _EPOCH, _MAXORDINAL, _MONTHNAMES, _build_struct_time,
_build_struct_time, _call_tzinfo_method, _check_date_fields, _check_date_fields, _check_int_field, _check_time_fields,
_check_time_fields, _check_tzinfo_arg, _check_tzname, _check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror,
_check_utc_offset, _cmp, _cmperror, _date_class, _days_before_month, _date_class, _days_before_month, _days_before_year, _days_in_month,
_days_before_year, _days_in_month, _format_time, _is_leap, _format_time, _is_leap, _isoweek1monday, _math, _ord2ymd,
_isoweek1monday, _math, _ord2ymd, _time, _time_class, _tzinfo_class, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord)
_wrap_strftime, _ymd2ord)
# XXX Since import * above excludes names that start with _, # XXX Since import * above excludes names that start with _,
# docstring does not get overwritten. In the future, it may be # docstring does not get overwritten. In the future, it may be
# appropriate to maintain a single module level docstring and # appropriate to maintain a single module level docstring and
......
...@@ -50,6 +50,17 @@ class TestModule(unittest.TestCase): ...@@ -50,6 +50,17 @@ class TestModule(unittest.TestCase):
self.assertEqual(datetime.MINYEAR, 1) self.assertEqual(datetime.MINYEAR, 1)
self.assertEqual(datetime.MAXYEAR, 9999) self.assertEqual(datetime.MAXYEAR, 9999)
def test_name_cleanup(self):
if '_Fast' not in str(self):
return
datetime = datetime_module
names = set(name for name in dir(datetime)
if not name.startswith('__') and not name.endswith('__'))
allowed = set(['MAXYEAR', 'MINYEAR', 'date', 'datetime',
'datetime_CAPI', 'time', 'timedelta', 'timezone',
'tzinfo'])
self.assertEqual(names - allowed, set([]))
############################################################################# #############################################################################
# tzinfo tests # tzinfo tests
...@@ -616,8 +627,12 @@ class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase): ...@@ -616,8 +627,12 @@ class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase):
# Single-field rounding. # Single-field rounding.
eq(td(milliseconds=0.4/1000), td(0)) # rounds to 0 eq(td(milliseconds=0.4/1000), td(0)) # rounds to 0
eq(td(milliseconds=-0.4/1000), td(0)) # rounds to 0 eq(td(milliseconds=-0.4/1000), td(0)) # rounds to 0
eq(td(milliseconds=0.5/1000), td(microseconds=0))
eq(td(milliseconds=-0.5/1000), td(microseconds=0))
eq(td(milliseconds=0.6/1000), td(microseconds=1)) eq(td(milliseconds=0.6/1000), td(microseconds=1))
eq(td(milliseconds=-0.6/1000), td(microseconds=-1)) eq(td(milliseconds=-0.6/1000), td(microseconds=-1))
eq(td(seconds=0.5/10**6), td(microseconds=0))
eq(td(seconds=-0.5/10**6), td(microseconds=0))
# Rounding due to contributions from more than one field. # Rounding due to contributions from more than one field.
us_per_hour = 3600e6 us_per_hour = 3600e6
...@@ -1131,11 +1146,13 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): ...@@ -1131,11 +1146,13 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
#check that this standard extension works #check that this standard extension works
t.strftime("%f") t.strftime("%f")
def test_format(self): def test_format(self):
dt = self.theclass(2007, 9, 10) dt = self.theclass(2007, 9, 10)
self.assertEqual(dt.__format__(''), str(dt)) self.assertEqual(dt.__format__(''), str(dt))
with self.assertRaisesRegex(TypeError, '^must be str, not int$'):
dt.__format__(123)
# check that a derived class's __str__() gets called # check that a derived class's __str__() gets called
class A(self.theclass): class A(self.theclass):
def __str__(self): def __str__(self):
...@@ -1391,9 +1408,10 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase): ...@@ -1391,9 +1408,10 @@ class TestDate(HarmlessMixedComparison, unittest.TestCase):
for month_byte in b'9', b'\0', b'\r', b'\xff': for month_byte in b'9', b'\0', b'\r', b'\xff':
self.assertRaises(TypeError, self.theclass, self.assertRaises(TypeError, self.theclass,
base[:2] + month_byte + base[3:]) base[:2] + month_byte + base[3:])
# Good bytes, but bad tzinfo: if issubclass(self.theclass, datetime):
self.assertRaises(TypeError, self.theclass, # Good bytes, but bad tzinfo:
bytes([1] * len(base)), 'EST') with self.assertRaisesRegex(TypeError, '^bad tzinfo state arg$'):
self.theclass(bytes([1] * len(base)), 'EST')
for ord_byte in range(1, 13): for ord_byte in range(1, 13):
# This shouldn't blow up because of the month byte alone. If # This shouldn't blow up because of the month byte alone. If
...@@ -1469,6 +1487,9 @@ class TestDateTime(TestDate): ...@@ -1469,6 +1487,9 @@ class TestDateTime(TestDate):
dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) dt = self.theclass(2007, 9, 10, 4, 5, 1, 123)
self.assertEqual(dt.__format__(''), str(dt)) self.assertEqual(dt.__format__(''), str(dt))
with self.assertRaisesRegex(TypeError, '^must be str, not int$'):
dt.__format__(123)
# check that a derived class's __str__() gets called # check that a derived class's __str__() gets called
class A(self.theclass): class A(self.theclass):
def __str__(self): def __str__(self):
...@@ -1789,6 +1810,7 @@ class TestDateTime(TestDate): ...@@ -1789,6 +1810,7 @@ class TestDateTime(TestDate):
tzinfo=timezone(timedelta(hours=-5), 'EST')) tzinfo=timezone(timedelta(hours=-5), 'EST'))
self.assertEqual(t.timestamp(), self.assertEqual(t.timestamp(),
18000 + 3600 + 2*60 + 3 + 4*1e-6) 18000 + 3600 + 2*60 + 3 + 4*1e-6)
def test_microsecond_rounding(self): def test_microsecond_rounding(self):
for fts in [self.theclass.fromtimestamp, for fts in [self.theclass.fromtimestamp,
self.theclass.utcfromtimestamp]: self.theclass.utcfromtimestamp]:
...@@ -1839,6 +1861,7 @@ class TestDateTime(TestDate): ...@@ -1839,6 +1861,7 @@ class TestDateTime(TestDate):
for insane in -1e200, 1e200: for insane in -1e200, 1e200:
self.assertRaises(OverflowError, self.theclass.utcfromtimestamp, self.assertRaises(OverflowError, self.theclass.utcfromtimestamp,
insane) insane)
@unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps")
def test_negative_float_fromtimestamp(self): def test_negative_float_fromtimestamp(self):
# The result is tz-dependent; at least test that this doesn't # The result is tz-dependent; at least test that this doesn't
...@@ -2218,6 +2241,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): ...@@ -2218,6 +2241,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
t = self.theclass(1, 2, 3, 4) t = self.theclass(1, 2, 3, 4)
self.assertEqual(t.__format__(''), str(t)) self.assertEqual(t.__format__(''), str(t))
with self.assertRaisesRegex(TypeError, '^must be str, not int$'):
t.__format__(123)
# check that a derived class's __str__() gets called # check that a derived class's __str__() gets called
class A(self.theclass): class A(self.theclass):
def __str__(self): def __str__(self):
...@@ -2347,6 +2373,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase): ...@@ -2347,6 +2373,9 @@ class TestTime(HarmlessMixedComparison, unittest.TestCase):
for hour_byte in ' ', '9', chr(24), '\xff': for hour_byte in ' ', '9', chr(24), '\xff':
self.assertRaises(TypeError, self.theclass, self.assertRaises(TypeError, self.theclass,
hour_byte + base[1:]) hour_byte + base[1:])
# Good bytes, but bad tzinfo:
with self.assertRaisesRegex(TypeError, '^bad tzinfo state arg$'):
self.theclass(bytes([1] * len(base)), 'EST')
# A mixin for classes with a tzinfo= argument. Subclasses must define # A mixin for classes with a tzinfo= argument. Subclasses must define
# theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) # theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever)
...@@ -2606,7 +2635,7 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase): ...@@ -2606,7 +2635,7 @@ class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase):
self.assertRaises(TypeError, t.strftime, "%Z") self.assertRaises(TypeError, t.strftime, "%Z")
# Issue #6697: # Issue #6697:
if '_Fast' in str(type(self)): if '_Fast' in str(self):
Badtzname.tz = '\ud800' Badtzname.tz = '\ud800'
self.assertRaises(ValueError, t.strftime, "%Z") self.assertRaises(ValueError, t.strftime, "%Z")
...@@ -3768,6 +3797,61 @@ class Oddballs(unittest.TestCase): ...@@ -3768,6 +3797,61 @@ class Oddballs(unittest.TestCase):
self.assertEqual(as_datetime, datetime_sc) self.assertEqual(as_datetime, datetime_sc)
self.assertEqual(datetime_sc, as_datetime) self.assertEqual(datetime_sc, as_datetime)
def test_extra_attributes(self):
for x in [date.today(),
time(),
datetime.utcnow(),
timedelta(),
tzinfo(),
timezone(timedelta())]:
with self.assertRaises(AttributeError):
x.abc = 1
def test_check_arg_types(self):
import decimal
class Number:
def __init__(self, value):
self.value = value
def __int__(self):
return self.value
for xx in [decimal.Decimal(10),
decimal.Decimal('10.9'),
Number(10)]:
self.assertEqual(datetime(10, 10, 10, 10, 10, 10, 10),
datetime(xx, xx, xx, xx, xx, xx, xx))
with self.assertRaisesRegex(TypeError, '^an integer is required '
'\(got type str\)$'):
datetime(10, 10, '10')
f10 = Number(10.9)
with self.assertRaisesRegex(TypeError, '^__int__ returned non-int '
'\(type float\)$'):
datetime(10, 10, f10)
class Float(float):
pass
s10 = Float(10.9)
with self.assertRaisesRegex(TypeError, '^integer argument expected, '
'got float$'):
datetime(10, 10, s10)
with self.assertRaises(TypeError):
datetime(10., 10, 10)
with self.assertRaises(TypeError):
datetime(10, 10., 10)
with self.assertRaises(TypeError):
datetime(10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10, 10, 10.)
with self.assertRaises(TypeError):
datetime(10, 10, 10, 10, 10, 10, 10.)
def test_main(): def test_main():
support.run_unittest(__name__) support.run_unittest(__name__)
......
...@@ -689,6 +689,7 @@ Per Øyvind Karlsen ...@@ -689,6 +689,7 @@ Per Øyvind Karlsen
Anton Kasyanov Anton Kasyanov
Lou Kates Lou Kates
Hiroaki Kawai Hiroaki Kawai
Brian Kearns
Sebastien Keim Sebastien Keim
Ryan Kelly Ryan Kelly
Dan Kenigsberg Dan Kenigsberg
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment