diff --git a/Lib/datetime.py b/Lib/datetime.py new file mode 100644 index 0000000000..a964b202e3 --- /dev/null +++ b/Lib/datetime.py @@ -0,0 +1,2472 @@ +"""Concrete date/time and related types. + +See http://www.iana.org/time-zones/repository/tz-link.html for +time zone and DST data sources. +""" + +import time as _time +import math as _math +import sys + +def _cmp(x, y): + return 0 if x == y else 1 if x > y else -1 + +MINYEAR = 1 +MAXYEAR = 9999 +_MAXORDINAL = 3652059 # date.max.toordinal() + +# Utility functions, adapted from Python's Demo/classes/Dates.py, which +# also assumes the current Gregorian calendar indefinitely extended in +# both directions. Difference: Dates.py calls January 1 of year 0 day +# number 1. The code here calls January 1 of year 1 day number 1. This is +# to match the definition of the "proleptic Gregorian" calendar in Dershowitz +# and Reingold's "Calendrical Calculations", where it's the base calendar +# for all computations. See the book for algorithms for converting between +# proleptic Gregorian ordinals and many other calendar systems. + +# -1 is a placeholder for indexing purposes. +_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. +dbm = 0 +for dim in _DAYS_IN_MONTH[1:]: + _DAYS_BEFORE_MONTH.append(dbm) + dbm += dim +del dbm, dim + +def _is_leap(year): + "year -> 1 if leap year, else 0." + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + +def _days_before_year(year): + "year -> number of days before January 1st of year." + y = year - 1 + return y*365 + y//4 - y//100 + y//400 + +def _days_in_month(year, month): + "year, month -> number of days in that month in that year." + assert 1 <= month <= 12, month + if month == 2 and _is_leap(year): + return 29 + return _DAYS_IN_MONTH[month] + +def _days_before_month(year, month): + "year, month -> number of days in year preceding first day of month." + assert 1 <= month <= 12, 'month must be in 1..12' + return _DAYS_BEFORE_MONTH[month] + (month > 2 and _is_leap(year)) + +def _ymd2ord(year, month, day): + "year, month, day -> ordinal, considering 01-Jan-0001 as day 1." + assert 1 <= month <= 12, 'month must be in 1..12' + dim = _days_in_month(year, month) + assert 1 <= day <= dim, ('day must be in 1..%d' % dim) + return (_days_before_year(year) + + _days_before_month(year, month) + + day) + +_DI400Y = _days_before_year(401) # number of days in 400 years +_DI100Y = _days_before_year(101) # " " " " 100 " +_DI4Y = _days_before_year(5) # " " " " 4 " + +# A 4-year cycle has an extra leap day over what we'd get from pasting +# together 4 single years. +assert _DI4Y == 4 * 365 + 1 + +# Similarly, a 400-year cycle has an extra leap day over what we'd get from +# pasting together 4 100-year cycles. +assert _DI400Y == 4 * _DI100Y + 1 + +# OTOH, a 100-year cycle has one fewer leap day than we'd get from +# pasting together 25 4-year cycles. +assert _DI100Y == 25 * _DI4Y - 1 + +def _ord2ymd(n): + "ordinal -> (year, month, day), considering 01-Jan-0001 as day 1." + + # n is a 1-based index, starting at 1-Jan-1. The pattern of leap years + # repeats exactly every 400 years. The basic strategy is to find the + # closest 400-year boundary at or before n, then work with the offset + # from that boundary to n. Life is much clearer if we subtract 1 from + # n first -- then the values of n at 400-year boundaries are exactly + # those divisible by _DI400Y: + # + # D M Y n n-1 + # -- --- ---- ---------- ---------------- + # 31 Dec -400 -_DI400Y -_DI400Y -1 + # 1 Jan -399 -_DI400Y +1 -_DI400Y 400-year boundary + # ... + # 30 Dec 000 -1 -2 + # 31 Dec 000 0 -1 + # 1 Jan 001 1 0 400-year boundary + # 2 Jan 001 2 1 + # 3 Jan 001 3 2 + # ... + # 31 Dec 400 _DI400Y _DI400Y -1 + # 1 Jan 401 _DI400Y +1 _DI400Y 400-year boundary + n -= 1 + n400, n = divmod(n, _DI400Y) + year = n400 * 400 + 1 # ..., -399, 1, 401, ... + + # Now n is the (non-negative) offset, in days, from January 1 of year, to + # the desired date. Now compute how many 100-year cycles precede n. + # Note that it's possible for n100 to equal 4! In that case 4 full + # 100-year cycles precede the desired day, which implies the desired + # day is December 31 at the end of a 400-year cycle. + n100, n = divmod(n, _DI100Y) + + # Now compute how many 4-year cycles precede it. + n4, n = divmod(n, _DI4Y) + + # And now how many single years. Again n1 can be 4, and again meaning + # that the desired day is December 31 at the end of the 4-year cycle. + n1, n = divmod(n, 365) + + year += n100 * 100 + n4 * 4 + n1 + if n1 == 4 or n100 == 4: + assert n == 0 + return year-1, 12, 31 + + # Now the year is correct, and n is the offset from January 1. We find + # the month via an estimate that's either exact or one too large. + leapyear = n1 == 3 and (n4 != 24 or n100 == 3) + assert leapyear == _is_leap(year) + month = (n + 50) >> 5 + preceding = _DAYS_BEFORE_MONTH[month] + (month > 2 and leapyear) + if preceding > n: # estimate is too large + month -= 1 + preceding -= _DAYS_IN_MONTH[month] + (month == 2 and leapyear) + n -= preceding + assert 0 <= n < _days_in_month(year, month) + + # Now the year and month are correct, and n is the offset from the + # start of that month: we're done! + return year, month, n+1 + +# Month and day names. For localized versions, see the calendar module. +_MONTHNAMES = [None, "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +_DAYNAMES = [None, "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + + +def _build_struct_time(y, m, d, hh, mm, ss, dstflag): + wday = (_ymd2ord(y, m, d) + 6) % 7 + dnum = _days_before_month(y, m) + d + return _time.struct_time((y, m, d, hh, mm, ss, wday, dnum, dstflag)) + +def _format_time(hh, mm, ss, us, timespec='auto'): + specs = { + 'hours': '{:02d}', + 'minutes': '{:02d}:{:02d}', + 'seconds': '{:02d}:{:02d}:{:02d}', + 'milliseconds': '{:02d}:{:02d}:{:02d}.{:03d}', + 'microseconds': '{:02d}:{:02d}:{:02d}.{:06d}' + } + + if timespec == 'auto': + # Skip trailing microseconds when us==0. + timespec = 'microseconds' if us else 'seconds' + elif timespec == 'milliseconds': + us //= 1000 + try: + fmt = specs[timespec] + except KeyError: + raise ValueError('Unknown timespec value') + else: + return fmt.format(hh, mm, ss, us) + +def _format_offset(off): + s = '' + if off is not None: + if off.days < 0: + sign = "-" + off = -off + else: + sign = "+" + hh, mm = divmod(off, timedelta(hours=1)) + mm, ss = divmod(mm, timedelta(minutes=1)) + s += "%s%02d:%02d" % (sign, hh, mm) + if ss or ss.microseconds: + s += ":%02d" % ss.seconds + + if ss.microseconds: + s += '.%06d' % ss.microseconds + return s + +# Correctly substitute for %z and %Z escapes in strftime formats. +def _wrap_strftime(object, format, timetuple): + # Don't call utcoffset() or tzname() unless actually needed. + freplace = None # the string to use for %f + 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. + newformat = [] + push = newformat.append + i, n = 0, len(format) + while i < n: + ch = format[i] + i += 1 + if ch == '%': + if i < n: + ch = format[i] + i += 1 + if ch == 'f': + if freplace is None: + freplace = '%06d' % getattr(object, + 'microsecond', 0) + newformat.append(freplace) + elif ch == 'z': + if zreplace is None: + zreplace = "" + if hasattr(object, "utcoffset"): + offset = object.utcoffset() + if offset is not None: + sign = '+' + if offset.days < 0: + offset = -offset + sign = '-' + h, rest = divmod(offset, timedelta(hours=1)) + m, rest = divmod(rest, timedelta(minutes=1)) + s = rest.seconds + u = offset.microseconds + if u: + zreplace = '%c%02d%02d%02d.%06d' % (sign, h, m, s, u) + elif s: + zreplace = '%c%02d%02d%02d' % (sign, h, m, s) + else: + zreplace = '%c%02d%02d' % (sign, h, m) + assert '%' not in zreplace + newformat.append(zreplace) + elif ch == 'Z': + if Zreplace is None: + Zreplace = "" + if hasattr(object, "tzname"): + s = object.tzname() + if s is not None: + # strftime is going to have at this: escape % + Zreplace = s.replace('%', '%%') + newformat.append(Zreplace) + else: + push('%') + push(ch) + else: + push('%') + else: + push(ch) + newformat = "".join(newformat) + return _time.strftime(newformat, timetuple) + +# Helpers for parsing the result of isoformat() +def _parse_isoformat_date(dtstr): + # It is assumed that this function will only be called with a + # string of length exactly 10, and (though this is not used) ASCII-only + year = int(dtstr[0:4]) + if dtstr[4] != '-': + raise ValueError('Invalid date separator: %s' % dtstr[4]) + + month = int(dtstr[5:7]) + + if dtstr[7] != '-': + raise ValueError('Invalid date separator') + + day = int(dtstr[8:10]) + + return [year, month, day] + +def _parse_hh_mm_ss_ff(tstr): + # Parses things of the form HH[:MM[:SS[.fff[fff]]]] + len_str = len(tstr) + + time_comps = [0, 0, 0, 0] + pos = 0 + for comp in range(0, 3): + if (len_str - pos) < 2: + raise ValueError('Incomplete time component') + + time_comps[comp] = int(tstr[pos:pos+2]) + + pos += 2 + next_char = tstr[pos:pos+1] + + if not next_char or comp >= 2: + break + + if next_char != ':': + raise ValueError('Invalid time separator: %c' % next_char) + + pos += 1 + + if pos < len_str: + if tstr[pos] != '.': + raise ValueError('Invalid microsecond component') + else: + pos += 1 + + len_remainder = len_str - pos + if len_remainder not in (3, 6): + raise ValueError('Invalid microsecond component') + + time_comps[3] = int(tstr[pos:]) + if len_remainder == 3: + time_comps[3] *= 1000 + + return time_comps + +def _parse_isoformat_time(tstr): + # Format supported is HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]] + len_str = len(tstr) + if len_str < 2: + raise ValueError('Isoformat time too short') + + # This is equivalent to re.search('[+-]', tstr), but faster + tz_pos = (tstr.find('-') + 1 or tstr.find('+') + 1) + timestr = tstr[:tz_pos-1] if tz_pos > 0 else tstr + + time_comps = _parse_hh_mm_ss_ff(timestr) + + tzi = None + if tz_pos > 0: + tzstr = tstr[tz_pos:] + + # Valid time zone strings are: + # HH:MM len: 5 + # HH:MM:SS len: 8 + # HH:MM:SS.ffffff len: 15 + + if len(tzstr) not in (5, 8, 15): + raise ValueError('Malformed time zone string') + + tz_comps = _parse_hh_mm_ss_ff(tzstr) + if all(x == 0 for x in tz_comps): + tzi = timezone.utc + else: + tzsign = -1 if tstr[tz_pos - 1] == '-' else 1 + + td = timedelta(hours=tz_comps[0], minutes=tz_comps[1], + seconds=tz_comps[2], microseconds=tz_comps[3]) + + tzi = timezone(tzsign * td) + + time_comps.append(tzi) + + return time_comps + + +# Just raise TypeError if the arg isn't None or a string. +def _check_tzname(name): + if name is not None and not isinstance(name, str): + raise TypeError("tzinfo.tzname() must return None or string, " + "not '%s'" % type(name)) + +# name is the offset-producing method, "utcoffset" or "dst". +# offset is what it returned. +# If offset isn't None or timedelta, raises TypeError. +# If offset is None, returns None. +# Else offset is checked for being in range. +# If it is, its integer value is returned. Else ValueError is raised. +def _check_utc_offset(name, offset): + assert name in ("utcoffset", "dst") + if offset is None: + return + if not isinstance(offset, timedelta): + raise TypeError("tzinfo.%s() must return None " + "or timedelta, not '%s'" % (name, type(offset))) + if not -timedelta(1) < offset < timedelta(1): + raise ValueError("%s()=%s, must be strictly between " + "-timedelta(hours=24) and timedelta(hours=24)" % + (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): + year = _check_int_field(year) + month = _check_int_field(month) + day = _check_int_field(day) + if not MINYEAR <= year <= MAXYEAR: + raise ValueError('year must be in %d..%d' % (MINYEAR, MAXYEAR), year) + if not 1 <= month <= 12: + raise ValueError('month must be in 1..12', month) + dim = _days_in_month(year, month) + if not 1 <= day <= dim: + raise ValueError('day must be in 1..%d' % dim, day) + return year, month, day + +def _check_time_fields(hour, minute, second, microsecond, fold): + hour = _check_int_field(hour) + minute = _check_int_field(minute) + second = _check_int_field(second) + microsecond = _check_int_field(microsecond) + if not 0 <= hour <= 23: + raise ValueError('hour must be in 0..23', hour) + if not 0 <= minute <= 59: + raise ValueError('minute must be in 0..59', minute) + if not 0 <= second <= 59: + raise ValueError('second must be in 0..59', second) + if not 0 <= microsecond <= 999999: + raise ValueError('microsecond must be in 0..999999', microsecond) + if fold not in (0, 1): + raise ValueError('fold must be either 0 or 1', fold) + return hour, minute, second, microsecond, fold + +def _check_tzinfo_arg(tz): + if tz is not None and not isinstance(tz, tzinfo): + raise TypeError("tzinfo argument must be None or of a tzinfo subclass") + +def _cmperror(x, y): + raise TypeError("can't compare '%s' to '%s'" % ( + type(x).__name__, type(y).__name__)) + +def _divide_and_round(a, b): + """divide a by b and round result to the nearest integer + + When the ratio is exactly half-way between two integers, + the even integer is returned. + """ + # Based on the reference implementation for divmod_near + # in Objects/longobject.c. + q, r = divmod(a, b) + # round up if either r / b > 0.5, or r / b == 0.5 and q is odd. + # The expression r / b > 0.5 is equivalent to 2 * r > b if b is + # positive, 2 * r < b if b negative. + r *= 2 + greater_than_half = r > b if b > 0 else r < b + if greater_than_half or r == b and q % 2 == 1: + q += 1 + + return q + + +class timedelta: + """Represent the difference between two datetime objects. + + Supported operators: + + - add, subtract timedelta + - unary plus, minus, abs + - compare to timedelta + - multiply, divide by int + + In addition, datetime supports subtraction of two datetime objects + returning a timedelta, and addition or subtraction of a datetime + and a timedelta giving a datetime. + + Representation: (days, seconds, microseconds). Why? Because I + felt like it. + """ + __slots__ = '_days', '_seconds', '_microseconds', '_hashcode' + + def __new__(cls, days=0, seconds=0, microseconds=0, + milliseconds=0, minutes=0, hours=0, weeks=0): + # Doing this efficiently and accurately in C is going to be difficult + # and error-prone, due to ubiquitous overflow possibilities, and that + # C double doesn't have enough bits of precision to represent + # microseconds over 10K years faithfully. The code here tries to make + # explicit where go-fast assumptions can be relied on, in order to + # guide the C implementation; it's way more convoluted than speed- + # ignoring auto-overflow-to-long idiomatic Python could be. + + # XXX Check that all inputs are ints or floats. + + # Final values, all integer. + # s and us fit in 32-bit signed ints; d isn't bounded. + d = s = us = 0 + + # Normalize everything to days, seconds, microseconds. + days += weeks*7 + seconds += minutes*60 + hours*3600 + microseconds += milliseconds*1000 + + # Get rid of all fractions, and normalize s and us. + # Take a deep breath . + if isinstance(days, float): + dayfrac, days = _math.modf(days) + daysecondsfrac, daysecondswhole = _math.modf(dayfrac * (24.*3600.)) + assert daysecondswhole == int(daysecondswhole) # can't overflow + s = int(daysecondswhole) + assert days == int(days) + d = int(days) + else: + daysecondsfrac = 0.0 + d = days + assert isinstance(daysecondsfrac, float) + assert abs(daysecondsfrac) <= 1.0 + assert isinstance(d, int) + assert abs(s) <= 24 * 3600 + # days isn't referenced again before redefinition + + if isinstance(seconds, float): + secondsfrac, seconds = _math.modf(seconds) + assert seconds == int(seconds) + seconds = int(seconds) + secondsfrac += daysecondsfrac + assert abs(secondsfrac) <= 2.0 + else: + secondsfrac = daysecondsfrac + # daysecondsfrac isn't referenced again + assert isinstance(secondsfrac, float) + assert abs(secondsfrac) <= 2.0 + + assert isinstance(seconds, int) + days, seconds = divmod(seconds, 24*3600) + d += days + s += int(seconds) # can't overflow + assert isinstance(s, int) + assert abs(s) <= 2 * 24 * 3600 + # seconds isn't referenced again before redefinition + + usdouble = secondsfrac * 1e6 + assert abs(usdouble) < 2.1e6 # exact value not critical + # secondsfrac isn't referenced again + + if isinstance(microseconds, float): + microseconds = round(microseconds + usdouble) + seconds, microseconds = divmod(microseconds, 1000000) + days, seconds = divmod(seconds, 24*3600) + d += days + s += seconds + else: + microseconds = int(microseconds) + seconds, microseconds = divmod(microseconds, 1000000) + days, seconds = divmod(seconds, 24*3600) + d += days + s += seconds + microseconds = round(microseconds + usdouble) + assert isinstance(s, int) + assert isinstance(microseconds, int) + assert abs(s) <= 3 * 24 * 3600 + assert abs(microseconds) < 3.1e6 + + # Just a little bit of carrying possible for microseconds and seconds. + seconds, us = divmod(microseconds, 1000000) + s += seconds + days, s = divmod(s, 24*3600) + d += days + + assert isinstance(d, int) + assert isinstance(s, int) and 0 <= s < 24*3600 + assert isinstance(us, int) and 0 <= us < 1000000 + + if abs(d) > 999999999: + raise OverflowError("timedelta # of days is too large: %d" % d) + + self = object.__new__(cls) + self._days = d + self._seconds = s + self._microseconds = us + self._hashcode = -1 + return self + + def __repr__(self): + args = [] + if self._days: + args.append("days=%d" % self._days) + if self._seconds: + args.append("seconds=%d" % self._seconds) + if self._microseconds: + args.append("microseconds=%d" % self._microseconds) + if not args: + args.append('0') + return "%s.%s(%s)" % (self.__class__.__module__, + self.__class__.__qualname__, + ', '.join(args)) + + def __str__(self): + mm, ss = divmod(self._seconds, 60) + hh, mm = divmod(mm, 60) + s = "%d:%02d:%02d" % (hh, mm, ss) + if self._days: + def plural(n): + return n, abs(n) != 1 and "s" or "" + s = ("%d day%s, " % plural(self._days)) + s + if self._microseconds: + s = s + ".%06d" % self._microseconds + return s + + def total_seconds(self): + """Total seconds in the duration.""" + return ((self.days * 86400 + self.seconds) * 10**6 + + self.microseconds) / 10**6 + + # Read-only field accessors + @property + def days(self): + """days""" + return self._days + + @property + def seconds(self): + """seconds""" + return self._seconds + + @property + def microseconds(self): + """microseconds""" + return self._microseconds + + def __add__(self, other): + if isinstance(other, timedelta): + # for CPython compatibility, we cannot use + # our __class__ here, but need a real timedelta + return timedelta(self._days + other._days, + self._seconds + other._seconds, + self._microseconds + other._microseconds) + return NotImplemented + + __radd__ = __add__ + + def __sub__(self, other): + if isinstance(other, timedelta): + # for CPython compatibility, we cannot use + # our __class__ here, but need a real timedelta + return timedelta(self._days - other._days, + self._seconds - other._seconds, + self._microseconds - other._microseconds) + return NotImplemented + + def __rsub__(self, other): + if isinstance(other, timedelta): + return -self + other + return NotImplemented + + def __neg__(self): + # for CPython compatibility, we cannot use + # our __class__ here, but need a real timedelta + return timedelta(-self._days, + -self._seconds, + -self._microseconds) + + def __pos__(self): + return self + + def __abs__(self): + if self._days < 0: + return -self + else: + return self + + def __mul__(self, other): + if isinstance(other, int): + # for CPython compatibility, we cannot use + # our __class__ here, but need a real timedelta + return timedelta(self._days * other, + self._seconds * other, + self._microseconds * other) + if isinstance(other, float): + usec = self._to_microseconds() + a, b = other.as_integer_ratio() + return timedelta(0, 0, _divide_and_round(usec * a, b)) + return NotImplemented + + __rmul__ = __mul__ + + def _to_microseconds(self): + return ((self._days * (24*3600) + self._seconds) * 1000000 + + self._microseconds) + + def __floordiv__(self, other): + if not isinstance(other, (int, timedelta)): + return NotImplemented + usec = self._to_microseconds() + if isinstance(other, timedelta): + return usec // other._to_microseconds() + if isinstance(other, int): + return timedelta(0, 0, usec // other) + + def __truediv__(self, other): + if not isinstance(other, (int, float, timedelta)): + return NotImplemented + usec = self._to_microseconds() + if isinstance(other, timedelta): + return usec / other._to_microseconds() + if isinstance(other, int): + return timedelta(0, 0, _divide_and_round(usec, other)) + if isinstance(other, float): + a, b = other.as_integer_ratio() + return timedelta(0, 0, _divide_and_round(b * usec, a)) + + def __mod__(self, other): + if isinstance(other, timedelta): + r = self._to_microseconds() % other._to_microseconds() + return timedelta(0, 0, r) + return NotImplemented + + def __divmod__(self, other): + if isinstance(other, timedelta): + q, r = divmod(self._to_microseconds(), + other._to_microseconds()) + return q, timedelta(0, 0, r) + return NotImplemented + + # Comparisons of timedelta objects with other. + + def __eq__(self, other): + if isinstance(other, timedelta): + return self._cmp(other) == 0 + else: + return False + + def __le__(self, other): + if isinstance(other, timedelta): + return self._cmp(other) <= 0 + else: + _cmperror(self, other) + + def __lt__(self, other): + if isinstance(other, timedelta): + return self._cmp(other) < 0 + else: + _cmperror(self, other) + + def __ge__(self, other): + if isinstance(other, timedelta): + return self._cmp(other) >= 0 + else: + _cmperror(self, other) + + def __gt__(self, other): + if isinstance(other, timedelta): + return self._cmp(other) > 0 + else: + _cmperror(self, other) + + def _cmp(self, other): + assert isinstance(other, timedelta) + return _cmp(self._getstate(), other._getstate()) + + def __hash__(self): + if self._hashcode == -1: + self._hashcode = hash(self._getstate()) + return self._hashcode + + def __bool__(self): + return (self._days != 0 or + self._seconds != 0 or + self._microseconds != 0) + + # Pickle support. + + def _getstate(self): + return (self._days, self._seconds, self._microseconds) + + def __reduce__(self): + return (self.__class__, self._getstate()) + +timedelta.min = timedelta(-999999999) +timedelta.max = timedelta(days=999999999, hours=23, minutes=59, seconds=59, + microseconds=999999) +timedelta.resolution = timedelta(microseconds=1) + +class date: + """Concrete date type. + + Constructors: + + __new__() + fromtimestamp() + today() + fromordinal() + + Operators: + + __repr__, __str__ + __eq__, __le__, __lt__, __ge__, __gt__, __hash__ + __add__, __radd__, __sub__ (add/radd only with timedelta arg) + + Methods: + + timetuple() + toordinal() + weekday() + isoweekday(), isocalendar(), isoformat() + ctime() + strftime() + + Properties (readonly): + year, month, day + """ + __slots__ = '_year', '_month', '_day', '_hashcode' + + def __new__(cls, year, month=None, day=None): + """Constructor. + + Arguments: + + year, month, day (required, base 1) + """ + if (month is None and + isinstance(year, (bytes, str)) and len(year) == 4 and + 1 <= ord(year[2:3]) <= 12): + # Pickle support + if isinstance(year, str): + try: + year = year.encode('latin1') + except UnicodeEncodeError: + # More informative error message. + raise ValueError( + "Failed to encode latin1 string when unpickling " + "a date object. " + "pickle.load(data, encoding='latin1') is assumed.") + self = object.__new__(cls) + self.__setstate(year) + self._hashcode = -1 + return self + year, month, day = _check_date_fields(year, month, day) + self = object.__new__(cls) + self._year = year + self._month = month + self._day = day + self._hashcode = -1 + return self + + # Additional constructors + + @classmethod + def fromtimestamp(cls, t): + "Construct a date from a POSIX timestamp (like time.time())." + y, m, d, hh, mm, ss, weekday, jday, dst = _time.localtime(t) + return cls(y, m, d) + + @classmethod + def today(cls): + "Construct a date from time.time()." + t = _time.time() + return cls.fromtimestamp(t) + + @classmethod + def fromordinal(cls, n): + """Construct a date from a proleptic Gregorian ordinal. + + January 1 of year 1 is day 1. Only the year, month and day are + non-zero in the result. + """ + y, m, d = _ord2ymd(n) + return cls(y, m, d) + + @classmethod + def fromisoformat(cls, date_string): + """Construct a date from the output of date.isoformat().""" + if not isinstance(date_string, str): + raise TypeError('fromisoformat: argument must be str') + + try: + assert len(date_string) == 10 + return cls(*_parse_isoformat_date(date_string)) + except Exception: + raise ValueError(f'Invalid isoformat string: {date_string!r}') + + + # Conversions to string + + def __repr__(self): + """Convert to formal string, for repr(). + + >>> dt = datetime(2010, 1, 1) + >>> repr(dt) + 'datetime.datetime(2010, 1, 1, 0, 0)' + + >>> dt = datetime(2010, 1, 1, tzinfo=timezone.utc) + >>> repr(dt) + 'datetime.datetime(2010, 1, 1, 0, 0, tzinfo=datetime.timezone.utc)' + """ + return "%s.%s(%d, %d, %d)" % (self.__class__.__module__, + self.__class__.__qualname__, + self._year, + self._month, + self._day) + # XXX These shouldn't depend on time.localtime(), because that + # clips the usable dates to [1970 .. 2038). At least ctime() is + # easily done without using strftime() -- that's better too because + # strftime("%c", ...) is locale specific. + + + def ctime(self): + "Return ctime() style string." + weekday = self.toordinal() % 7 or 7 + return "%s %s %2d 00:00:00 %04d" % ( + _DAYNAMES[weekday], + _MONTHNAMES[self._month], + self._day, self._year) + + def strftime(self, fmt): + "Format using strftime()." + return _wrap_strftime(self, fmt, self.timetuple()) + + def __format__(self, fmt): + if not isinstance(fmt, str): + raise TypeError("must be str, not %s" % type(fmt).__name__) + if len(fmt) != 0: + return self.strftime(fmt) + return str(self) + + def isoformat(self): + """Return the date formatted according to ISO. + + This is 'YYYY-MM-DD'. + + References: + - http://www.w3.org/TR/NOTE-datetime + - http://www.cl.cam.ac.uk/~mgk25/iso-time.html + """ + return "%04d-%02d-%02d" % (self._year, self._month, self._day) + + __str__ = isoformat + + # Read-only field accessors + @property + def year(self): + """year (1-9999)""" + return self._year + + @property + def month(self): + """month (1-12)""" + return self._month + + @property + def day(self): + """day (1-31)""" + return self._day + + # Standard conversions, __eq__, __le__, __lt__, __ge__, __gt__, + # __hash__ (and helpers) + + def timetuple(self): + "Return local time tuple compatible with time.localtime()." + return _build_struct_time(self._year, self._month, self._day, + 0, 0, 0, -1) + + def toordinal(self): + """Return proleptic Gregorian ordinal for the year, month and day. + + January 1 of year 1 is day 1. Only the year, month and day values + contribute to the result. + """ + return _ymd2ord(self._year, self._month, self._day) + + def replace(self, year=None, month=None, day=None): + """Return a new date with new values for the specified fields.""" + if year is None: + year = self._year + if month is None: + month = self._month + if day is None: + day = self._day + return type(self)(year, month, day) + + # Comparisons of date objects with other. + + def __eq__(self, other): + if isinstance(other, date): + return self._cmp(other) == 0 + return NotImplemented + + def __le__(self, other): + if isinstance(other, date): + return self._cmp(other) <= 0 + return NotImplemented + + def __lt__(self, other): + if isinstance(other, date): + return self._cmp(other) < 0 + return NotImplemented + + def __ge__(self, other): + if isinstance(other, date): + return self._cmp(other) >= 0 + return NotImplemented + + def __gt__(self, other): + if isinstance(other, date): + return self._cmp(other) > 0 + return NotImplemented + + def _cmp(self, other): + assert isinstance(other, date) + y, m, d = self._year, self._month, self._day + y2, m2, d2 = other._year, other._month, other._day + return _cmp((y, m, d), (y2, m2, d2)) + + def __hash__(self): + "Hash." + if self._hashcode == -1: + self._hashcode = hash(self._getstate()) + return self._hashcode + + # Computations + + def __add__(self, other): + "Add a date to a timedelta." + if isinstance(other, timedelta): + o = self.toordinal() + other.days + if 0 < o <= _MAXORDINAL: + return date.fromordinal(o) + raise OverflowError("result out of range") + return NotImplemented + + __radd__ = __add__ + + def __sub__(self, other): + """Subtract two dates, or a date and a timedelta.""" + if isinstance(other, timedelta): + return self + timedelta(-other.days) + if isinstance(other, date): + days1 = self.toordinal() + days2 = other.toordinal() + return timedelta(days1 - days2) + return NotImplemented + + def weekday(self): + "Return day of the week, where Monday == 0 ... Sunday == 6." + return (self.toordinal() + 6) % 7 + + # Day-of-the-week and week-of-the-year, according to ISO + + def isoweekday(self): + "Return day of the week, where Monday == 1 ... Sunday == 7." + # 1-Jan-0001 is a Monday + return self.toordinal() % 7 or 7 + + def isocalendar(self): + """Return a 3-tuple containing ISO year, week number, and weekday. + + The first ISO week of the year is the (Mon-Sun) week + containing the year's first Thursday; everything else derives + from that. + + The first week is 1; Monday is 1 ... Sunday is 7. + + ISO calendar algorithm taken from + http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm + (used with permission) + """ + year = self._year + week1monday = _isoweek1monday(year) + today = _ymd2ord(self._year, self._month, self._day) + # Internally, week and day have origin 0 + week, day = divmod(today - week1monday, 7) + if week < 0: + year -= 1 + week1monday = _isoweek1monday(year) + week, day = divmod(today - week1monday, 7) + elif week >= 52: + if today >= _isoweek1monday(year+1): + year += 1 + week = 0 + return year, week+1, day+1 + + # Pickle support. + + def _getstate(self): + yhi, ylo = divmod(self._year, 256) + return bytes([yhi, ylo, self._month, self._day]), + + def __setstate(self, string): + yhi, ylo, self._month, self._day = string + self._year = yhi * 256 + ylo + + def __reduce__(self): + return (self.__class__, self._getstate()) + +_date_class = date # so functions w/ args named "date" can get at the class + +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. + + Subclasses must override the name(), utcoffset() and dst() methods. + """ + __slots__ = () + + def tzname(self, dt): + "datetime -> string name of time zone." + raise NotImplementedError("tzinfo subclass must override tzname()") + + def utcoffset(self, dt): + "datetime -> timedelta, positive for east of UTC, negative for west of UTC" + raise NotImplementedError("tzinfo subclass must override utcoffset()") + + def dst(self, dt): + """datetime -> DST offset as timedelta, positive for east of UTC. + + Return 0 if DST not in effect. utcoffset() must include the DST + offset. + """ + raise NotImplementedError("tzinfo subclass must override dst()") + + 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") + + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # See the long comment block at the end of this file for an + # explanation of this algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + if delta: + dt += delta + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + # Pickle support. + + def __reduce__(self): + getinitargs = getattr(self, "__getinitargs__", None) + if getinitargs: + args = getinitargs() + else: + args = () + getstate = getattr(self, "__getstate__", None) + if getstate: + state = getstate() + else: + state = getattr(self, "__dict__", None) or None + if state is None: + return (self.__class__, args) + else: + return (self.__class__, args, state) + +_tzinfo_class = tzinfo + +class time: + """Time with time zone. + + Constructors: + + __new__() + + Operators: + + __repr__, __str__ + __eq__, __le__, __lt__, __ge__, __gt__, __hash__ + + Methods: + + strftime() + isoformat() + utcoffset() + tzname() + dst() + + Properties (readonly): + hour, minute, second, microsecond, tzinfo, fold + """ + __slots__ = '_hour', '_minute', '_second', '_microsecond', '_tzinfo', '_hashcode', '_fold' + + def __new__(cls, hour=0, minute=0, second=0, microsecond=0, tzinfo=None, *, fold=0): + """Constructor. + + Arguments: + + hour, minute (required) + second, microsecond (default to zero) + tzinfo (default to None) + fold (keyword only, default to zero) + """ + if (isinstance(hour, (bytes, str)) and len(hour) == 6 and + ord(hour[0:1])&0x7F < 24): + # Pickle support + if isinstance(hour, str): + try: + hour = hour.encode('latin1') + except UnicodeEncodeError: + # More informative error message. + raise ValueError( + "Failed to encode latin1 string when unpickling " + "a time object. " + "pickle.load(data, encoding='latin1') is assumed.") + self = object.__new__(cls) + self.__setstate(hour, minute or None) + self._hashcode = -1 + return self + hour, minute, second, microsecond, fold = _check_time_fields( + hour, minute, second, microsecond, fold) + _check_tzinfo_arg(tzinfo) + self = object.__new__(cls) + self._hour = hour + self._minute = minute + self._second = second + self._microsecond = microsecond + self._tzinfo = tzinfo + self._hashcode = -1 + self._fold = fold + return self + + # Read-only field accessors + @property + def hour(self): + """hour (0-23)""" + return self._hour + + @property + def minute(self): + """minute (0-59)""" + return self._minute + + @property + def second(self): + """second (0-59)""" + return self._second + + @property + def microsecond(self): + """microsecond (0-999999)""" + return self._microsecond + + @property + def tzinfo(self): + """timezone info object""" + return self._tzinfo + + @property + def fold(self): + return self._fold + + # Standard conversions, __hash__ (and helpers) + + # Comparisons of time objects with other. + + def __eq__(self, other): + if isinstance(other, time): + return self._cmp(other, allow_mixed=True) == 0 + else: + return False + + def __le__(self, other): + if isinstance(other, time): + return self._cmp(other) <= 0 + else: + _cmperror(self, other) + + def __lt__(self, other): + if isinstance(other, time): + return self._cmp(other) < 0 + else: + _cmperror(self, other) + + def __ge__(self, other): + if isinstance(other, time): + return self._cmp(other) >= 0 + else: + _cmperror(self, other) + + def __gt__(self, other): + if isinstance(other, time): + return self._cmp(other) > 0 + else: + _cmperror(self, other) + + def _cmp(self, other, allow_mixed=False): + assert isinstance(other, time) + mytz = self._tzinfo + ottz = other._tzinfo + myoff = otoff = None + + if mytz is ottz: + base_compare = True + else: + myoff = self.utcoffset() + otoff = other.utcoffset() + base_compare = myoff == otoff + + if base_compare: + return _cmp((self._hour, self._minute, self._second, + self._microsecond), + (other._hour, other._minute, other._second, + other._microsecond)) + if myoff is None or otoff is None: + if allow_mixed: + return 2 # arbitrary non-zero value + else: + raise TypeError("cannot compare naive and aware times") + myhhmm = self._hour * 60 + self._minute - myoff//timedelta(minutes=1) + othhmm = other._hour * 60 + other._minute - otoff//timedelta(minutes=1) + return _cmp((myhhmm, self._second, self._microsecond), + (othhmm, other._second, other._microsecond)) + + def __hash__(self): + """Hash.""" + if self._hashcode == -1: + if self.fold: + t = self.replace(fold=0) + else: + t = self + tzoff = t.utcoffset() + if not tzoff: # zero or None + self._hashcode = hash(t._getstate()[0]) + else: + h, m = divmod(timedelta(hours=self.hour, minutes=self.minute) - tzoff, + timedelta(hours=1)) + assert not m % timedelta(minutes=1), "whole minute" + m //= timedelta(minutes=1) + 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 + + def _tzstr(self): + """Return formatted timezone offset (+xx:xx) or an empty string.""" + off = self.utcoffset() + return _format_offset(off) + + def __repr__(self): + """Convert to formal string, for repr().""" + if self._microsecond != 0: + s = ", %d, %d" % (self._second, self._microsecond) + elif self._second != 0: + s = ", %d" % self._second + else: + s = "" + s= "%s.%s(%d, %d%s)" % (self.__class__.__module__, + self.__class__.__qualname__, + self._hour, self._minute, s) + 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'): + """Return the time formatted according to ISO. + + The full format is 'HH:MM:SS.mmmmmm+zz:zz'. By default, the fractional + part is omitted if self.microsecond == 0. + + The optional argument timespec specifies the number of additional + terms of the time to include. + """ + s = _format_time(self._hour, self._minute, self._second, + self._microsecond, timespec) + tz = self._tzstr() + if tz: + s += tz + return s + + __str__ = isoformat + + @classmethod + def fromisoformat(cls, time_string): + """Construct a time from the output of isoformat().""" + if not isinstance(time_string, str): + raise TypeError('fromisoformat: argument must be str') + + try: + return cls(*_parse_isoformat_time(time_string)) + except Exception: + raise ValueError(f'Invalid isoformat string: {time_string!r}') + + + def strftime(self, fmt): + """Format using strftime(). The date part of the timestamp passed + to underlying strftime should not be used. + """ + # The year must be >= 1000 else Python's strftime implementation + # can raise a bogus exception. + timetuple = (1900, 1, 1, + self._hour, self._minute, self._second, + 0, 1, -1) + return _wrap_strftime(self, fmt, timetuple) + + def __format__(self, fmt): + if not isinstance(fmt, str): + raise TypeError("must be str, not %s" % type(fmt).__name__) + if len(fmt) != 0: + return self.strftime(fmt) + return str(self) + + # Timezone functions + + def utcoffset(self): + """Return the timezone offset as timedelta, positive east of UTC + (negative west of UTC).""" + if self._tzinfo is None: + return None + offset = self._tzinfo.utcoffset(None) + _check_utc_offset("utcoffset", offset) + return offset + + def tzname(self): + """Return the timezone name. + + Note that the name is 100% informational -- there's no requirement that + it mean anything in particular. For example, "GMT", "UTC", "-500", + "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies. + """ + if self._tzinfo is None: + return None + name = self._tzinfo.tzname(None) + _check_tzname(name) + return name + + def dst(self): + """Return 0 if DST is not in effect, or the DST offset (as timedelta + positive eastward) if DST is in effect. + + This is purely informational; the DST offset has already been added to + the UTC offset returned by utcoffset() if applicable, so there's no + need to consult dst() unless you're interested in displaying the DST + info. + """ + if self._tzinfo is None: + return None + offset = self._tzinfo.dst(None) + _check_utc_offset("dst", offset) + return offset + + def replace(self, hour=None, minute=None, second=None, microsecond=None, + tzinfo=True, *, fold=None): + """Return a new time with new values for the specified fields.""" + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tzinfo is True: + tzinfo = self.tzinfo + if fold is None: + fold = self._fold + return type(self)(hour, minute, second, microsecond, tzinfo, fold=fold) + + # Pickle support. + + def _getstate(self, protocol=3): + us2, us3 = divmod(self._microsecond, 256) + us1, us2 = divmod(us2, 256) + 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,) + else: + return (basestate, self._tzinfo) + + def __setstate(self, string, tzinfo): + if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class): + raise TypeError("bad tzinfo state arg") + 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_ex__(self, protocol): + return (time, self._getstate(protocol)) + + def __reduce__(self): + return self.__reduce_ex__(2) + +_time_class = time # so functions w/ args named "time" can get at the class + +time.min = time(0, 0, 0) +time.max = time(23, 59, 59, 999999) +time.resolution = timedelta(microseconds=1) + +class datetime(date): + """datetime(year, month, day[, hour[, minute[, second[, microsecond[,tzinfo]]]]]) + + 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. + """ + __slots__ = date.__slots__ + time.__slots__ + + def __new__(cls, year, month=None, day=None, hour=0, minute=0, second=0, + microsecond=0, tzinfo=None, *, fold=0): + if (isinstance(year, (bytes, str)) and len(year) == 10 and + 1 <= ord(year[2:3])&0x7F <= 12): + # Pickle support + if isinstance(year, str): + try: + year = bytes(year, 'latin1') + except UnicodeEncodeError: + # More informative error message. + raise ValueError( + "Failed to encode latin1 string when unpickling " + "a datetime object. " + "pickle.load(data, encoding='latin1') is assumed.") + self = object.__new__(cls) + self.__setstate(year, month) + self._hashcode = -1 + return self + year, month, day = _check_date_fields(year, month, day) + hour, minute, second, microsecond, fold = _check_time_fields( + hour, minute, second, microsecond, fold) + _check_tzinfo_arg(tzinfo) + self = object.__new__(cls) + self._year = year + self._month = month + self._day = day + self._hour = hour + self._minute = minute + self._second = second + self._microsecond = microsecond + self._tzinfo = tzinfo + self._hashcode = -1 + self._fold = fold + return self + + # Read-only field accessors + @property + def hour(self): + """hour (0-23)""" + return self._hour + + @property + def minute(self): + """minute (0-59)""" + return self._minute + + @property + def second(self): + """second (0-59)""" + return self._second + + @property + def microsecond(self): + """microsecond (0-999999)""" + return self._microsecond + + @property + def tzinfo(self): + """timezone info object""" + return self._tzinfo + + @property + def fold(self): + return self._fold + + @classmethod + def _fromtimestamp(cls, t, utc, tz): + """Construct a datetime from a POSIX timestamp (like time.time()). + + A timezone info object may be passed in as well. + """ + frac, t = _math.modf(t) + us = round(frac * 1e6) + if us >= 1000000: + t += 1 + us -= 1000000 + elif us < 0: + t -= 1 + us += 1000000 + + 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 + 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 + + # On Windows localtime_s throws an OSError for negative values, + # thus we can't perform fold detection for values of time less + # than the max time fold. See comments in _datetimemodule's + # version of this method for more details. + if t < max_fold_seconds and sys.platform.startswith("win"): + return result + + 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): + """Construct a datetime from a POSIX timestamp (like time.time()). + + A timezone info object may be passed in as well. + """ + _check_tzinfo_arg(tz) + + return cls._fromtimestamp(t, tz is not None, tz) + + @classmethod + def utcfromtimestamp(cls, t): + """Construct a naive UTC datetime from a POSIX timestamp.""" + return cls._fromtimestamp(t, True, None) + + @classmethod + def now(cls, tz=None): + "Construct a datetime from time.time() and optional time zone info." + t = _time.time() + return cls.fromtimestamp(t, tz) + + @classmethod + def utcnow(cls): + "Construct a UTC datetime from time.time()." + t = _time.time() + return cls.utcfromtimestamp(t) + + @classmethod + def combine(cls, date, time, tzinfo=True): + "Construct a datetime from a given date and a given time." + if not isinstance(date, _date_class): + raise TypeError("date argument must be a date instance") + if not isinstance(time, _time_class): + raise TypeError("time argument must be a time instance") + if tzinfo is True: + tzinfo = time.tzinfo + return cls(date.year, date.month, date.day, + time.hour, time.minute, time.second, time.microsecond, + tzinfo, fold=time.fold) + + @classmethod + def fromisoformat(cls, date_string): + """Construct a datetime from the output of datetime.isoformat().""" + if not isinstance(date_string, str): + raise TypeError('fromisoformat: argument must be str') + + # Split this at the separator + dstr = date_string[0:10] + tstr = date_string[11:] + + try: + date_components = _parse_isoformat_date(dstr) + except ValueError: + raise ValueError(f'Invalid isoformat string: {date_string!r}') + + if tstr: + try: + time_components = _parse_isoformat_time(tstr) + except ValueError: + raise ValueError(f'Invalid isoformat string: {date_string!r}') + else: + time_components = [0, 0, 0, 0, None] + + return cls(*(date_components + time_components)) + + def timetuple(self): + "Return local time tuple compatible with time.localtime()." + dst = self.dst() + if dst is None: + dst = -1 + elif dst: + dst = 1 + else: + dst = 0 + return _build_struct_time(self.year, self.month, self.day, + 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: + s = self._mktime() + return s + self.microsecond / 1e6 + else: + return (self - _EPOCH).total_seconds() + + def utctimetuple(self): + "Return UTC time tuple compatible with time.gmtime()." + offset = self.utcoffset() + if offset: + self -= offset + y, m, d = self.year, self.month, self.day + hh, mm, ss = self.hour, self.minute, self.second + return _build_struct_time(y, m, d, hh, mm, ss, 0) + + def date(self): + "Return the date part." + return date(self._year, self._month, self._day) + + def time(self): + "Return the time part, with tzinfo None." + 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, fold=self.fold) + + def replace(self, year=None, month=None, day=None, hour=None, + 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 + if month is None: + month = self.month + if day is None: + day = self.day + if hour is None: + hour = self.hour + if minute is None: + minute = self.minute + if second is None: + second = self.second + if microsecond is None: + microsecond = self.microsecond + if tzinfo is True: + tzinfo = self.tzinfo + if fold is None: + fold = self.fold + return type(self)(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: + 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: + mytz = self._local_timezone() + myoffset = mytz.utcoffset(self) + else: + myoffset = mytz.utcoffset(self) + if myoffset is None: + mytz = self.replace(tzinfo=None)._local_timezone() + myoffset = mytz.utcoffset(self) + + if tz is mytz: + return self + + # Convert self to UTC, and attach the new time zone object. + utc = (self - myoffset).replace(tzinfo=tz) + + # Convert from UTC to tz's local time. + return tz.fromutc(utc) + + # Ways to produce a string. + + def ctime(self): + "Return ctime() style string." + weekday = self.toordinal() % 7 or 7 + return "%s %s %2d %02d:%02d:%02d %04d" % ( + _DAYNAMES[weekday], + _MONTHNAMES[self._month], + self._day, + self._hour, self._minute, self._second, + self._year) + + def isoformat(self, sep='T', timespec='auto'): + """Return the time formatted according to ISO. + + The full format looks like 'YYYY-MM-DD HH:MM:SS.mmmmmm'. + By default, the fractional part is omitted if self.microsecond == 0. + + If self.tzinfo is not None, the UTC offset is also attached, giving + giving a full format of 'YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM'. + + Optional argument sep specifies the separator between date and + time, default 'T'. + + The optional argument timespec specifies the number of additional + terms of the time to include. + """ + s = ("%04d-%02d-%02d%c" % (self._year, self._month, self._day, sep) + + _format_time(self._hour, self._minute, self._second, + self._microsecond, timespec)) + + off = self.utcoffset() + tz = _format_offset(off) + if tz: + s += tz + + return s + + def __repr__(self): + """Convert to formal string, for repr().""" + L = [self._year, self._month, self._day, # These are never zero + self._hour, self._minute, self._second, self._microsecond] + if L[-1] == 0: + del L[-1] + if L[-1] == 0: + del L[-1] + s = "%s.%s(%s)" % (self.__class__.__module__, + self.__class__.__qualname__, + ", ".join(map(str, L))) + 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): + "Convert to string, for str()." + return self.isoformat(sep=' ') + + @classmethod + def strptime(cls, date_string, format): + 'string, format -> new datetime parsed from a string (like time.strptime()).' + import _strptime + return _strptime._strptime_datetime(cls, date_string, format) + + def utcoffset(self): + """Return the timezone offset as timedelta positive east of UTC (negative west of + UTC).""" + if self._tzinfo is None: + return None + offset = self._tzinfo.utcoffset(self) + _check_utc_offset("utcoffset", offset) + return offset + + def tzname(self): + """Return the timezone name. + + Note that the name is 100% informational -- there's no requirement that + it mean anything in particular. For example, "GMT", "UTC", "-500", + "-5:00", "EDT", "US/Eastern", "America/New York" are all valid replies. + """ + if self._tzinfo is None: + return None + name = self._tzinfo.tzname(self) + _check_tzname(name) + return name + + def dst(self): + """Return 0 if DST is not in effect, or the DST offset (as timedelta + positive eastward) if DST is in effect. + + This is purely informational; the DST offset has already been added to + the UTC offset returned by utcoffset() if applicable, so there's no + need to consult dst() unless you're interested in displaying the DST + info. + """ + if self._tzinfo is None: + return None + offset = self._tzinfo.dst(self) + _check_utc_offset("dst", offset) + return offset + + # Comparisons of datetime objects with other. + + def __eq__(self, other): + if isinstance(other, datetime): + return self._cmp(other, allow_mixed=True) == 0 + elif not isinstance(other, date): + return NotImplemented + else: + return False + + def __le__(self, other): + if isinstance(other, datetime): + return self._cmp(other) <= 0 + elif not isinstance(other, date): + return NotImplemented + else: + _cmperror(self, other) + + def __lt__(self, other): + if isinstance(other, datetime): + return self._cmp(other) < 0 + elif not isinstance(other, date): + return NotImplemented + else: + _cmperror(self, other) + + def __ge__(self, other): + if isinstance(other, datetime): + return self._cmp(other) >= 0 + elif not isinstance(other, date): + return NotImplemented + else: + _cmperror(self, other) + + def __gt__(self, other): + if isinstance(other, datetime): + return self._cmp(other) > 0 + elif not isinstance(other, date): + return NotImplemented + else: + _cmperror(self, other) + + def _cmp(self, other, allow_mixed=False): + assert isinstance(other, datetime) + mytz = self._tzinfo + ottz = other._tzinfo + myoff = otoff = None + + if mytz is ottz: + base_compare = True + 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: + return _cmp((self._year, self._month, self._day, + self._hour, self._minute, self._second, + self._microsecond), + (other._year, other._month, other._day, + other._hour, other._minute, other._second, + other._microsecond)) + if myoff is None or otoff is None: + if allow_mixed: + return 2 # arbitrary non-zero value + else: + raise TypeError("cannot compare naive and aware datetimes") + # XXX What follows could be done more efficiently... + diff = self - other # this will take offsets into account + if diff.days < 0: + return -1 + return diff and 1 or 0 + + def __add__(self, other): + "Add a datetime and a timedelta." + if not isinstance(other, timedelta): + return NotImplemented + delta = timedelta(self.toordinal(), + hours=self._hour, + minutes=self._minute, + seconds=self._second, + microseconds=self._microsecond) + delta += other + hour, rem = divmod(delta.seconds, 3600) + minute, second = divmod(rem, 60) + if 0 < delta.days <= _MAXORDINAL: + return datetime.combine(date.fromordinal(delta.days), + time(hour, minute, second, + delta.microseconds, + tzinfo=self._tzinfo)) + raise OverflowError("result out of range") + + __radd__ = __add__ + + def __sub__(self, other): + "Subtract two datetimes, or a datetime and a timedelta." + if not isinstance(other, datetime): + if isinstance(other, timedelta): + return self + -other + return NotImplemented + + days1 = self.toordinal() + days2 = other.toordinal() + secs1 = self._second + self._minute * 60 + self._hour * 3600 + secs2 = other._second + other._minute * 60 + other._hour * 3600 + base = timedelta(days1 - days2, + secs1 - secs2, + self._microsecond - other._microsecond) + if self._tzinfo is other._tzinfo: + return base + myoff = self.utcoffset() + otoff = other.utcoffset() + if myoff == otoff: + return base + if myoff is None or otoff is None: + raise TypeError("cannot mix naive and timezone-aware time") + return base + otoff - myoff + + def __hash__(self): + if self._hashcode == -1: + if self.fold: + t = self.replace(fold=0) + else: + t = self + tzoff = t.utcoffset() + if tzoff is None: + self._hashcode = hash(t._getstate()[0]) + else: + 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. + + def _getstate(self, protocol=3): + yhi, ylo = divmod(self._year, 256) + us2, us3 = divmod(self._microsecond, 256) + us1, us2 = divmod(us2, 256) + 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: + return (basestate,) + else: + return (basestate, self._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, 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_ex__(self, protocol): + return (self.__class__, self._getstate(protocol)) + + def __reduce__(self): + return self.__reduce_ex__(2) + + +datetime.min = datetime(1, 1, 1) +datetime.max = datetime(9999, 12, 31, 23, 59, 59, 999999) +datetime.resolution = timedelta(microseconds=1) + + +def _isoweek1monday(year): + # Helper to calculate the day number of the Monday starting week 1 + # XXX This could be done more efficiently + THURSDAY = 3 + firstday = _ymd2ord(year, 1, 1) + firstweekday = (firstday + 6) % 7 # See weekday() above + week1monday = firstday - firstweekday + if firstweekday > THURSDAY: + week1monday += 7 + return week1monday + +class timezone(tzinfo): + __slots__ = '_offset', '_name' + + # Sentinel value to disallow None + _Omitted = object() + def __new__(cls, offset, name=_Omitted): + if not isinstance(offset, timedelta): + raise TypeError("offset must be a timedelta") + if name is cls._Omitted: + if not offset: + return cls.utc + name = None + elif not isinstance(name, str): + raise TypeError("name must be a string") + if not cls._minoffset <= offset <= cls._maxoffset: + raise ValueError("offset must be a timedelta " + "strictly between -timedelta(hours=24) and " + "timedelta(hours=24).") + return cls._create(offset, name) + + @classmethod + def _create(cls, offset, name=None): + self = tzinfo.__new__(cls) + self._offset = offset + self._name = name + return self + + def __getinitargs__(self): + """pickle support""" + if self._name is None: + return (self._offset,) + return (self._offset, self._name) + + def __eq__(self, other): + if type(other) != timezone: + return False + return self._offset == other._offset + + def __hash__(self): + return hash(self._offset) + + def __repr__(self): + """Convert to formal string, for repr(). + + >>> tz = timezone.utc + >>> repr(tz) + 'datetime.timezone.utc' + >>> tz = timezone(timedelta(hours=-5), 'EST') + >>> repr(tz) + "datetime.timezone(datetime.timedelta(-1, 68400), 'EST')" + """ + if self is self.utc: + return 'datetime.timezone.utc' + if self._name is None: + return "%s.%s(%r)" % (self.__class__.__module__, + self.__class__.__qualname__, + self._offset) + return "%s.%s(%r, %r)" % (self.__class__.__module__, + self.__class__.__qualname__, + self._offset, self._name) + + def __str__(self): + return self.tzname(None) + + def utcoffset(self, dt): + if isinstance(dt, datetime) or dt is None: + return self._offset + raise TypeError("utcoffset() argument must be a datetime instance" + " or None") + + def tzname(self, dt): + if isinstance(dt, datetime) or dt is None: + if self._name is None: + return self._name_from_offset(self._offset) + return self._name + raise TypeError("tzname() argument must be a datetime instance" + " or None") + + def dst(self, dt): + if isinstance(dt, datetime) or dt is None: + return None + raise TypeError("dst() argument must be a datetime instance" + " or None") + + def fromutc(self, dt): + if isinstance(dt, datetime): + if dt.tzinfo is not self: + raise ValueError("fromutc: dt.tzinfo " + "is not self") + return dt + self._offset + raise TypeError("fromutc() argument must be a datetime instance" + " or None") + + _maxoffset = timedelta(hours=23, minutes=59) + _minoffset = -_maxoffset + + @staticmethod + def _name_from_offset(delta): + if not delta: + return 'UTC' + if delta < timedelta(0): + sign = '-' + delta = -delta + else: + sign = '+' + hours, rest = divmod(delta, timedelta(hours=1)) + minutes, rest = divmod(rest, timedelta(minutes=1)) + seconds = rest.seconds + microseconds = rest.microseconds + if microseconds: + return (f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}' + f'.{microseconds:06d}') + if seconds: + return f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}' + return f'UTC{sign}{hours:02d}:{minutes:02d}' + +timezone.utc = timezone._create(timedelta(0)) +timezone.min = timezone._create(timezone._minoffset) +timezone.max = timezone._create(timezone._maxoffset) +_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) + +# Some time zone algebra. For a datetime x, let +# x.n = x stripped of its timezone -- its naive time. +# x.o = x.utcoffset(), and assuming that doesn't raise an exception or +# return None +# x.d = x.dst(), and assuming that doesn't raise an exception or +# return None +# x.s = x's standard offset, x.o - x.d +# +# Now some derived rules, where k is a duration (timedelta). +# +# 1. x.o = x.s + x.d +# This follows from the definition of x.s. +# +# 2. If x and y have the same tzinfo member, x.s = y.s. +# This is actually a requirement, an assumption we need to make about +# sane tzinfo classes. +# +# 3. The naive UTC time corresponding to x is x.n - x.o. +# This is again a requirement for a sane tzinfo class. +# +# 4. (x+k).s = x.s +# This follows from #2, and that datimetimetz+timedelta preserves tzinfo. +# +# 5. (x+k).n = x.n + k +# Again follows from how arithmetic is defined. +# +# Now we can explain tz.fromutc(x). Let's assume it's an interesting case +# (meaning that the various tzinfo methods exist, and don't blow up or return +# None when called). +# +# The function wants to return a datetime y with timezone tz, equivalent to x. +# x is already in UTC. +# +# By #3, we want +# +# y.n - y.o = x.n [1] +# +# The algorithm starts by attaching tz to x.n, and calling that y. So +# x.n = y.n at the start. Then it wants to add a duration k to y, so that [1] +# becomes true; in effect, we want to solve [2] for k: +# +# (y+k).n - (y+k).o = x.n [2] +# +# By #1, this is the same as +# +# (y+k).n - ((y+k).s + (y+k).d) = x.n [3] +# +# By #5, (y+k).n = y.n + k, which equals x.n + k because x.n=y.n at the start. +# Substituting that into [3], +# +# x.n + k - (y+k).s - (y+k).d = x.n; the x.n terms cancel, leaving +# k - (y+k).s - (y+k).d = 0; rearranging, +# k = (y+k).s - (y+k).d; by #4, (y+k).s == y.s, so +# k = y.s - (y+k).d +# +# On the RHS, (y+k).d can't be computed directly, but y.s can be, and we +# approximate k by ignoring the (y+k).d term at first. Note that k can't be +# very large, since all offset-returning methods return a duration of magnitude +# less than 24 hours. For that reason, if y is firmly in std time, (y+k).d must +# be 0, so ignoring it has no consequence then. +# +# In any case, the new value is +# +# z = y + y.s [4] +# +# It's helpful to step back at look at [4] from a higher level: it's simply +# mapping from UTC to tz's standard time. +# +# At this point, if +# +# z.n - z.o = x.n [5] +# +# we have an equivalent time, and are almost done. The insecurity here is +# at the start of daylight time. Picture US Eastern for concreteness. The wall +# time jumps from 1:59 to 3:00, and wall hours of the form 2:MM don't make good +# sense then. The docs ask that an Eastern tzinfo class consider such a time to +# be EDT (because it's "after 2"), which is a redundant spelling of 1:MM EST +# on the day DST starts. We want to return the 1:MM EST spelling because that's +# the only spelling that makes sense on the local wall clock. +# +# In fact, if [5] holds at this point, we do have the standard-time spelling, +# but that takes a bit of proof. We first prove a stronger result. What's the +# difference between the LHS and RHS of [5]? Let +# +# diff = x.n - (z.n - z.o) [6] +# +# Now +# z.n = by [4] +# (y + y.s).n = by #5 +# y.n + y.s = since y.n = x.n +# x.n + y.s = since z and y are have the same tzinfo member, +# y.s = z.s by #2 +# x.n + z.s +# +# Plugging that back into [6] gives +# +# diff = +# x.n - ((x.n + z.s) - z.o) = expanding +# x.n - x.n - z.s + z.o = cancelling +# - z.s + z.o = by #2 +# z.d +# +# So diff = z.d. +# +# If [5] is true now, diff = 0, so z.d = 0 too, and we have the standard-time +# spelling we wanted in the endcase described above. We're done. Contrarily, +# if z.d = 0, then we have a UTC equivalent, and are also done. +# +# If [5] is not true now, diff = z.d != 0, and z.d is the offset we need to +# add to z (in effect, z is in tz's standard time, and we need to shift the +# local clock into tz's daylight time). +# +# Let +# +# z' = z + z.d = z + diff [7] +# +# and we can again ask whether +# +# z'.n - z'.o = x.n [8] +# +# If so, we're done. If not, the tzinfo class is insane, according to the +# assumptions we've made. This also requires a bit of proof. As before, let's +# compute the difference between the LHS and RHS of [8] (and skipping some of +# the justifications for the kinds of substitutions we've done several times +# already): +# +# diff' = x.n - (z'.n - z'.o) = replacing z'.n via [7] +# x.n - (z.n + diff - z'.o) = replacing diff via [6] +# x.n - (z.n + x.n - (z.n - z.o) - z'.o) = +# x.n - z.n - x.n + z.n - z.o + z'.o = cancel x.n +# - z.n + z.n - z.o + z'.o = cancel z.n +# - z.o + z'.o = #1 twice +# -z.s - z.d + z'.s + z'.d = z and z' have same tzinfo +# z'.d - z.d +# +# So z' is UTC-equivalent to x iff z'.d = z.d at this point. If they are equal, +# we've found the UTC-equivalent so are done. In fact, we stop with [7] and +# return z', not bothering to compute z'.d. +# +# How could z.d and z'd differ? z' = z + z.d [7], so merely moving z' by +# a dst() offset, and starting *from* a time already in DST (we know z.d != 0), +# would have to change the result dst() returns: we start in DST, and moving +# a little further into it takes us out of DST. +# +# There isn't a sane case where this can happen. The closest it gets is at +# the end of DST, where there's an hour in UTC with no spelling in a hybrid +# tzinfo class. In US Eastern, that's 5:MM UTC = 0:MM EST = 1:MM EDT. During +# that hour, on an Eastern clock 1:MM is taken as being in standard time (6:MM +# UTC) because the docs insist on that, but 0:MM is taken as being in daylight +# time (4:MM UTC). There is no local time mapping to 5:MM UTC. The local +# clock jumps from 1:59 back to 1:00 again, and repeats the 1:MM hour in +# standard time. Since that's what the local clock *does*, we want to map both +# UTC hours 5:MM and 6:MM to 1:MM Eastern. The result is ambiguous +# in local time, but so it goes -- it's the way the local clock works. +# +# When x = 5:MM UTC is the input to this algorithm, x.o=0, y.o=-5 and y.d=0, +# so z=0:MM. z.d=60 (minutes) then, so [5] doesn't hold and we keep going. +# z' = z + z.d = 1:MM then, and z'.d=0, and z'.d - z.d = -60 != 0 so [8] +# (correctly) concludes that z' is not UTC-equivalent to x. +# +# Because we know z.d said z was in daylight time (else [5] would have held and +# we would have stopped then), and we know z.d != z'.d (else [8] would have held +# and we have stopped then), and there are only 2 possible values dst() can +# return in Eastern, it follows that z'.d must be 0 (which it is in the example, +# but the reasoning doesn't depend on the example -- it depends on there being +# two possible dst() outcomes, one zero and the other non-zero). Therefore +# z' must be in standard time, and is the spelling we want in this case. +# +# Note again that z' is not UTC-equivalent as far as the hybrid tzinfo class is +# concerned (because it takes z' as being in standard time rather than the +# daylight time we intend here), but returning it gives the real-life "local +# clock repeats an hour" behavior when mapping the "unspellable" UTC hour into +# tz. +# +# When the input is 6:MM, z=1:MM and z.d=0, and we stop at once, again with +# the 1:MM standard time spelling we want. +# +# So how can this break? One of the assumptions must be violated. Two +# possibilities: +# +# 1) [2] effectively says that y.s is invariant across all y belong to a given +# time zone. This isn't true if, for political reasons or continental drift, +# a region decides to change its base offset from UTC. +# +# 2) There may be versions of "double daylight" time where the tail end of +# the analysis gives up a step too early. I haven't thought about that +# enough to say. +# +# In any case, it's clear that the default fromutc() is strong enough to handle +# "almost all" time zones: so long as the standard offset is invariant, it +# doesn't matter if daylight time transition points change from year to year, or +# if daylight time is skipped in some years; it doesn't matter how large or +# small dst() may get within its bounds; and it doesn't even matter if some +# perverse time zone returns a negative dst()). So a breaking case must be +# pretty bizarre, and a tzinfo subclass can override fromutc() if it is. + +try: + from _datetime import * +except ImportError: + pass +else: + # Clean up unused names + del (_DAYNAMES, _DAYS_BEFORE_MONTH, _DAYS_IN_MONTH, _DI100Y, _DI400Y, + _DI4Y, _EPOCH, _MAXORDINAL, _MONTHNAMES, _build_struct_time, + _check_date_fields, _check_int_field, _check_time_fields, + _check_tzinfo_arg, _check_tzname, _check_utc_offset, _cmp, _cmperror, + _date_class, _days_before_month, _days_before_year, _days_in_month, + _format_time, _format_offset, _is_leap, _isoweek1monday, _math, + _ord2ymd, _time, _time_class, _tzinfo_class, _wrap_strftime, _ymd2ord, + _divide_and_round, _parse_isoformat_date, _parse_isoformat_time, + _parse_hh_mm_ss_ff) + # XXX Since import * above excludes names that start with _, + # docstring does not get overwritten. In the future, it may be + # appropriate to maintain a single module level docstring and + # remove the following line. + from _datetime import __doc__ diff --git a/bytecode/src/bytecode.rs b/bytecode/src/bytecode.rs index 3802429b8b..990a9e1bf7 100644 --- a/bytecode/src/bytecode.rs +++ b/bytecode/src/bytecode.rs @@ -59,10 +59,19 @@ bitflags! { pub type Label = usize; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +/// An indication where the name must be accessed. pub enum NameScope { + /// The name will be in the local scope. Local, + + /// The name will be located in scope surrounding the current scope. NonLocal, + + /// The name will be in global scope. Global, + + /// The name will be located in any scope between the current scope and the top scope. + Free, } /// Transforms a value prior to formatting it. diff --git a/compiler/src/compile.rs b/compiler/src/compile.rs index 23b46f1b92..2a95190d67 100644 --- a/compiler/src/compile.rs +++ b/compiler/src/compile.rs @@ -282,8 +282,8 @@ impl Compiler { match symbol.scope { SymbolScope::Global => bytecode::NameScope::Global, SymbolScope::Nonlocal => bytecode::NameScope::NonLocal, - SymbolScope::Unknown => bytecode::NameScope::Local, - SymbolScope::Local => bytecode::NameScope::Local, + SymbolScope::Unknown => bytecode::NameScope::Free, + SymbolScope::Local => bytecode::NameScope::Free, } } @@ -500,7 +500,7 @@ impl Compiler { self.compile_jump_if(test, true, end_label)?; self.emit(Instruction::LoadName { name: String::from("AssertionError"), - scope: bytecode::NameScope::Local, + scope: bytecode::NameScope::Global, }); match msg { Some(e) => { @@ -736,7 +736,7 @@ impl Compiler { // Check exception type: self.emit(Instruction::LoadName { name: String::from("isinstance"), - scope: bytecode::NameScope::Local, + scope: bytecode::NameScope::Global, }); self.emit(Instruction::Rotate { amount: 2 }); self.compile_expression(exc_type)?; @@ -931,11 +931,11 @@ impl Compiler { self.emit(Instruction::LoadName { name: "__name__".to_string(), - scope: bytecode::NameScope::Local, + scope: bytecode::NameScope::Free, }); self.emit(Instruction::StoreName { name: "__module__".to_string(), - scope: bytecode::NameScope::Local, + scope: bytecode::NameScope::Free, }); self.compile_statements(new_body)?; self.emit(Instruction::LoadConst { diff --git a/compiler/src/symboltable.rs b/compiler/src/symboltable.rs index 42dadee340..d99ae82184 100644 --- a/compiler/src/symboltable.rs +++ b/compiler/src/symboltable.rs @@ -31,6 +31,9 @@ pub fn statements_to_symbol_table( /// Captures all symbols in the current scope, and has a list of subscopes in this scope. #[derive(Clone, Default)] pub struct SymbolTable { + /// The name of this symbol table. Often the name of the class or function. + pub name: String, + /// A set of symbols present on this scope level. pub symbols: IndexMap, @@ -40,8 +43,9 @@ pub struct SymbolTable { } impl SymbolTable { - fn new() -> Self { + fn new(name: String) -> Self { SymbolTable { + name, symbols: Default::default(), sub_tables: vec![], } @@ -209,20 +213,22 @@ impl SymbolTableAnalyzer { if symbol.is_assigned || symbol.is_parameter { symbol.scope = SymbolScope::Local; } else { - // TODO: comment this out and make it work properly: - /* - let found_in_outer_scope = self - .tables - .iter() - .any(|t| t.symbols.contains_key(&symbol.name)); + // Interesting stuff about the __class__ variable: + // https://docs.python.org/3/reference/datamodel.html?highlight=__class__#creating-the-class-object + let found_in_outer_scope = (symbol.name == "__class__") + || self + .tables + .iter() + .skip(1) + .any(|t| t.symbols.contains_key(&symbol.name)); + if found_in_outer_scope { // Symbol is in some outer scope. - + symbol.is_free = true; } else { // Well, it must be a global then :) - // symbol.scope = SymbolScope::Global; + symbol.scope = SymbolScope::Global; } - */ } } } @@ -257,8 +263,7 @@ enum ExpressionContext { impl SymbolTableBuilder { fn prepare(&mut self) { - let table = SymbolTable::new(); - self.tables.push(table); + self.enter_block("top") } fn finish(&mut self) -> Result { @@ -268,9 +273,9 @@ impl SymbolTableBuilder { Ok(symbol_table) } - fn enter_block(&mut self) { + fn enter_block(&mut self, name: &str) { // let parent = Some(self.tables.last().unwrap().clone()); - let table = SymbolTable::new(); + let table = SymbolTable::new(name.to_string()); self.tables.push(table); } @@ -343,7 +348,7 @@ impl SymbolTableBuilder { if let Some(expression) = returns { self.scan_expression(expression, &ExpressionContext::Load)?; } - self.enter_function(args)?; + self.enter_function(name, args)?; self.scan_statements(body)?; self.leave_block(); } @@ -355,7 +360,7 @@ impl SymbolTableBuilder { decorator_list, } => { self.register_name(name, SymbolUsage::Assigned)?; - self.enter_block(); + self.enter_block(name); self.scan_statements(body)?; self.leave_block(); self.scan_expressions(bases, &ExpressionContext::Load)?; @@ -607,7 +612,7 @@ impl SymbolTableBuilder { } } Lambda { args, body } => { - self.enter_function(args)?; + self.enter_function("lambda", args)?; self.scan_expression(body, &ExpressionContext::Load)?; self.leave_block(); } @@ -620,7 +625,7 @@ impl SymbolTableBuilder { Ok(()) } - fn enter_function(&mut self, args: &ast::Parameters) -> SymbolTableResult { + fn enter_function(&mut self, name: &str, args: &ast::Parameters) -> SymbolTableResult { // Evaluate eventual default parameters: self.scan_expressions(&args.defaults, &ExpressionContext::Load)?; for kw_default in &args.kw_defaults { @@ -639,7 +644,7 @@ impl SymbolTableBuilder { self.scan_parameter_annotation(name)?; } - self.enter_block(); + self.enter_block(name); // Fill scope with parameter names: self.scan_parameters(&args.args)?; diff --git a/crawl_sourcecode.py b/crawl_sourcecode.py index 33708edbf5..7e1290220d 100644 --- a/crawl_sourcecode.py +++ b/crawl_sourcecode.py @@ -73,4 +73,4 @@ print() print('======== dis.dis ========') print() co = compile(source, filename, 'exec') -print(dis.dis(co)) +dis.dis(co) diff --git a/tests/snippets/arraymodule.py b/tests/snippets/arraymodule.py new file mode 100644 index 0000000000..fb3a87ee13 --- /dev/null +++ b/tests/snippets/arraymodule.py @@ -0,0 +1,15 @@ +from array import array + +a1 = array("b", [0, 1, 2, 3]) + +assert a1.tobytes() == b"\x00\x01\x02\x03" +assert a1[2] == 2 + +assert list(a1) == [0, 1, 2, 3] + +a1.reverse() +assert a1 == array("B", [3, 2, 1, 0]) + +a1.extend([4, 5, 6, 7]) + +assert a1 == array("h", [3, 2, 1, 0, 4, 5, 6, 7]) diff --git a/tests/snippets/ints.py b/tests/snippets/ints.py index 0e89d44f32..4bb1a51272 100644 --- a/tests/snippets/ints.py +++ b/tests/snippets/ints.py @@ -97,14 +97,57 @@ assert 10 // -4 == -3 assert -10 // -4 == 2 assert int() == 0 +assert int(1) == 1 assert int("101", 2) == 5 assert int("101", base=2) == 5 -assert int(1) == 1 + +# implied base +assert int('1', base=0) == 1 +assert int('123', base=0) == 123 +assert int('0b101', base=0) == 5 +assert int('0B101', base=0) == 5 +assert int('0o100', base=0) == 64 +assert int('0O100', base=0) == 64 +assert int('0xFF', base=0) == 255 +assert int('0XFF', base=0) == 255 +with assertRaises(ValueError): + int('0xFF', base=10) +with assertRaises(ValueError): + int('0oFF', base=10) +with assertRaises(ValueError): + int('0bFF', base=10) +with assertRaises(ValueError): + int('0bFF', base=10) +with assertRaises(ValueError): + int(b"F\xc3\xb8\xc3\xb6\xbbB\xc3\xa5r") +with assertRaises(ValueError): + int(b"F\xc3\xb8\xc3\xb6\xbbB\xc3\xa5r") + +# underscore +assert int('0xFF_FF_FF', base=16) == 16_777_215 +with assertRaises(ValueError): + int("_123_") +with assertRaises(ValueError): + int("123_") +with assertRaises(ValueError): + int("_123") +with assertRaises(ValueError): + int("1__23") + +# signed +assert int('-123') == -123 +assert int('+0b101', base=2) == +5 + +# trailing spaces assert int(' 1') == 1 assert int('1 ') == 1 assert int(' 1 ') == 1 assert int('10', base=0) == 10 +# type byte, signed, implied base +assert int(b' -0XFF ', base=0) == -255 + + assert int.from_bytes(b'\x00\x10', 'big') == 16 assert int.from_bytes(b'\x00\x10', 'little') == 4096 assert int.from_bytes(b'\xfc\x00', 'big', signed=True) == -1024 @@ -179,4 +222,4 @@ assert (1).__round__(0) == 1 assert_raises(TypeError, lambda: (0).__round__(None)) assert_raises(TypeError, lambda: (1).__round__(None)) assert_raises(TypeError, lambda: (0).__round__(0.0)) -assert_raises(TypeError, lambda: (1).__round__(0.0)) \ No newline at end of file +assert_raises(TypeError, lambda: (1).__round__(0.0)) diff --git a/tests/snippets/stdlib_datetime.py b/tests/snippets/stdlib_datetime.py new file mode 100644 index 0000000000..30862154c5 --- /dev/null +++ b/tests/snippets/stdlib_datetime.py @@ -0,0 +1,3812 @@ +"""Test date/time type. + +See http://www.zope.org/Members/fdrake/DateTimeWiki/TestCases +""" + +# import copy +import sys +import random + +from operator import lt, le, gt, ge, eq, ne, truediv, floordiv, mod + +import datetime as datetime_module +from datetime import MINYEAR, MAXYEAR +from datetime import timedelta +from datetime import tzinfo +from datetime import time +from datetime import timezone +from datetime import date, datetime +import time as _time + +from testutils import assertRaises + +def _assert_print(f, args): + raised = True + try: + f() + raised = False + finally: + if raised: + print('Assertion Failure:', *args) + +def _typed(obj): + return '{}({})'.format(type(obj), obj) + + +def assert_equal(a, b): + _assert_print(lambda: a == b, [_typed(a), '==', _typed(b)]) + + +def assert_true(e): + _assert_print(lambda: e is True, [_typed(e), 'is True']) + + +def assert_false(e): + _assert_print(lambda: e is False, [_typed(e), 'is False']) + + +def assert_isinstance(obj, klass): + _assert_print(lambda: isinstance(obj, klass), ['isisntance(', _typed(obj), ',', klass, ')']) + +def assert_in(a, b): + _assert_print(lambda: a in b, [a, 'in', b]) + + +# An arbitrary collection of objects of non-datetime types, for testing +# mixed-type comparisons. +OTHERSTUFF = (10, 34.5, "abc", {}, [], ()) + + +# XXX Copied from test_float. +INF = float("inf") +NAN = float("nan") + + +############################################################################# +# module tests + +# class TestModule(unittest.TestCase): +# def test_constants(self): +assert_equal(datetime_module.MINYEAR, 1) +assert_equal(datetime_module.MAXYEAR, 9999) + +if hasattr(datetime_module, '_divide_and_round'): + # def test_divide_and_round(self): + dar = datetime_module._divide_and_round + + assert_equal(dar(-10, -3), 3) + assert_equal(dar(5, -2), -2) + + # four cases: (2 signs of a) x (2 signs of b) + assert_equal(dar(7, 3), 2) + assert_equal(dar(-7, 3), -2) + assert_equal(dar(7, -3), -2) + assert_equal(dar(-7, -3), 2) + + # ties to even - eight cases: + # (2 signs of a) x (2 signs of b) x (even / odd quotient) + assert_equal(dar(10, 4), 2) + assert_equal(dar(-10, 4), -2) + assert_equal(dar(10, -4), -2) + assert_equal(dar(-10, -4), 2) + + assert_equal(dar(6, 4), 2) + assert_equal(dar(-6, 4), -2) + assert_equal(dar(6, -4), -2) + assert_equal(dar(-6, -4), 2) + +############################################################################# +# tzinfo tests + +class FixedOffset(tzinfo): + + def __init__(self, offset, name, dstoffset=42): + if isinstance(offset, int): + offset = timedelta(minutes=offset) + if isinstance(dstoffset, int): + dstoffset = timedelta(minutes=dstoffset) + self.__offset = offset + self.__name = name + self.__dstoffset = dstoffset + def __repr__(self): + return self.__name.lower() + def utcoffset(self, dt): + return self.__offset + def tzname(self, dt): + return self.__name + def dst(self, dt): + return self.__dstoffset + +class PicklableFixedOffset(FixedOffset): + + def __init__(self, offset=None, name=None, dstoffset=None): + FixedOffset.__init__(self, offset, name, dstoffset) + +class _TZInfo(tzinfo): + def utcoffset(self, datetime_module): + return random.random() + +# class TestTZInfo(unittest.TestCase): + +# def test_refcnt_crash_bug_22044(self): +tz1 = _TZInfo() +dt1 = datetime(2014, 7, 21, 11, 32, 3, 0, tz1) +with assertRaises(TypeError): + dt1.utcoffset() + +# def test_non_abstractness(self): +# In order to allow subclasses to get pickled, the C implementation +# wasn't able to get away with having __init__ raise +# NotImplementedError. +useless = tzinfo() +dt = datetime.max +with assertRaises(NotImplementedError): + useless.tzname(dt) +with assertRaises(NotImplementedError): + useless.utcoffset(dt) +with assertRaises(NotImplementedError): + useless.dst(dt) + +# def test_subclass_must_override(self): +class NotEnough(tzinfo): + def __init__(self, offset, name): + self.__offset = offset + self.__name = name +assert_true(issubclass(NotEnough, tzinfo)) +ne = NotEnough(3, "NotByALongShot") +assert_isinstance(ne, tzinfo) + +dt = datetime.now() +assertRaises(NotImplementedError, ne.tzname, dt) +assertRaises(NotImplementedError, ne.utcoffset, dt) +assertRaises(NotImplementedError, ne.dst, dt) + +# XXX: bug #1302 +# def test_normal(self): +#fo = FixedOffset(3, "Three") +#assert_isinstance(fo, tzinfo) +#for dt in datetime.now(), None: +# assert_equal(fo.utcoffset(dt), timedelta(minutes=3)) +# assert_equal(fo.tzname(dt), "Three") +# assert_equal(fo.dst(dt), timedelta(minutes=42)) + +''' +class TestTimeZone(unittest.TestCase): + + def setUp(self): + self.ACDT = timezone(timedelta(hours=9.5), 'ACDT') + self.EST = timezone(-timedelta(hours=5), 'EST') + self.DT = datetime(2010, 1, 1) + + def test_str(self): + for tz in [self.ACDT, self.EST, timezone.utc, + timezone.min, timezone.max]: + assert_equal(str(tz), tz.tzname(None)) + + def test_repr(self): + datetime = datetime_module + for tz in [self.ACDT, self.EST, timezone.utc, + timezone.min, timezone.max]: + # test round-trip + tzrep = repr(tz) + assert_equal(tz, eval(tzrep)) + + def test_class_members(self): + limit = timedelta(hours=23, minutes=59) + assert_equal(timezone.utc.utcoffset(None), ZERO) + assert_equal(timezone.min.utcoffset(None), -limit) + assert_equal(timezone.max.utcoffset(None), limit) + + + def test_constructor(self): + self.assertIs(timezone.utc, timezone(timedelta(0))) + self.assertIsNot(timezone.utc, timezone(timedelta(0), 'UTC')) + assert_equal(timezone.utc, timezone(timedelta(0), 'UTC')) + # invalid offsets + for invalid in [timedelta(microseconds=1), timedelta(1, 1), + timedelta(seconds=1), timedelta(1), -timedelta(1)]: + assertRaises(ValueError, timezone, invalid) + assertRaises(ValueError, timezone, -invalid) + + with assertRaises(TypeError): timezone(None) + with assertRaises(TypeError): timezone(42) + with assertRaises(TypeError): timezone(ZERO, None) + with assertRaises(TypeError): timezone(ZERO, 42) + with assertRaises(TypeError): timezone(ZERO, 'ABC', 'extra') + + def test_inheritance(self): + assert_isinstance(timezone.utc, tzinfo) + assert_isinstance(self.EST, tzinfo) + + def test_utcoffset(self): + dummy = self.DT + for h in [0, 1.5, 12]: + offset = h * HOUR + assert_equal(offset, timezone(offset).utcoffset(dummy)) + assert_equal(-offset, timezone(-offset).utcoffset(dummy)) + + with assertRaises(TypeError): self.EST.utcoffset('') + with assertRaises(TypeError): self.EST.utcoffset(5) + + + def test_dst(self): + self.assertIsNone(timezone.utc.dst(self.DT)) + + with assertRaises(TypeError): self.EST.dst('') + with assertRaises(TypeError): self.EST.dst(5) + + def test_tzname(self): + assert_equal('UTC+00:00', timezone(ZERO).tzname(None)) + assert_equal('UTC-05:00', timezone(-5 * HOUR).tzname(None)) + assert_equal('UTC+09:30', timezone(9.5 * HOUR).tzname(None)) + assert_equal('UTC-00:01', timezone(timedelta(minutes=-1)).tzname(None)) + assert_equal('XYZ', timezone(-5 * HOUR, 'XYZ').tzname(None)) + + with assertRaises(TypeError): self.EST.tzname('') + with assertRaises(TypeError): self.EST.tzname(5) + + def test_fromutc(self): + with assertRaises(ValueError): + timezone.utc.fromutc(self.DT) + with assertRaises(TypeError): + timezone.utc.fromutc('not datetime') + for tz in [self.EST, self.ACDT, Eastern]: + utctime = self.DT.replace(tzinfo=tz) + local = tz.fromutc(utctime) + assert_equal(local - utctime, tz.utcoffset(local)) + assert_equal(local, + self.DT.replace(tzinfo=timezone.utc)) + + def test_comparison(self): + self.assertNotEqual(timezone(ZERO), timezone(HOUR)) + assert_equal(timezone(HOUR), timezone(HOUR)) + assert_equal(timezone(-5 * HOUR), timezone(-5 * HOUR, 'EST')) + with assertRaises(TypeError): timezone(ZERO) < timezone(ZERO) + assert_in(timezone(ZERO), {timezone(ZERO)}) + assert_true(timezone(ZERO) != None) + assert_false(timezone(ZERO) == None) + + def test_aware_datetime(self): + # test that timezone instances can be used by datetime + t = datetime(1, 1, 1) + for tz in [timezone.min, timezone.max, timezone.utc]: + assert_equal(tz.tzname(t), + t.replace(tzinfo=tz).tzname()) + assert_equal(tz.utcoffset(t), + t.replace(tzinfo=tz).utcoffset()) + assert_equal(tz.dst(t), + t.replace(tzinfo=tz).dst()) + + def test_pickle(self): + for tz in self.ACDT, self.EST, timezone.min, timezone.max: + for pickler, unpickler, proto in pickle_choices: + tz_copy = unpickler.loads(pickler.dumps(tz, proto)) + assert_equal(tz_copy, tz) + tz = timezone.utc + for pickler, unpickler, proto in pickle_choices: + tz_copy = unpickler.loads(pickler.dumps(tz, proto)) + self.assertIs(tz_copy, tz) + + def test_copy(self): + for tz in self.ACDT, self.EST, timezone.min, timezone.max: + tz_copy = copy.copy(tz) + assert_equal(tz_copy, tz) + tz = timezone.utc + tz_copy = copy.copy(tz) + self.assertIs(tz_copy, tz) + + def test_deepcopy(self): + for tz in self.ACDT, self.EST, timezone.min, timezone.max: + tz_copy = copy.deepcopy(tz) + assert_equal(tz_copy, tz) + tz = timezone.utc + tz_copy = copy.deepcopy(tz) + self.assertIs(tz_copy, tz) +''' + +############################################################################# +# Base class for testing a particular aspect of timedelta, time, date and +# datetime comparisons. + +# class HarmlessMixedComparison: + # Test that __eq__ and __ne__ don't complain for mixed-type comparisons. + + # Subclasses must define 'theclass', and theclass(1, 1, 1) must be a + # legit constructor. + +for theclass in timedelta, date, time: + # def test_harmless_mixed_comparison(self): + me = theclass(1, 1, 1) + + assert_false(me == ()) + assert_true(me != ()) + assert_false(() == me) + assert_true(() != me) + + assert_in(me, [1, 20, [], me]) + assert_in([], [me, 1, 20, []]) + + # def test_harmful_mixed_comparison(self): + me = theclass(1, 1, 1) + + assertRaises(TypeError, lambda: me < ()) + assertRaises(TypeError, lambda: me <= ()) + assertRaises(TypeError, lambda: me > ()) + assertRaises(TypeError, lambda: me >= ()) + + assertRaises(TypeError, lambda: () < me) + assertRaises(TypeError, lambda: () <= me) + assertRaises(TypeError, lambda: () > me) + assertRaises(TypeError, lambda: () >= me) + +''' +############################################################################# +# timedelta tests +class TestTimeDelta(HarmlessMixedComparison, unittest.TestCase): + + theclass = timedelta + + def test_constructor(self): + eq = assert_equal + td = timedelta + + # Check keyword args to constructor + eq(td(), td(weeks=0, days=0, hours=0, minutes=0, seconds=0, + milliseconds=0, microseconds=0)) + eq(td(1), td(days=1)) + eq(td(0, 1), td(seconds=1)) + eq(td(0, 0, 1), td(microseconds=1)) + eq(td(weeks=1), td(days=7)) + eq(td(days=1), td(hours=24)) + eq(td(hours=1), td(minutes=60)) + eq(td(minutes=1), td(seconds=60)) + eq(td(seconds=1), td(milliseconds=1000)) + eq(td(milliseconds=1), td(microseconds=1000)) + + # Check float args to constructor + eq(td(weeks=1.0/7), td(days=1)) + eq(td(days=1.0/24), td(hours=1)) + eq(td(hours=1.0/60), td(minutes=1)) + eq(td(minutes=1.0/60), td(seconds=1)) + eq(td(seconds=0.001), td(milliseconds=1)) + eq(td(milliseconds=0.001), td(microseconds=1)) + + def test_computations(self): + eq = assert_equal + td = timedelta + + a = td(7) # One week + b = td(0, 60) # One minute + c = td(0, 0, 1000) # One millisecond + eq(a+b+c, td(7, 60, 1000)) + eq(a-b, td(6, 24*3600 - 60)) + eq(b.__rsub__(a), td(6, 24*3600 - 60)) + eq(-a, td(-7)) + eq(+a, td(7)) + eq(-b, td(-1, 24*3600 - 60)) + eq(-c, td(-1, 24*3600 - 1, 999000)) + eq(abs(a), a) + eq(abs(-a), a) + eq(td(6, 24*3600), a) + eq(td(0, 0, 60*1000000), b) + eq(a*10, td(70)) + eq(a*10, 10*a) + eq(a*10, 10*a) + eq(b*10, td(0, 600)) + eq(10*b, td(0, 600)) + eq(b*10, td(0, 600)) + eq(c*10, td(0, 0, 10000)) + eq(10*c, td(0, 0, 10000)) + eq(c*10, td(0, 0, 10000)) + eq(a*-1, -a) + eq(b*-2, -b-b) + eq(c*-2, -c+-c) + eq(b*(60*24), (b*60)*24) + eq(b*(60*24), (60*b)*24) + eq(c*1000, td(0, 1)) + eq(1000*c, td(0, 1)) + eq(a//7, td(1)) + eq(b//10, td(0, 6)) + eq(c//1000, td(0, 0, 1)) + eq(a//10, td(0, 7*24*360)) + eq(a//3600000, td(0, 0, 7*24*1000)) + eq(a/0.5, td(14)) + eq(b/0.5, td(0, 120)) + eq(a/7, td(1)) + eq(b/10, td(0, 6)) + eq(c/1000, td(0, 0, 1)) + eq(a/10, td(0, 7*24*360)) + eq(a/3600000, td(0, 0, 7*24*1000)) + + # Multiplication by float + us = td(microseconds=1) + eq((3*us) * 0.5, 2*us) + eq((5*us) * 0.5, 2*us) + eq(0.5 * (3*us), 2*us) + eq(0.5 * (5*us), 2*us) + eq((-3*us) * 0.5, -2*us) + eq((-5*us) * 0.5, -2*us) + + # Issue #23521 + eq(td(seconds=1) * 0.123456, td(microseconds=123456)) + eq(td(seconds=1) * 0.6112295, td(microseconds=611229)) + + # Division by int and float + eq((3*us) / 2, 2*us) + eq((5*us) / 2, 2*us) + eq((-3*us) / 2.0, -2*us) + eq((-5*us) / 2.0, -2*us) + eq((3*us) / -2, -2*us) + eq((5*us) / -2, -2*us) + eq((3*us) / -2.0, -2*us) + eq((5*us) / -2.0, -2*us) + for i in range(-10, 10): + eq((i*us/3)//us, round(i/3)) + for i in range(-10, 10): + eq((i*us/-3)//us, round(i/-3)) + + # Issue #23521 + eq(td(seconds=1) / (1 / 0.6112295), td(microseconds=611229)) + + # Issue #11576 + eq(td(999999999, 86399, 999999) - td(999999999, 86399, 999998), + td(0, 0, 1)) + eq(td(999999999, 1, 1) - td(999999999, 1, 0), + td(0, 0, 1)) + + def test_disallowed_computations(self): + a = timedelta(42) + + # Add/sub ints or floats should be illegal + for i in 1, 1.0: + assertRaises(TypeError, lambda: a+i) + assertRaises(TypeError, lambda: a-i) + assertRaises(TypeError, lambda: i+a) + assertRaises(TypeError, lambda: i-a) + + # Division of int by timedelta doesn't make sense. + # Division by zero doesn't make sense. + zero = 0 + assertRaises(TypeError, lambda: zero // a) + assertRaises(ZeroDivisionError, lambda: a // zero) + assertRaises(ZeroDivisionError, lambda: a / zero) + assertRaises(ZeroDivisionError, lambda: a / 0.0) + assertRaises(TypeError, lambda: a / '') + + @support.requires_IEEE_754 + def test_disallowed_special(self): + a = timedelta(42) + assertRaises(ValueError, a.__mul__, NAN) + assertRaises(ValueError, a.__truediv__, NAN) + + def test_basic_attributes(self): + days, seconds, us = 1, 7, 31 + td = timedelta(days, seconds, us) + assert_equal(td.days, days) + assert_equal(td.seconds, seconds) + assert_equal(td.microseconds, us) + + def test_total_seconds(self): + td = timedelta(days=365) + assert_equal(td.total_seconds(), 31536000.0) + for total_seconds in [123456.789012, -123456.789012, 0.123456, 0, 1e6]: + td = timedelta(seconds=total_seconds) + assert_equal(td.total_seconds(), total_seconds) + # Issue8644: Test that td.total_seconds() has the same + # accuracy as td / timedelta(seconds=1). + for ms in [-1, -2, -123]: + td = timedelta(microseconds=ms) + assert_equal(td.total_seconds(), td / timedelta(seconds=1)) + + def test_carries(self): + t1 = timedelta(days=100, + weeks=-7, + hours=-24*(100-49), + minutes=-3, + seconds=12, + microseconds=(3*60 - 12) * 1e6 + 1) + t2 = timedelta(microseconds=1) + assert_equal(t1, t2) + + def test_hash_equality(self): + t1 = timedelta(days=100, + weeks=-7, + hours=-24*(100-49), + minutes=-3, + seconds=12, + microseconds=(3*60 - 12) * 1000000) + t2 = timedelta() + assert_equal(hash(t1), hash(t2)) + + t1 += timedelta(weeks=7) + t2 += timedelta(days=7*7) + assert_equal(t1, t2) + assert_equal(hash(t1), hash(t2)) + + d = {t1: 1} + d[t2] = 2 + assert_equal(len(d), 1) + assert_equal(d[t1], 2) + + def test_pickling(self): + args = 12, 34, 56 + orig = timedelta(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_compare(self): + t1 = timedelta(2, 3, 4) + t2 = timedelta(2, 3, 4) + assert_equal(t1, t2) + assert_true(t1 <= t2) + assert_true(t1 >= t2) + assert_false(t1 != t2) + assert_false(t1 < t2) + assert_false(t1 > t2) + + for args in (3, 3, 3), (2, 4, 4), (2, 3, 5): + t2 = timedelta(*args) # this is larger than t1 + assert_true(t1 < t2) + assert_true(t2 > t1) + assert_true(t1 <= t2) + assert_true(t2 >= t1) + assert_true(t1 != t2) + assert_true(t2 != t1) + assert_false(t1 == t2) + assert_false(t2 == t1) + assert_false(t1 > t2) + assert_false(t2 < t1) + assert_false(t1 >= t2) + assert_false(t2 <= t1) + + for badarg in OTHERSTUFF: + assert_equal(t1 == badarg, False) + assert_equal(t1 != badarg, True) + assert_equal(badarg == t1, False) + assert_equal(badarg != t1, True) + + assertRaises(TypeError, lambda: t1 <= badarg) + assertRaises(TypeError, lambda: t1 < badarg) + assertRaises(TypeError, lambda: t1 > badarg) + assertRaises(TypeError, lambda: t1 >= badarg) + assertRaises(TypeError, lambda: badarg <= t1) + assertRaises(TypeError, lambda: badarg < t1) + assertRaises(TypeError, lambda: badarg > t1) + assertRaises(TypeError, lambda: badarg >= t1) + + def test_str(self): + td = timedelta + eq = assert_equal + + eq(str(td(1)), "1 day, 0:00:00") + eq(str(td(-1)), "-1 day, 0:00:00") + eq(str(td(2)), "2 days, 0:00:00") + eq(str(td(-2)), "-2 days, 0:00:00") + + eq(str(td(hours=12, minutes=58, seconds=59)), "12:58:59") + eq(str(td(hours=2, minutes=3, seconds=4)), "2:03:04") + eq(str(td(weeks=-30, hours=23, minutes=12, seconds=34)), + "-210 days, 23:12:34") + + eq(str(td(milliseconds=1)), "0:00:00.001000") + eq(str(td(microseconds=3)), "0:00:00.000003") + + eq(str(td(days=999999999, hours=23, minutes=59, seconds=59, + microseconds=999999)), + "999999999 days, 23:59:59.999999") + + def test_repr(self): + name = 'datetime.' + self.theclass.__name__ + assert_equal(repr(self.theclass(1)), + "%s(1)" % name) + assert_equal(repr(self.theclass(10, 2)), + "%s(10, 2)" % name) + assert_equal(repr(self.theclass(-10, 2, 400000)), + "%s(-10, 2, 400000)" % name) + + def test_roundtrip(self): + for td in (timedelta(days=999999999, hours=23, minutes=59, + seconds=59, microseconds=999999), + timedelta(days=-999999999), + timedelta(days=-999999999, seconds=1), + timedelta(days=1, seconds=2, microseconds=3)): + + # Verify td -> string -> td identity. + s = repr(td) + assert_true(s.startswith('datetime.')) + s = s[9:] + td2 = eval(s) + assert_equal(td, td2) + + # Verify identity via reconstructing from pieces. + td2 = timedelta(td.days, td.seconds, td.microseconds) + assert_equal(td, td2) + + def test_resolution_info(self): + assert_isinstance(timedelta.min, timedelta) + assert_isinstance(timedelta.max, timedelta) + assert_isinstance(timedelta.resolution, timedelta) + assert_true(timedelta.max > timedelta.min) + assert_equal(timedelta.min, timedelta(-999999999)) + assert_equal(timedelta.max, timedelta(999999999, 24*3600-1, 1e6-1)) + assert_equal(timedelta.resolution, timedelta(0, 0, 1)) + + def test_overflow(self): + tiny = timedelta.resolution + + td = timedelta.min + tiny + td -= tiny # no problem + assertRaises(OverflowError, td.__sub__, tiny) + assertRaises(OverflowError, td.__add__, -tiny) + + td = timedelta.max - tiny + td += tiny # no problem + assertRaises(OverflowError, td.__add__, tiny) + assertRaises(OverflowError, td.__sub__, -tiny) + + assertRaises(OverflowError, lambda: -timedelta.max) + + day = timedelta(1) + assertRaises(OverflowError, day.__mul__, 10**9) + assertRaises(OverflowError, day.__mul__, 1e9) + assertRaises(OverflowError, day.__truediv__, 1e-20) + assertRaises(OverflowError, day.__truediv__, 1e-10) + assertRaises(OverflowError, day.__truediv__, 9e-10) + + @support.requires_IEEE_754 + def _test_overflow_special(self): + day = timedelta(1) + assertRaises(OverflowError, day.__mul__, INF) + assertRaises(OverflowError, day.__mul__, -INF) + + def test_microsecond_rounding(self): + td = timedelta + eq = assert_equal + + # 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.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=1.5/1000), td(microseconds=2)) + eq(td(milliseconds=-1.5/1000), td(microseconds=-2)) + eq(td(seconds=0.5/10**6), td(microseconds=0)) + eq(td(seconds=-0.5/10**6), td(microseconds=-0)) + eq(td(seconds=1/2**7), td(microseconds=7812)) + eq(td(seconds=-1/2**7), td(microseconds=-7812)) + + # Rounding due to contributions from more than one field. + us_per_hour = 3600e6 + us_per_day = us_per_hour * 24 + eq(td(days=.4/us_per_day), td(0)) + eq(td(hours=.2/us_per_hour), td(0)) + eq(td(days=.4/us_per_day, hours=.2/us_per_hour), td(microseconds=1)) + + eq(td(days=-.4/us_per_day), td(0)) + eq(td(hours=-.2/us_per_hour), td(0)) + eq(td(days=-.4/us_per_day, hours=-.2/us_per_hour), td(microseconds=-1)) + + # Test for a patch in Issue 8860 + eq(td(microseconds=0.5), 0.5*td(microseconds=1.0)) + eq(td(microseconds=0.5)//td.resolution, 0.5*td.resolution//td.resolution) + + def test_massive_normalization(self): + td = timedelta(microseconds=-1) + assert_equal((td.days, td.seconds, td.microseconds), + (-1, 24*3600-1, 999999)) + + def test_bool(self): + assert_true(timedelta(1)) + assert_true(timedelta(0, 1)) + assert_true(timedelta(0, 0, 1)) + assert_true(timedelta(microseconds=1)) + assert_false(timedelta(0)) + + def test_subclass_timedelta(self): + + class T(timedelta): + @staticmethod + def from_td(td): + return T(td.days, td.seconds, td.microseconds) + + def as_hours(self): + sum = (self.days * 24 + + self.seconds / 3600.0 + + self.microseconds / 3600e6) + return round(sum) + + t1 = T(days=1) + self.assertIs(type(t1), T) + assert_equal(t1.as_hours(), 24) + + t2 = T(days=-1, seconds=-3600) + self.assertIs(type(t2), T) + assert_equal(t2.as_hours(), -25) + + t3 = t1 + t2 + self.assertIs(type(t3), timedelta) + t4 = T.from_td(t3) + self.assertIs(type(t4), T) + assert_equal(t3.days, t4.days) + assert_equal(t3.seconds, t4.seconds) + assert_equal(t3.microseconds, t4.microseconds) + assert_equal(str(t3), str(t4)) + assert_equal(t4.as_hours(), -1) + + def test_division(self): + t = timedelta(hours=1, minutes=24, seconds=19) + second = timedelta(seconds=1) + assert_equal(t / second, 5059.0) + assert_equal(t // second, 5059) + + t = timedelta(minutes=2, seconds=30) + minute = timedelta(minutes=1) + assert_equal(t / minute, 2.5) + assert_equal(t // minute, 2) + + zerotd = timedelta(0) + assertRaises(ZeroDivisionError, truediv, t, zerotd) + assertRaises(ZeroDivisionError, floordiv, t, zerotd) + + # assertRaises(TypeError, truediv, t, 2) + # note: floor division of a timedelta by an integer *is* + # currently permitted. + + def test_remainder(self): + t = timedelta(minutes=2, seconds=30) + minute = timedelta(minutes=1) + r = t % minute + assert_equal(r, timedelta(seconds=30)) + + t = timedelta(minutes=-2, seconds=30) + r = t % minute + assert_equal(r, timedelta(seconds=30)) + + zerotd = timedelta(0) + assertRaises(ZeroDivisionError, mod, t, zerotd) + + assertRaises(TypeError, mod, t, 10) + + def test_divmod(self): + t = timedelta(minutes=2, seconds=30) + minute = timedelta(minutes=1) + q, r = divmod(t, minute) + assert_equal(q, 2) + assert_equal(r, timedelta(seconds=30)) + + t = timedelta(minutes=-2, seconds=30) + q, r = divmod(t, minute) + assert_equal(q, -2) + assert_equal(r, timedelta(seconds=30)) + + zerotd = timedelta(0) + assertRaises(ZeroDivisionError, divmod, t, zerotd) + + assertRaises(TypeError, divmod, t, 10) + + +############################################################################# +# date tests + +class TestDateOnly(unittest.TestCase): + # Tests here won't pass if also run on datetime objects, so don't + # subclass this to test datetimes too. + + def test_delta_non_days_ignored(self): + dt = date(2000, 1, 2) + delta = timedelta(days=1, hours=2, minutes=3, seconds=4, + microseconds=5) + days = timedelta(delta.days) + assert_equal(days, timedelta(1)) + + dt2 = dt + delta + assert_equal(dt2, dt + days) + + dt2 = delta + dt + assert_equal(dt2, dt + days) + + dt2 = dt - delta + assert_equal(dt2, dt - days) + + delta = -delta + days = timedelta(delta.days) + assert_equal(days, timedelta(-2)) + + dt2 = dt + delta + assert_equal(dt2, dt + days) + + dt2 = delta + dt + assert_equal(dt2, dt + days) + + dt2 = dt - delta + assert_equal(dt2, dt - days) + +class SubclassDate(date): + sub_var = 1 + +class TestDate(HarmlessMixedComparison, unittest.TestCase): + # Tests here should pass for both dates and datetimes, except for a + # few tests that TestDateTime overrides. + + theclass = date + + def test_basic_attributes(self): + dt = self.theclass(2002, 3, 1) + assert_equal(dt.year, 2002) + assert_equal(dt.month, 3) + assert_equal(dt.day, 1) + + def test_roundtrip(self): + for dt in (self.theclass(1, 2, 3), + self.theclass.today()): + # Verify dt -> string -> date identity. + s = repr(dt) + assert_true(s.startswith('datetime.')) + s = s[9:] + dt2 = eval(s) + assert_equal(dt, dt2) + + # Verify identity via reconstructing from pieces. + dt2 = self.theclass(dt.year, dt.month, dt.day) + assert_equal(dt, dt2) + + def test_ordinal_conversions(self): + # Check some fixed values. + for y, m, d, n in [(1, 1, 1, 1), # calendar origin + (1, 12, 31, 365), + (2, 1, 1, 366), + # first example from "Calendrical Calculations" + (1945, 11, 12, 710347)]: + d = self.theclass(y, m, d) + assert_equal(n, d.toordinal()) + fromord = self.theclass.fromordinal(n) + assert_equal(d, fromord) + if hasattr(fromord, "hour"): + # if we're checking something fancier than a date, verify + # the extra fields have been zeroed out + assert_equal(fromord.hour, 0) + assert_equal(fromord.minute, 0) + assert_equal(fromord.second, 0) + assert_equal(fromord.microsecond, 0) + + # Check first and last days of year spottily across the whole + # range of years supported. + for year in range(MINYEAR, MAXYEAR+1, 7): + # Verify (year, 1, 1) -> ordinal -> y, m, d is identity. + d = self.theclass(year, 1, 1) + n = d.toordinal() + d2 = self.theclass.fromordinal(n) + assert_equal(d, d2) + # Verify that moving back a day gets to the end of year-1. + if year > 1: + d = self.theclass.fromordinal(n-1) + d2 = self.theclass(year-1, 12, 31) + assert_equal(d, d2) + assert_equal(d2.toordinal(), n-1) + + # Test every day in a leap-year and a non-leap year. + dim = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + for year, isleap in (2000, True), (2002, False): + n = self.theclass(year, 1, 1).toordinal() + for month, maxday in zip(range(1, 13), dim): + if month == 2 and isleap: + maxday += 1 + for day in range(1, maxday+1): + d = self.theclass(year, month, day) + assert_equal(d.toordinal(), n) + assert_equal(d, self.theclass.fromordinal(n)) + n += 1 + + def test_extreme_ordinals(self): + a = self.theclass.min + a = self.theclass(a.year, a.month, a.day) # get rid of time parts + aord = a.toordinal() + b = a.fromordinal(aord) + assert_equal(a, b) + + assertRaises(ValueError, lambda: a.fromordinal(aord - 1)) + + b = a + timedelta(days=1) + assert_equal(b.toordinal(), aord + 1) + assert_equal(b, self.theclass.fromordinal(aord + 1)) + + a = self.theclass.max + a = self.theclass(a.year, a.month, a.day) # get rid of time parts + aord = a.toordinal() + b = a.fromordinal(aord) + assert_equal(a, b) + + assertRaises(ValueError, lambda: a.fromordinal(aord + 1)) + + b = a - timedelta(days=1) + assert_equal(b.toordinal(), aord - 1) + assert_equal(b, self.theclass.fromordinal(aord - 1)) + + def test_bad_constructor_arguments(self): + # bad years + self.theclass(MINYEAR, 1, 1) # no exception + self.theclass(MAXYEAR, 1, 1) # no exception + assertRaises(ValueError, self.theclass, MINYEAR-1, 1, 1) + assertRaises(ValueError, self.theclass, MAXYEAR+1, 1, 1) + # bad months + self.theclass(2000, 1, 1) # no exception + self.theclass(2000, 12, 1) # no exception + assertRaises(ValueError, self.theclass, 2000, 0, 1) + assertRaises(ValueError, self.theclass, 2000, 13, 1) + # bad days + self.theclass(2000, 2, 29) # no exception + self.theclass(2004, 2, 29) # no exception + self.theclass(2400, 2, 29) # no exception + assertRaises(ValueError, self.theclass, 2000, 2, 30) + assertRaises(ValueError, self.theclass, 2001, 2, 29) + assertRaises(ValueError, self.theclass, 2100, 2, 29) + assertRaises(ValueError, self.theclass, 1900, 2, 29) + assertRaises(ValueError, self.theclass, 2000, 1, 0) + assertRaises(ValueError, self.theclass, 2000, 1, 32) + + def test_hash_equality(self): + d = self.theclass(2000, 12, 31) + # same thing + e = self.theclass(2000, 12, 31) + assert_equal(d, e) + assert_equal(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + assert_equal(len(dic), 1) + assert_equal(dic[d], 2) + assert_equal(dic[e], 2) + + d = self.theclass(2001, 1, 1) + # same thing + e = self.theclass(2001, 1, 1) + assert_equal(d, e) + assert_equal(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + assert_equal(len(dic), 1) + assert_equal(dic[d], 2) + assert_equal(dic[e], 2) + + def test_computations(self): + a = self.theclass(2002, 1, 31) + b = self.theclass(1956, 1, 31) + c = self.theclass(2001,2,1) + + diff = a-b + assert_equal(diff.days, 46*365 + len(range(1956, 2002, 4))) + assert_equal(diff.seconds, 0) + assert_equal(diff.microseconds, 0) + + day = timedelta(1) + week = timedelta(7) + a = self.theclass(2002, 3, 2) + assert_equal(a + day, self.theclass(2002, 3, 3)) + assert_equal(day + a, self.theclass(2002, 3, 3)) + assert_equal(a - day, self.theclass(2002, 3, 1)) + assert_equal(-day + a, self.theclass(2002, 3, 1)) + assert_equal(a + week, self.theclass(2002, 3, 9)) + assert_equal(a - week, self.theclass(2002, 2, 23)) + assert_equal(a + 52*week, self.theclass(2003, 3, 1)) + assert_equal(a - 52*week, self.theclass(2001, 3, 3)) + assert_equal((a + week) - a, week) + assert_equal((a + day) - a, day) + assert_equal((a - week) - a, -week) + assert_equal((a - day) - a, -day) + assert_equal(a - (a + week), -week) + assert_equal(a - (a + day), -day) + assert_equal(a - (a - week), week) + assert_equal(a - (a - day), day) + assert_equal(c - (c - day), day) + + # Add/sub ints or floats should be illegal + for i in 1, 1.0: + assertRaises(TypeError, lambda: a+i) + assertRaises(TypeError, lambda: a-i) + assertRaises(TypeError, lambda: i+a) + assertRaises(TypeError, lambda: i-a) + + # delta - date is senseless. + assertRaises(TypeError, lambda: day - a) + # mixing date and (delta or date) via * or // is senseless + assertRaises(TypeError, lambda: day * a) + assertRaises(TypeError, lambda: a * day) + assertRaises(TypeError, lambda: day // a) + assertRaises(TypeError, lambda: a // day) + assertRaises(TypeError, lambda: a * a) + assertRaises(TypeError, lambda: a // a) + # date + date is senseless + assertRaises(TypeError, lambda: a + a) + + def test_overflow(self): + tiny = self.theclass.resolution + + for delta in [tiny, timedelta(1), timedelta(2)]: + dt = self.theclass.min + delta + dt -= delta # no problem + assertRaises(OverflowError, dt.__sub__, delta) + assertRaises(OverflowError, dt.__add__, -delta) + + dt = self.theclass.max - delta + dt += delta # no problem + assertRaises(OverflowError, dt.__add__, delta) + assertRaises(OverflowError, dt.__sub__, -delta) + + def test_fromtimestamp(self): + import time + + # Try an arbitrary fixed value. + year, month, day = 1999, 9, 19 + ts = time.mktime((year, month, day, 0, 0, 0, 0, 0, -1)) + d = self.theclass.fromtimestamp(ts) + assert_equal(d.year, year) + assert_equal(d.month, month) + assert_equal(d.day, day) + + def test_insane_fromtimestamp(self): + # It's possible that some platform maps time_t to double, + # and that this test will fail there. This test should + # exempt such platforms (provided they return reasonable + # results!). + for insane in -1e200, 1e200: + assertRaises(OverflowError, self.theclass.fromtimestamp, + insane) + + def test_today(self): + import time + + # We claim that today() is like fromtimestamp(time.time()), so + # prove it. + for dummy in range(3): + today = self.theclass.today() + ts = time.time() + todayagain = self.theclass.fromtimestamp(ts) + if today == todayagain: + break + # There are several legit reasons that could fail: + # 1. It recently became midnight, between the today() and the + # time() calls. + # 2. The platform time() has such fine resolution that we'll + # never get the same value twice. + # 3. The platform time() has poor resolution, and we just + # happened to call today() right before a resolution quantum + # boundary. + # 4. The system clock got fiddled between calls. + # In any case, wait a little while and try again. + time.sleep(0.1) + + # It worked or it didn't. If it didn't, assume it's reason #2, and + # let the test pass if they're within half a second of each other. + if today != todayagain: + self.assertAlmostEqual(todayagain, today, + delta=timedelta(seconds=0.5)) + + def test_weekday(self): + for i in range(7): + # March 4, 2002 is a Monday + assert_equal(self.theclass(2002, 3, 4+i).weekday(), i) + assert_equal(self.theclass(2002, 3, 4+i).isoweekday(), i+1) + # January 2, 1956 is a Monday + assert_equal(self.theclass(1956, 1, 2+i).weekday(), i) + assert_equal(self.theclass(1956, 1, 2+i).isoweekday(), i+1) + + def test_isocalendar(self): + # Check examples from + # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm + for i in range(7): + d = self.theclass(2003, 12, 22+i) + assert_equal(d.isocalendar(), (2003, 52, i+1)) + d = self.theclass(2003, 12, 29) + timedelta(i) + assert_equal(d.isocalendar(), (2004, 1, i+1)) + d = self.theclass(2004, 1, 5+i) + assert_equal(d.isocalendar(), (2004, 2, i+1)) + d = self.theclass(2009, 12, 21+i) + assert_equal(d.isocalendar(), (2009, 52, i+1)) + d = self.theclass(2009, 12, 28) + timedelta(i) + assert_equal(d.isocalendar(), (2009, 53, i+1)) + d = self.theclass(2010, 1, 4+i) + assert_equal(d.isocalendar(), (2010, 1, i+1)) + + def test_iso_long_years(self): + # Calculate long ISO years and compare to table from + # http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm + ISO_LONG_YEARS_TABLE = """ + 4 32 60 88 + 9 37 65 93 + 15 43 71 99 + 20 48 76 + 26 54 82 + + 105 133 161 189 + 111 139 167 195 + 116 144 172 + 122 150 178 + 128 156 184 + + 201 229 257 285 + 207 235 263 291 + 212 240 268 296 + 218 246 274 + 224 252 280 + + 303 331 359 387 + 308 336 364 392 + 314 342 370 398 + 320 348 376 + 325 353 381 + """ + iso_long_years = sorted(map(int, ISO_LONG_YEARS_TABLE.split())) + L = [] + for i in range(400): + d = self.theclass(2000+i, 12, 31) + d1 = self.theclass(1600+i, 12, 31) + assert_equal(d.isocalendar()[1:], d1.isocalendar()[1:]) + if d.isocalendar()[1] == 53: + L.append(i) + assert_equal(L, iso_long_years) + + def test_isoformat(self): + t = self.theclass(2, 3, 2) + assert_equal(t.isoformat(), "0002-03-02") + + def test_ctime(self): + t = self.theclass(2002, 3, 2) + assert_equal(t.ctime(), "Sat Mar 2 00:00:00 2002") + + def test_strftime(self): + t = self.theclass(2005, 3, 2) + assert_equal(t.strftime("m:%m d:%d y:%y"), "m:03 d:02 y:05") + assert_equal(t.strftime(""), "") # SF bug #761337 + assert_equal(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + + assertRaises(TypeError, t.strftime) # needs an arg + assertRaises(TypeError, t.strftime, "one", "two") # too many args + assertRaises(TypeError, t.strftime, 42) # arg wrong type + + # test that unicode input is allowed (issue 2782) + assert_equal(t.strftime("%m"), "03") + + # A naive object replaces %z and %Z w/ empty strings. + assert_equal(t.strftime("'%z' '%Z'"), "'' ''") + + #make sure that invalid format specifiers are handled correctly + #assertRaises(ValueError, t.strftime, "%e") + #assertRaises(ValueError, t.strftime, "%") + #assertRaises(ValueError, t.strftime, "%#") + + #oh well, some systems just ignore those invalid ones. + #at least, excercise them to make sure that no crashes + #are generated + for f in ["%e", "%", "%#"]: + try: + t.strftime(f) + except ValueError: + pass + + #check that this standard extension works + t.strftime("%f") + + + def test_format(self): + dt = self.theclass(2007, 9, 10) + assert_equal(dt.__format__(''), str(dt)) + + # check that a derived class's __str__() gets called + class A(self.theclass): + def __str__(self): + return 'A' + a = A(2007, 9, 10) + assert_equal(a.__format__(''), 'A') + + # check that a derived class's strftime gets called + class B(self.theclass): + def strftime(self, format_spec): + return 'B' + b = B(2007, 9, 10) + assert_equal(b.__format__(''), str(dt)) + + for fmt in ["m:%m d:%d y:%y", + "m:%m d:%d y:%y H:%H M:%M S:%S", + "%z %Z", + ]: + assert_equal(dt.__format__(fmt), dt.strftime(fmt)) + assert_equal(a.__format__(fmt), dt.strftime(fmt)) + assert_equal(b.__format__(fmt), 'B') + + def test_resolution_info(self): + # XXX: Should min and max respect subclassing? + if issubclass(self.theclass, datetime): + expected_class = datetime + else: + expected_class = date + assert_isinstance(self.theclass.min, expected_class) + assert_isinstance(self.theclass.max, expected_class) + assert_isinstance(self.theclass.resolution, timedelta) + assert_true(self.theclass.max > self.theclass.min) + + def test_extreme_timedelta(self): + big = self.theclass.max - self.theclass.min + # 3652058 days, 23 hours, 59 minutes, 59 seconds, 999999 microseconds + n = (big.days*24*3600 + big.seconds)*1000000 + big.microseconds + # n == 315537897599999999 ~= 2**58.13 + justasbig = timedelta(0, 0, n) + assert_equal(big, justasbig) + assert_equal(self.theclass.min + big, self.theclass.max) + assert_equal(self.theclass.max - big, self.theclass.min) + + def test_timetuple(self): + for i in range(7): + # January 2, 1956 is a Monday (0) + d = self.theclass(1956, 1, 2+i) + t = d.timetuple() + assert_equal(t, (1956, 1, 2+i, 0, 0, 0, i, 2+i, -1)) + # February 1, 1956 is a Wednesday (2) + d = self.theclass(1956, 2, 1+i) + t = d.timetuple() + assert_equal(t, (1956, 2, 1+i, 0, 0, 0, (2+i)%7, 32+i, -1)) + # March 1, 1956 is a Thursday (3), and is the 31+29+1 = 61st day + # of the year. + d = self.theclass(1956, 3, 1+i) + t = d.timetuple() + assert_equal(t, (1956, 3, 1+i, 0, 0, 0, (3+i)%7, 61+i, -1)) + assert_equal(t.tm_year, 1956) + assert_equal(t.tm_mon, 3) + assert_equal(t.tm_mday, 1+i) + assert_equal(t.tm_hour, 0) + assert_equal(t.tm_min, 0) + assert_equal(t.tm_sec, 0) + assert_equal(t.tm_wday, (3+i)%7) + assert_equal(t.tm_yday, 61+i) + assert_equal(t.tm_isdst, -1) + + def test_pickling(self): + args = 6, 7, 23 + orig = self.theclass(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_compare(self): + t1 = self.theclass(2, 3, 4) + t2 = self.theclass(2, 3, 4) + assert_equal(t1, t2) + assert_true(t1 <= t2) + assert_true(t1 >= t2) + assert_false(t1 != t2) + assert_false(t1 < t2) + assert_false(t1 > t2) + + for args in (3, 3, 3), (2, 4, 4), (2, 3, 5): + t2 = self.theclass(*args) # this is larger than t1 + assert_true(t1 < t2) + assert_true(t2 > t1) + assert_true(t1 <= t2) + assert_true(t2 >= t1) + assert_true(t1 != t2) + assert_true(t2 != t1) + assert_false(t1 == t2) + assert_false(t2 == t1) + assert_false(t1 > t2) + assert_false(t2 < t1) + assert_false(t1 >= t2) + assert_false(t2 <= t1) + + for badarg in OTHERSTUFF: + assert_equal(t1 == badarg, False) + assert_equal(t1 != badarg, True) + assert_equal(badarg == t1, False) + assert_equal(badarg != t1, True) + + assertRaises(TypeError, lambda: t1 < badarg) + assertRaises(TypeError, lambda: t1 > badarg) + assertRaises(TypeError, lambda: t1 >= badarg) + assertRaises(TypeError, lambda: badarg <= t1) + assertRaises(TypeError, lambda: badarg < t1) + assertRaises(TypeError, lambda: badarg > t1) + assertRaises(TypeError, lambda: badarg >= t1) + + def test_mixed_compare(self): + our = self.theclass(2000, 4, 5) + + # Our class can be compared for equality to other classes + assert_equal(our == 1, False) + assert_equal(1 == our, False) + assert_equal(our != 1, True) + assert_equal(1 != our, True) + + # But the ordering is undefined + assertRaises(TypeError, lambda: our < 1) + assertRaises(TypeError, lambda: 1 < our) + + # Repeat those tests with a different class + + class SomeClass: + pass + + their = SomeClass() + assert_equal(our == their, False) + assert_equal(their == our, False) + assert_equal(our != their, True) + assert_equal(their != our, True) + assertRaises(TypeError, lambda: our < their) + assertRaises(TypeError, lambda: their < our) + + # However, if the other class explicitly defines ordering + # relative to our class, it is allowed to do so + + class LargerThanAnything: + def __lt__(self, other): + return False + def __le__(self, other): + return isinstance(other, LargerThanAnything) + def __eq__(self, other): + return isinstance(other, LargerThanAnything) + def __ne__(self, other): + return not isinstance(other, LargerThanAnything) + def __gt__(self, other): + return not isinstance(other, LargerThanAnything) + def __ge__(self, other): + return True + + their = LargerThanAnything() + assert_equal(our == their, False) + assert_equal(their == our, False) + assert_equal(our != their, True) + assert_equal(their != our, True) + assert_equal(our < their, True) + assert_equal(their < our, False) + + def test_bool(self): + # All dates are considered true. + assert_true(self.theclass.min) + assert_true(self.theclass.max) + + def test_strftime_y2k(self): + for y in (1, 49, 70, 99, 100, 999, 1000, 1970): + d = self.theclass(y, 1, 1) + # Issue 13305: For years < 1000, the value is not always + # padded to 4 digits across platforms. The C standard + # assumes year >= 1900, so it does not specify the number + # of digits. + if d.strftime("%Y") != '%04d' % y: + # Year 42 returns '42', not padded + assert_equal(d.strftime("%Y"), '%d' % y) + # '0042' is obtained anyway + assert_equal(d.strftime("%4Y"), '%04d' % y) + + def test_replace(self): + cls = self.theclass + args = [1, 2, 3] + base = cls(*args) + assert_equal(base, base.replace()) + + i = 0 + for name, newval in (("year", 2), + ("month", 3), + ("day", 4)): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + assert_equal(expected, got) + i += 1 + + # Out of bounds. + base = cls(2000, 2, 29) + assertRaises(ValueError, base.replace, year=2001) + + def test_subclass_date(self): + + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop('extra') + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.year + self.month + + args = 2003, 4, 14 + + dt1 = self.theclass(*args) + dt2 = C(*args, **{'extra': 7}) + + assert_equal(dt2.__class__, C) + assert_equal(dt2.theAnswer, 42) + assert_equal(dt2.extra, 7) + assert_equal(dt1.toordinal(), dt2.toordinal()) + assert_equal(dt2.newmeth(-7), dt1.year + dt1.month - 7) + + def test_pickling_subclass_date(self): + + args = 6, 7, 23 + orig = SubclassDate(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_backdoor_resistance(self): + # For fast unpickling, the constructor accepts a pickle byte string. + # This is a low-overhead backdoor. A user can (by intent or + # mistake) pass a string directly, which (if it's the right length) + # will get treated like a pickle, and bypass the normal sanity + # checks in the constructor. This can create insane objects. + # The constructor doesn't want to burn the time to validate all + # fields, but does check the month field. This stops, e.g., + # datetime.datetime('1995-03-25') from yielding an insane object. + base = b'1995-03-25' + if not issubclass(self.theclass, datetime): + base = base[:4] + for month_byte in b'9', b'\0', b'\r', b'\xff': + assertRaises(TypeError, self.theclass, + base[:2] + month_byte + base[3:]) + # Good bytes, but bad tzinfo: + assertRaises(TypeError, self.theclass, + bytes([1] * len(base)), 'EST') + + for ord_byte in range(1, 13): + # This shouldn't blow up because of the month byte alone. If + # the implementation changes to do more-careful checking, it may + # blow up because other fields are insane. + self.theclass(base[:2] + bytes([ord_byte]) + base[3:]) + +############################################################################# +# datetime tests + +class SubclassDatetime(datetime): + sub_var = 1 + +class TestDateTime(TestDate): + + theclass = datetime + + def test_basic_attributes(self): + dt = self.theclass(2002, 3, 1, 12, 0) + assert_equal(dt.year, 2002) + assert_equal(dt.month, 3) + assert_equal(dt.day, 1) + assert_equal(dt.hour, 12) + assert_equal(dt.minute, 0) + assert_equal(dt.second, 0) + assert_equal(dt.microsecond, 0) + + def test_basic_attributes_nonzero(self): + # Make sure all attributes are non-zero so bugs in + # bit-shifting access show up. + dt = self.theclass(2002, 3, 1, 12, 59, 59, 8000) + assert_equal(dt.year, 2002) + assert_equal(dt.month, 3) + assert_equal(dt.day, 1) + assert_equal(dt.hour, 12) + assert_equal(dt.minute, 59) + assert_equal(dt.second, 59) + assert_equal(dt.microsecond, 8000) + + def test_roundtrip(self): + for dt in (self.theclass(1, 2, 3, 4, 5, 6, 7), + self.theclass.now()): + # Verify dt -> string -> datetime identity. + s = repr(dt) + assert_true(s.startswith('datetime.')) + s = s[9:] + dt2 = eval(s) + assert_equal(dt, dt2) + + # Verify identity via reconstructing from pieces. + dt2 = self.theclass(dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.microsecond) + assert_equal(dt, dt2) + + def test_isoformat(self): + t = self.theclass(2, 3, 2, 4, 5, 1, 123) + assert_equal(t.isoformat(), "0002-03-02T04:05:01.000123") + assert_equal(t.isoformat('T'), "0002-03-02T04:05:01.000123") + assert_equal(t.isoformat(' '), "0002-03-02 04:05:01.000123") + assert_equal(t.isoformat('\x00'), "0002-03-02\x0004:05:01.000123") + # str is ISO format with the separator forced to a blank. + assert_equal(str(t), "0002-03-02 04:05:01.000123") + + t = self.theclass(2, 3, 2) + assert_equal(t.isoformat(), "0002-03-02T00:00:00") + assert_equal(t.isoformat('T'), "0002-03-02T00:00:00") + assert_equal(t.isoformat(' '), "0002-03-02 00:00:00") + # str is ISO format with the separator forced to a blank. + assert_equal(str(t), "0002-03-02 00:00:00") + + def test_format(self): + dt = self.theclass(2007, 9, 10, 4, 5, 1, 123) + assert_equal(dt.__format__(''), str(dt)) + + # check that a derived class's __str__() gets called + class A(self.theclass): + def __str__(self): + return 'A' + a = A(2007, 9, 10, 4, 5, 1, 123) + assert_equal(a.__format__(''), 'A') + + # check that a derived class's strftime gets called + class B(self.theclass): + def strftime(self, format_spec): + return 'B' + b = B(2007, 9, 10, 4, 5, 1, 123) + assert_equal(b.__format__(''), str(dt)) + + for fmt in ["m:%m d:%d y:%y", + "m:%m d:%d y:%y H:%H M:%M S:%S", + "%z %Z", + ]: + assert_equal(dt.__format__(fmt), dt.strftime(fmt)) + assert_equal(a.__format__(fmt), dt.strftime(fmt)) + assert_equal(b.__format__(fmt), 'B') + + def test_more_ctime(self): + # Test fields that TestDate doesn't touch. + import time + + t = self.theclass(2002, 3, 2, 18, 3, 5, 123) + assert_equal(t.ctime(), "Sat Mar 2 18:03:05 2002") + # Oops! The next line fails on Win2K under MSVC 6, so it's commented + # out. The difference is that t.ctime() produces " 2" for the day, + # but platform ctime() produces "02" for the day. According to + # C99, t.ctime() is correct here. + # assert_equal(t.ctime(), time.ctime(time.mktime(t.timetuple()))) + + # So test a case where that difference doesn't matter. + t = self.theclass(2002, 3, 22, 18, 3, 5, 123) + assert_equal(t.ctime(), time.ctime(time.mktime(t.timetuple()))) + + def test_tz_independent_comparing(self): + dt1 = self.theclass(2002, 3, 1, 9, 0, 0) + dt2 = self.theclass(2002, 3, 1, 10, 0, 0) + dt3 = self.theclass(2002, 3, 1, 9, 0, 0) + assert_equal(dt1, dt3) + assert_true(dt2 > dt3) + + # Make sure comparison doesn't forget microseconds, and isn't done + # via comparing a float timestamp (an IEEE double doesn't have enough + # precision to span microsecond resolution across years 1 thru 9999, + # so comparing via timestamp necessarily calls some distinct values + # equal). + dt1 = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999998) + us = timedelta(microseconds=1) + dt2 = dt1 + us + assert_equal(dt2 - dt1, us) + assert_true(dt1 < dt2) + + def test_strftime_with_bad_tzname_replace(self): + # verify ok if tzinfo.tzname().replace() returns a non-string + class MyTzInfo(FixedOffset): + def tzname(self, dt): + class MyStr(str): + def replace(self, *args): + return None + return MyStr('name') + t = self.theclass(2005, 3, 2, 0, 0, 0, 0, MyTzInfo(3, 'name')) + assertRaises(TypeError, t.strftime, '%Z') + + def test_bad_constructor_arguments(self): + # bad years + self.theclass(MINYEAR, 1, 1) # no exception + self.theclass(MAXYEAR, 1, 1) # no exception + assertRaises(ValueError, self.theclass, MINYEAR-1, 1, 1) + assertRaises(ValueError, self.theclass, MAXYEAR+1, 1, 1) + # bad months + self.theclass(2000, 1, 1) # no exception + self.theclass(2000, 12, 1) # no exception + assertRaises(ValueError, self.theclass, 2000, 0, 1) + assertRaises(ValueError, self.theclass, 2000, 13, 1) + # bad days + self.theclass(2000, 2, 29) # no exception + self.theclass(2004, 2, 29) # no exception + self.theclass(2400, 2, 29) # no exception + assertRaises(ValueError, self.theclass, 2000, 2, 30) + assertRaises(ValueError, self.theclass, 2001, 2, 29) + assertRaises(ValueError, self.theclass, 2100, 2, 29) + assertRaises(ValueError, self.theclass, 1900, 2, 29) + assertRaises(ValueError, self.theclass, 2000, 1, 0) + assertRaises(ValueError, self.theclass, 2000, 1, 32) + # bad hours + self.theclass(2000, 1, 31, 0) # no exception + self.theclass(2000, 1, 31, 23) # no exception + assertRaises(ValueError, self.theclass, 2000, 1, 31, -1) + assertRaises(ValueError, self.theclass, 2000, 1, 31, 24) + # bad minutes + self.theclass(2000, 1, 31, 23, 0) # no exception + self.theclass(2000, 1, 31, 23, 59) # no exception + assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, -1) + assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 60) + # bad seconds + self.theclass(2000, 1, 31, 23, 59, 0) # no exception + self.theclass(2000, 1, 31, 23, 59, 59) # no exception + assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, -1) + assertRaises(ValueError, self.theclass, 2000, 1, 31, 23, 59, 60) + # bad microseconds + self.theclass(2000, 1, 31, 23, 59, 59, 0) # no exception + self.theclass(2000, 1, 31, 23, 59, 59, 999999) # no exception + assertRaises(ValueError, self.theclass, + 2000, 1, 31, 23, 59, 59, -1) + assertRaises(ValueError, self.theclass, + 2000, 1, 31, 23, 59, 59, + 1000000) + + def test_hash_equality(self): + d = self.theclass(2000, 12, 31, 23, 30, 17) + e = self.theclass(2000, 12, 31, 23, 30, 17) + assert_equal(d, e) + assert_equal(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + assert_equal(len(dic), 1) + assert_equal(dic[d], 2) + assert_equal(dic[e], 2) + + d = self.theclass(2001, 1, 1, 0, 5, 17) + e = self.theclass(2001, 1, 1, 0, 5, 17) + assert_equal(d, e) + assert_equal(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + assert_equal(len(dic), 1) + assert_equal(dic[d], 2) + assert_equal(dic[e], 2) + + def test_computations(self): + a = self.theclass(2002, 1, 31) + b = self.theclass(1956, 1, 31) + diff = a-b + assert_equal(diff.days, 46*365 + len(range(1956, 2002, 4))) + assert_equal(diff.seconds, 0) + assert_equal(diff.microseconds, 0) + a = self.theclass(2002, 3, 2, 17, 6) + millisec = timedelta(0, 0, 1000) + hour = timedelta(0, 3600) + day = timedelta(1) + week = timedelta(7) + assert_equal(a + hour, self.theclass(2002, 3, 2, 18, 6)) + assert_equal(hour + a, self.theclass(2002, 3, 2, 18, 6)) + assert_equal(a + 10*hour, self.theclass(2002, 3, 3, 3, 6)) + assert_equal(a - hour, self.theclass(2002, 3, 2, 16, 6)) + assert_equal(-hour + a, self.theclass(2002, 3, 2, 16, 6)) + assert_equal(a - hour, a + -hour) + assert_equal(a - 20*hour, self.theclass(2002, 3, 1, 21, 6)) + assert_equal(a + day, self.theclass(2002, 3, 3, 17, 6)) + assert_equal(a - day, self.theclass(2002, 3, 1, 17, 6)) + assert_equal(a + week, self.theclass(2002, 3, 9, 17, 6)) + assert_equal(a - week, self.theclass(2002, 2, 23, 17, 6)) + assert_equal(a + 52*week, self.theclass(2003, 3, 1, 17, 6)) + assert_equal(a - 52*week, self.theclass(2001, 3, 3, 17, 6)) + assert_equal((a + week) - a, week) + assert_equal((a + day) - a, day) + assert_equal((a + hour) - a, hour) + assert_equal((a + millisec) - a, millisec) + assert_equal((a - week) - a, -week) + assert_equal((a - day) - a, -day) + assert_equal((a - hour) - a, -hour) + assert_equal((a - millisec) - a, -millisec) + assert_equal(a - (a + week), -week) + assert_equal(a - (a + day), -day) + assert_equal(a - (a + hour), -hour) + assert_equal(a - (a + millisec), -millisec) + assert_equal(a - (a - week), week) + assert_equal(a - (a - day), day) + assert_equal(a - (a - hour), hour) + assert_equal(a - (a - millisec), millisec) + assert_equal(a + (week + day + hour + millisec), + self.theclass(2002, 3, 10, 18, 6, 0, 1000)) + assert_equal(a + (week + day + hour + millisec), + (((a + week) + day) + hour) + millisec) + assert_equal(a - (week + day + hour + millisec), + self.theclass(2002, 2, 22, 16, 5, 59, 999000)) + assert_equal(a - (week + day + hour + millisec), + (((a - week) - day) - hour) - millisec) + # Add/sub ints or floats should be illegal + for i in 1, 1.0: + assertRaises(TypeError, lambda: a+i) + assertRaises(TypeError, lambda: a-i) + assertRaises(TypeError, lambda: i+a) + assertRaises(TypeError, lambda: i-a) + + # delta - datetime is senseless. + assertRaises(TypeError, lambda: day - a) + # mixing datetime and (delta or datetime) via * or // is senseless + assertRaises(TypeError, lambda: day * a) + assertRaises(TypeError, lambda: a * day) + assertRaises(TypeError, lambda: day // a) + assertRaises(TypeError, lambda: a // day) + assertRaises(TypeError, lambda: a * a) + assertRaises(TypeError, lambda: a // a) + # datetime + datetime is senseless + assertRaises(TypeError, lambda: a + a) + + def test_pickling(self): + args = 6, 7, 23, 20, 59, 1, 64**2 + orig = self.theclass(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_more_pickling(self): + a = self.theclass(2003, 2, 7, 16, 48, 37, 444116) + for proto in range(pickle.HIGHEST_PROTOCOL + 1): + s = pickle.dumps(a, proto) + b = pickle.loads(s) + assert_equal(b.year, 2003) + assert_equal(b.month, 2) + assert_equal(b.day, 7) + + def test_pickling_subclass_datetime(self): + args = 6, 7, 23, 20, 59, 1, 64**2 + orig = SubclassDatetime(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_more_compare(self): + # The test_compare() inherited from TestDate covers the error cases. + # We just want to test lexicographic ordering on the members datetime + # has that date lacks. + args = [2000, 11, 29, 20, 58, 16, 999998] + t1 = self.theclass(*args) + t2 = self.theclass(*args) + assert_equal(t1, t2) + assert_true(t1 <= t2) + assert_true(t1 >= t2) + assert_false(t1 != t2) + assert_false(t1 < t2) + assert_false(t1 > t2) + + for i in range(len(args)): + newargs = args[:] + newargs[i] = args[i] + 1 + t2 = self.theclass(*newargs) # this is larger than t1 + assert_true(t1 < t2) + assert_true(t2 > t1) + assert_true(t1 <= t2) + assert_true(t2 >= t1) + assert_true(t1 != t2) + assert_true(t2 != t1) + assert_false(t1 == t2) + assert_false(t2 == t1) + assert_false(t1 > t2) + assert_false(t2 < t1) + assert_false(t1 >= t2) + assert_false(t2 <= t1) + + + # A helper for timestamp constructor tests. + def verify_field_equality(self, expected, got): + assert_equal(expected.tm_year, got.year) + assert_equal(expected.tm_mon, got.month) + assert_equal(expected.tm_mday, got.day) + assert_equal(expected.tm_hour, got.hour) + assert_equal(expected.tm_min, got.minute) + assert_equal(expected.tm_sec, got.second) + + def test_fromtimestamp(self): + import time + + ts = time.time() + expected = time.localtime(ts) + got = self.theclass.fromtimestamp(ts) + self.verify_field_equality(expected, got) + + def test_utcfromtimestamp(self): + import time + + ts = time.time() + expected = time.gmtime(ts) + got = self.theclass.utcfromtimestamp(ts) + self.verify_field_equality(expected, got) + + # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in + # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0). + @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') + def test_timestamp_naive(self): + t = self.theclass(1970, 1, 1) + assert_equal(t.timestamp(), 18000.0) + t = self.theclass(1970, 1, 1, 1, 2, 3, 4) + assert_equal(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) + assert_in(self.theclass.fromtimestamp(t.timestamp()), + [t - timedelta(hours=1), t + timedelta(hours=1)]) + # Ambiguous hour defaults to DST + t = self.theclass(2012, 11, 4, 1, 30) + assert_equal(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)]: + try: + s = t.timestamp() + except OverflowError: + pass + else: + assert_equal(self.theclass.fromtimestamp(s), t) + + def test_timestamp_aware(self): + t = self.theclass(1970, 1, 1, tzinfo=timezone.utc) + assert_equal(t.timestamp(), 0.0) + t = self.theclass(1970, 1, 1, 1, 2, 3, 4, tzinfo=timezone.utc) + assert_equal(t.timestamp(), + 3600 + 2*60 + 3 + 4*1e-6) + t = self.theclass(1970, 1, 1, 1, 2, 3, 4, + tzinfo=timezone(timedelta(hours=-5), 'EST')) + assert_equal(t.timestamp(), + 18000 + 3600 + 2*60 + 3 + 4*1e-6) + + def test_microsecond_rounding(self): + for fts in [self.theclass.fromtimestamp, + self.theclass.utcfromtimestamp]: + zero = fts(0) + assert_equal(zero.second, 0) + assert_equal(zero.microsecond, 0) + one = fts(1e-6) + try: + minus_one = fts(-1e-6) + except OSError: + # localtime(-1) and gmtime(-1) is not supported on Windows + pass + else: + assert_equal(minus_one.second, 59) + assert_equal(minus_one.microsecond, 999999) + + t = fts(-1e-8) + assert_equal(t, zero) + t = fts(-9e-7) + assert_equal(t, minus_one) + t = fts(-1e-7) + assert_equal(t, zero) + t = fts(-1/2**7) + assert_equal(t.second, 59) + assert_equal(t.microsecond, 992188) + + t = fts(1e-7) + assert_equal(t, zero) + t = fts(9e-7) + assert_equal(t, one) + t = fts(0.99999949) + assert_equal(t.second, 0) + assert_equal(t.microsecond, 999999) + t = fts(0.9999999) + assert_equal(t.second, 1) + assert_equal(t.microsecond, 0) + t = fts(1/2**7) + assert_equal(t.second, 0) + assert_equal(t.microsecond, 7812) + + def test_insane_fromtimestamp(self): + # It's possible that some platform maps time_t to double, + # and that this test will fail there. This test should + # exempt such platforms (provided they return reasonable + # results!). + for insane in -1e200, 1e200: + assertRaises(OverflowError, self.theclass.fromtimestamp, + insane) + + def test_insane_utcfromtimestamp(self): + # It's possible that some platform maps time_t to double, + # and that this test will fail there. This test should + # exempt such platforms (provided they return reasonable + # results!). + for insane in -1e200, 1e200: + assertRaises(OverflowError, self.theclass.utcfromtimestamp, + insane) + @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") + def test_negative_float_fromtimestamp(self): + # The result is tz-dependent; at least test that this doesn't + # fail (like it did before bug 1646728 was fixed). + self.theclass.fromtimestamp(-1.05) + + @unittest.skipIf(sys.platform == "win32", "Windows doesn't accept negative timestamps") + def test_negative_float_utcfromtimestamp(self): + d = self.theclass.utcfromtimestamp(-1.05) + assert_equal(d, self.theclass(1969, 12, 31, 23, 59, 58, 950000)) + + def test_utcnow(self): + import time + + # Call it a success if utcnow() and utcfromtimestamp() are within + # a second of each other. + tolerance = timedelta(seconds=1) + for dummy in range(3): + from_now = self.theclass.utcnow() + from_timestamp = self.theclass.utcfromtimestamp(time.time()) + if abs(from_timestamp - from_now) <= tolerance: + break + # Else try again a few times. + self.assertLessEqual(abs(from_timestamp - from_now), tolerance) + + + def test_more_timetuple(self): + # This tests fields beyond those tested by the TestDate.test_timetuple. + t = self.theclass(2004, 12, 31, 6, 22, 33) + assert_equal(t.timetuple(), (2004, 12, 31, 6, 22, 33, 4, 366, -1)) + assert_equal(t.timetuple(), + (t.year, t.month, t.day, + t.hour, t.minute, t.second, + t.weekday(), + t.toordinal() - date(t.year, 1, 1).toordinal() + 1, + -1)) + tt = t.timetuple() + assert_equal(tt.tm_year, t.year) + assert_equal(tt.tm_mon, t.month) + assert_equal(tt.tm_mday, t.day) + assert_equal(tt.tm_hour, t.hour) + assert_equal(tt.tm_min, t.minute) + assert_equal(tt.tm_sec, t.second) + assert_equal(tt.tm_wday, t.weekday()) + assert_equal(tt.tm_yday, t.toordinal() - + date(t.year, 1, 1).toordinal() + 1) + assert_equal(tt.tm_isdst, -1) + + def test_more_strftime(self): + # This tests fields beyond those tested by the TestDate.test_strftime. + t = self.theclass(2004, 12, 31, 6, 22, 33, 47) + assert_equal(t.strftime("%m %d %y %f %S %M %H %j"), + "12 31 04 000047 33 22 06 366") + + def test_extract(self): + dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234) + assert_equal(dt.date(), date(2002, 3, 4)) + assert_equal(dt.time(), time(18, 45, 3, 1234)) + + def test_combine(self): + d = date(2002, 3, 4) + t = time(18, 45, 3, 1234) + expected = self.theclass(2002, 3, 4, 18, 45, 3, 1234) + combine = self.theclass.combine + dt = combine(d, t) + assert_equal(dt, expected) + + dt = combine(time=t, date=d) + assert_equal(dt, expected) + + assert_equal(d, dt.date()) + assert_equal(t, dt.time()) + assert_equal(dt, combine(dt.date(), dt.time())) + + assertRaises(TypeError, combine) # need an arg + assertRaises(TypeError, combine, d) # need two args + assertRaises(TypeError, combine, t, d) # args reversed + assertRaises(TypeError, combine, d, t, 1) # too many args + assertRaises(TypeError, combine, "date", "time") # wrong types + assertRaises(TypeError, combine, d, "time") # wrong type + assertRaises(TypeError, combine, "date", t) # wrong type + + def test_replace(self): + cls = self.theclass + args = [1, 2, 3, 4, 5, 6, 7] + base = cls(*args) + assert_equal(base, base.replace()) + + i = 0 + for name, newval in (("year", 2), + ("month", 3), + ("day", 4), + ("hour", 5), + ("minute", 6), + ("second", 7), + ("microsecond", 8)): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + assert_equal(expected, got) + i += 1 + + # Out of bounds. + base = cls(2000, 2, 29) + assertRaises(ValueError, base.replace, year=2001) + + def test_astimezone(self): + # Pretty boring! The TZ test is more interesting here. astimezone() + # simply can't be applied to a naive object. + dt = self.theclass.now() + f = FixedOffset(44, "") + assertRaises(ValueError, dt.astimezone) # naive + assertRaises(TypeError, dt.astimezone, f, f) # too many args + assertRaises(TypeError, dt.astimezone, dt) # arg wrong type + assertRaises(ValueError, dt.astimezone, f) # naive + assertRaises(ValueError, dt.astimezone, tz=f) # naive + + class Bogus(tzinfo): + def utcoffset(self, dt): return None + def dst(self, dt): return timedelta(0) + bog = Bogus() + assertRaises(ValueError, dt.astimezone, bog) # naive + assertRaises(ValueError, + dt.replace(tzinfo=bog).astimezone, f) + + class AlsoBogus(tzinfo): + def utcoffset(self, dt): return timedelta(0) + def dst(self, dt): return None + alsobog = AlsoBogus() + assertRaises(ValueError, dt.astimezone, alsobog) # also naive + + def test_subclass_datetime(self): + + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop('extra') + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.year + self.month + self.second + + args = 2003, 4, 14, 12, 13, 41 + + dt1 = self.theclass(*args) + dt2 = C(*args, **{'extra': 7}) + + assert_equal(dt2.__class__, C) + assert_equal(dt2.theAnswer, 42) + assert_equal(dt2.extra, 7) + assert_equal(dt1.toordinal(), dt2.toordinal()) + assert_equal(dt2.newmeth(-7), dt1.year + dt1.month + + dt1.second - 7) + +class TestSubclassDateTime(TestDateTime): + theclass = SubclassDatetime + # Override tests not designed for subclass + @unittest.skip('not appropriate for subclasses') + def test_roundtrip(self): + pass + +class SubclassTime(time): + sub_var = 1 + +class TestTime(HarmlessMixedComparison, unittest.TestCase): + + theclass = time + + def test_basic_attributes(self): + t = self.theclass(12, 0) + assert_equal(t.hour, 12) + assert_equal(t.minute, 0) + assert_equal(t.second, 0) + assert_equal(t.microsecond, 0) + + def test_basic_attributes_nonzero(self): + # Make sure all attributes are non-zero so bugs in + # bit-shifting access show up. + t = self.theclass(12, 59, 59, 8000) + assert_equal(t.hour, 12) + assert_equal(t.minute, 59) + assert_equal(t.second, 59) + assert_equal(t.microsecond, 8000) + + def test_roundtrip(self): + t = self.theclass(1, 2, 3, 4) + + # Verify t -> string -> time identity. + s = repr(t) + assert_true(s.startswith('datetime.')) + s = s[9:] + t2 = eval(s) + assert_equal(t, t2) + + # Verify identity via reconstructing from pieces. + t2 = self.theclass(t.hour, t.minute, t.second, + t.microsecond) + assert_equal(t, t2) + + def test_comparing(self): + args = [1, 2, 3, 4] + t1 = self.theclass(*args) + t2 = self.theclass(*args) + assert_equal(t1, t2) + assert_true(t1 <= t2) + assert_true(t1 >= t2) + assert_false(t1 != t2) + assert_false(t1 < t2) + assert_false(t1 > t2) + + for i in range(len(args)): + newargs = args[:] + newargs[i] = args[i] + 1 + t2 = self.theclass(*newargs) # this is larger than t1 + assert_true(t1 < t2) + assert_true(t2 > t1) + assert_true(t1 <= t2) + assert_true(t2 >= t1) + assert_true(t1 != t2) + assert_true(t2 != t1) + assert_false(t1 == t2) + assert_false(t2 == t1) + assert_false(t1 > t2) + assert_false(t2 < t1) + assert_false(t1 >= t2) + assert_false(t2 <= t1) + + for badarg in OTHERSTUFF: + assert_equal(t1 == badarg, False) + assert_equal(t1 != badarg, True) + assert_equal(badarg == t1, False) + assert_equal(badarg != t1, True) + + assertRaises(TypeError, lambda: t1 <= badarg) + assertRaises(TypeError, lambda: t1 < badarg) + assertRaises(TypeError, lambda: t1 > badarg) + assertRaises(TypeError, lambda: t1 >= badarg) + assertRaises(TypeError, lambda: badarg <= t1) + assertRaises(TypeError, lambda: badarg < t1) + assertRaises(TypeError, lambda: badarg > t1) + assertRaises(TypeError, lambda: badarg >= t1) + + def test_bad_constructor_arguments(self): + # bad hours + self.theclass(0, 0) # no exception + self.theclass(23, 0) # no exception + assertRaises(ValueError, self.theclass, -1, 0) + assertRaises(ValueError, self.theclass, 24, 0) + # bad minutes + self.theclass(23, 0) # no exception + self.theclass(23, 59) # no exception + assertRaises(ValueError, self.theclass, 23, -1) + assertRaises(ValueError, self.theclass, 23, 60) + # bad seconds + self.theclass(23, 59, 0) # no exception + self.theclass(23, 59, 59) # no exception + assertRaises(ValueError, self.theclass, 23, 59, -1) + assertRaises(ValueError, self.theclass, 23, 59, 60) + # bad microseconds + self.theclass(23, 59, 59, 0) # no exception + self.theclass(23, 59, 59, 999999) # no exception + assertRaises(ValueError, self.theclass, 23, 59, 59, -1) + assertRaises(ValueError, self.theclass, 23, 59, 59, 1000000) + + def test_hash_equality(self): + d = self.theclass(23, 30, 17) + e = self.theclass(23, 30, 17) + assert_equal(d, e) + assert_equal(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + assert_equal(len(dic), 1) + assert_equal(dic[d], 2) + assert_equal(dic[e], 2) + + d = self.theclass(0, 5, 17) + e = self.theclass(0, 5, 17) + assert_equal(d, e) + assert_equal(hash(d), hash(e)) + + dic = {d: 1} + dic[e] = 2 + assert_equal(len(dic), 1) + assert_equal(dic[d], 2) + assert_equal(dic[e], 2) + + def test_isoformat(self): + t = self.theclass(4, 5, 1, 123) + assert_equal(t.isoformat(), "04:05:01.000123") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass() + assert_equal(t.isoformat(), "00:00:00") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass(microsecond=1) + assert_equal(t.isoformat(), "00:00:00.000001") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass(microsecond=10) + assert_equal(t.isoformat(), "00:00:00.000010") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass(microsecond=100) + assert_equal(t.isoformat(), "00:00:00.000100") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass(microsecond=1000) + assert_equal(t.isoformat(), "00:00:00.001000") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass(microsecond=10000) + assert_equal(t.isoformat(), "00:00:00.010000") + assert_equal(t.isoformat(), str(t)) + + t = self.theclass(microsecond=100000) + assert_equal(t.isoformat(), "00:00:00.100000") + assert_equal(t.isoformat(), str(t)) + + def test_1653736(self): + # verify it doesn't accept extra keyword arguments + t = self.theclass(second=1) + assertRaises(TypeError, t.isoformat, foo=3) + + def test_strftime(self): + t = self.theclass(1, 2, 3, 4) + assert_equal(t.strftime('%H %M %S %f'), "01 02 03 000004") + # A naive object replaces %z and %Z with empty strings. + assert_equal(t.strftime("'%z' '%Z'"), "'' ''") + + def test_format(self): + t = self.theclass(1, 2, 3, 4) + assert_equal(t.__format__(''), str(t)) + + # check that a derived class's __str__() gets called + class A(self.theclass): + def __str__(self): + return 'A' + a = A(1, 2, 3, 4) + assert_equal(a.__format__(''), 'A') + + # check that a derived class's strftime gets called + class B(self.theclass): + def strftime(self, format_spec): + return 'B' + b = B(1, 2, 3, 4) + assert_equal(b.__format__(''), str(t)) + + for fmt in ['%H %M %S', + ]: + assert_equal(t.__format__(fmt), t.strftime(fmt)) + assert_equal(a.__format__(fmt), t.strftime(fmt)) + assert_equal(b.__format__(fmt), 'B') + + def test_str(self): + assert_equal(str(self.theclass(1, 2, 3, 4)), "01:02:03.000004") + assert_equal(str(self.theclass(10, 2, 3, 4000)), "10:02:03.004000") + assert_equal(str(self.theclass(0, 2, 3, 400000)), "00:02:03.400000") + assert_equal(str(self.theclass(12, 2, 3, 0)), "12:02:03") + assert_equal(str(self.theclass(23, 15, 0, 0)), "23:15:00") + + def test_repr(self): + name = 'datetime.' + self.theclass.__name__ + assert_equal(repr(self.theclass(1, 2, 3, 4)), + "%s(1, 2, 3, 4)" % name) + assert_equal(repr(self.theclass(10, 2, 3, 4000)), + "%s(10, 2, 3, 4000)" % name) + assert_equal(repr(self.theclass(0, 2, 3, 400000)), + "%s(0, 2, 3, 400000)" % name) + assert_equal(repr(self.theclass(12, 2, 3, 0)), + "%s(12, 2, 3)" % name) + assert_equal(repr(self.theclass(23, 15, 0, 0)), + "%s(23, 15)" % name) + + def test_resolution_info(self): + assert_isinstance(self.theclass.min, self.theclass) + assert_isinstance(self.theclass.max, self.theclass) + assert_isinstance(self.theclass.resolution, timedelta) + assert_true(self.theclass.max > self.theclass.min) + + def test_pickling(self): + args = 20, 59, 16, 64**2 + orig = self.theclass(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_pickling_subclass_time(self): + args = 20, 59, 16, 64**2 + orig = SubclassTime(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + def test_bool(self): + cls = self.theclass + assert_true(cls(1)) + assert_true(cls(0, 1)) + assert_true(cls(0, 0, 1)) + assert_true(cls(0, 0, 0, 1)) + assert_false(cls(0)) + assert_false(cls()) + + def test_replace(self): + cls = self.theclass + args = [1, 2, 3, 4] + base = cls(*args) + assert_equal(base, base.replace()) + + i = 0 + for name, newval in (("hour", 5), + ("minute", 6), + ("second", 7), + ("microsecond", 8)): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + assert_equal(expected, got) + i += 1 + + # Out of bounds. + base = cls(1) + assertRaises(ValueError, base.replace, hour=24) + assertRaises(ValueError, base.replace, minute=-1) + assertRaises(ValueError, base.replace, second=100) + assertRaises(ValueError, base.replace, microsecond=1000000) + + def test_subclass_time(self): + + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop('extra') + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.hour + self.second + + args = 4, 5, 6 + + dt1 = self.theclass(*args) + dt2 = C(*args, **{'extra': 7}) + + assert_equal(dt2.__class__, C) + assert_equal(dt2.theAnswer, 42) + assert_equal(dt2.extra, 7) + assert_equal(dt1.isoformat(), dt2.isoformat()) + assert_equal(dt2.newmeth(-7), dt1.hour + dt1.second - 7) + + def test_backdoor_resistance(self): + # see TestDate.test_backdoor_resistance(). + base = '2:59.0' + for hour_byte in ' ', '9', chr(24), '\xff': + assertRaises(TypeError, self.theclass, + hour_byte + base[1:]) + +# A mixin for classes with a tzinfo= argument. Subclasses must define +# theclass as a class atribute, and theclass(1, 1, 1, tzinfo=whatever) +# must be legit (which is true for time and datetime). +class TZInfoBase: + + def test_argument_passing(self): + cls = self.theclass + # A datetime passes itself on, a time passes None. + class introspective(tzinfo): + def tzname(self, dt): return dt and "real" or "none" + def utcoffset(self, dt): + return timedelta(minutes = dt and 42 or -42) + dst = utcoffset + + obj = cls(1, 2, 3, tzinfo=introspective()) + + expected = cls is time and "none" or "real" + assert_equal(obj.tzname(), expected) + + expected = timedelta(minutes=(cls is time and -42 or 42)) + assert_equal(obj.utcoffset(), expected) + assert_equal(obj.dst(), expected) + + def test_bad_tzinfo_classes(self): + cls = self.theclass + assertRaises(TypeError, cls, 1, 1, 1, tzinfo=12) + + class NiceTry(object): + def __init__(self): pass + def utcoffset(self, dt): pass + assertRaises(TypeError, cls, 1, 1, 1, tzinfo=NiceTry) + + class BetterTry(tzinfo): + def __init__(self): pass + def utcoffset(self, dt): pass + b = BetterTry() + t = cls(1, 1, 1, tzinfo=b) + self.assertIs(t.tzinfo, b) + + def test_utc_offset_out_of_bounds(self): + class Edgy(tzinfo): + def __init__(self, offset): + self.offset = timedelta(minutes=offset) + def utcoffset(self, dt): + return self.offset + + cls = self.theclass + for offset, legit in ((-1440, False), + (-1439, True), + (1439, True), + (1440, False)): + if cls is time: + t = cls(1, 2, 3, tzinfo=Edgy(offset)) + elif cls is datetime: + t = cls(6, 6, 6, 1, 2, 3, tzinfo=Edgy(offset)) + else: + assert 0, "impossible" + if legit: + aofs = abs(offset) + h, m = divmod(aofs, 60) + tag = "%c%02d:%02d" % (offset < 0 and '-' or '+', h, m) + if isinstance(t, datetime): + t = t.timetz() + assert_equal(str(t), "01:02:03" + tag) + else: + assertRaises(ValueError, str, t) + + def test_tzinfo_classes(self): + cls = self.theclass + class C1(tzinfo): + def utcoffset(self, dt): return None + def dst(self, dt): return None + def tzname(self, dt): return None + for t in (cls(1, 1, 1), + cls(1, 1, 1, tzinfo=None), + cls(1, 1, 1, tzinfo=C1())): + self.assertIsNone(t.utcoffset()) + self.assertIsNone(t.dst()) + self.assertIsNone(t.tzname()) + + class C3(tzinfo): + def utcoffset(self, dt): return timedelta(minutes=-1439) + def dst(self, dt): return timedelta(minutes=1439) + def tzname(self, dt): return "aname" + t = cls(1, 1, 1, tzinfo=C3()) + assert_equal(t.utcoffset(), timedelta(minutes=-1439)) + assert_equal(t.dst(), timedelta(minutes=1439)) + assert_equal(t.tzname(), "aname") + + # Wrong types. + class C4(tzinfo): + def utcoffset(self, dt): return "aname" + def dst(self, dt): return 7 + def tzname(self, dt): return 0 + t = cls(1, 1, 1, tzinfo=C4()) + assertRaises(TypeError, t.utcoffset) + assertRaises(TypeError, t.dst) + assertRaises(TypeError, t.tzname) + + # Offset out of range. + class C6(tzinfo): + def utcoffset(self, dt): return timedelta(hours=-24) + def dst(self, dt): return timedelta(hours=24) + t = cls(1, 1, 1, tzinfo=C6()) + assertRaises(ValueError, t.utcoffset) + assertRaises(ValueError, t.dst) + + # Not a whole number of minutes. + class C7(tzinfo): + def utcoffset(self, dt): return timedelta(seconds=61) + def dst(self, dt): return timedelta(microseconds=-81) + t = cls(1, 1, 1, tzinfo=C7()) + assertRaises(ValueError, t.utcoffset) + assertRaises(ValueError, t.dst) + + def test_aware_compare(self): + cls = self.theclass + + # Ensure that utcoffset() gets ignored if the comparands have + # the same tzinfo member. + class OperandDependentOffset(tzinfo): + def utcoffset(self, t): + if t.minute < 10: + # d0 and d1 equal after adjustment + return timedelta(minutes=t.minute) + else: + # d2 off in the weeds + return timedelta(minutes=59) + + base = cls(8, 9, 10, tzinfo=OperandDependentOffset()) + d0 = base.replace(minute=3) + d1 = base.replace(minute=9) + d2 = base.replace(minute=11) + for x in d0, d1, d2: + for y in d0, d1, d2: + for op in lt, le, gt, ge, eq, ne: + got = op(x, y) + expected = op(x.minute, y.minute) + assert_equal(got, expected) + + # However, if they're different members, uctoffset is not ignored. + # Note that a time can't actually have an operand-depedent offset, + # though (and time.utcoffset() passes None to tzinfo.utcoffset()), + # so skip this test for time. + if cls is not time: + d0 = base.replace(minute=3, tzinfo=OperandDependentOffset()) + d1 = base.replace(minute=9, tzinfo=OperandDependentOffset()) + d2 = base.replace(minute=11, tzinfo=OperandDependentOffset()) + for x in d0, d1, d2: + for y in d0, d1, d2: + got = (x > y) - (x < y) + if (x is d0 or x is d1) and (y is d0 or y is d1): + expected = 0 + elif x is y is d2: + expected = 0 + elif x is d2: + expected = -1 + else: + assert y is d2 + expected = 1 + assert_equal(got, expected) + + +# Testing time objects with a non-None tzinfo. +class TestTimeTZ(TestTime, TZInfoBase, unittest.TestCase): + theclass = time + + def test_empty(self): + t = self.theclass() + assert_equal(t.hour, 0) + assert_equal(t.minute, 0) + assert_equal(t.second, 0) + assert_equal(t.microsecond, 0) + self.assertIsNone(t.tzinfo) + + def test_zones(self): + est = FixedOffset(-300, "EST", 1) + utc = FixedOffset(0, "UTC", -2) + met = FixedOffset(60, "MET", 3) + t1 = time( 7, 47, tzinfo=est) + t2 = time(12, 47, tzinfo=utc) + t3 = time(13, 47, tzinfo=met) + t4 = time(microsecond=40) + t5 = time(microsecond=40, tzinfo=utc) + + assert_equal(t1.tzinfo, est) + assert_equal(t2.tzinfo, utc) + assert_equal(t3.tzinfo, met) + self.assertIsNone(t4.tzinfo) + assert_equal(t5.tzinfo, utc) + + assert_equal(t1.utcoffset(), timedelta(minutes=-300)) + assert_equal(t2.utcoffset(), timedelta(minutes=0)) + assert_equal(t3.utcoffset(), timedelta(minutes=60)) + self.assertIsNone(t4.utcoffset()) + assertRaises(TypeError, t1.utcoffset, "no args") + + assert_equal(t1.tzname(), "EST") + assert_equal(t2.tzname(), "UTC") + assert_equal(t3.tzname(), "MET") + self.assertIsNone(t4.tzname()) + assertRaises(TypeError, t1.tzname, "no args") + + assert_equal(t1.dst(), timedelta(minutes=1)) + assert_equal(t2.dst(), timedelta(minutes=-2)) + assert_equal(t3.dst(), timedelta(minutes=3)) + self.assertIsNone(t4.dst()) + assertRaises(TypeError, t1.dst, "no args") + + assert_equal(hash(t1), hash(t2)) + assert_equal(hash(t1), hash(t3)) + assert_equal(hash(t2), hash(t3)) + + assert_equal(t1, t2) + assert_equal(t1, t3) + assert_equal(t2, t3) + self.assertNotEqual(t4, t5) # mixed tz-aware & naive + assertRaises(TypeError, lambda: t4 < t5) # mixed tz-aware & naive + assertRaises(TypeError, lambda: t5 < t4) # mixed tz-aware & naive + + assert_equal(str(t1), "07:47:00-05:00") + assert_equal(str(t2), "12:47:00+00:00") + assert_equal(str(t3), "13:47:00+01:00") + assert_equal(str(t4), "00:00:00.000040") + assert_equal(str(t5), "00:00:00.000040+00:00") + + assert_equal(t1.isoformat(), "07:47:00-05:00") + assert_equal(t2.isoformat(), "12:47:00+00:00") + assert_equal(t3.isoformat(), "13:47:00+01:00") + assert_equal(t4.isoformat(), "00:00:00.000040") + assert_equal(t5.isoformat(), "00:00:00.000040+00:00") + + d = 'datetime.time' + assert_equal(repr(t1), d + "(7, 47, tzinfo=est)") + assert_equal(repr(t2), d + "(12, 47, tzinfo=utc)") + assert_equal(repr(t3), d + "(13, 47, tzinfo=met)") + assert_equal(repr(t4), d + "(0, 0, 0, 40)") + assert_equal(repr(t5), d + "(0, 0, 0, 40, tzinfo=utc)") + + assert_equal(t1.strftime("%H:%M:%S %%Z=%Z %%z=%z"), + "07:47:00 %Z=EST %z=-0500") + assert_equal(t2.strftime("%H:%M:%S %Z %z"), "12:47:00 UTC +0000") + assert_equal(t3.strftime("%H:%M:%S %Z %z"), "13:47:00 MET +0100") + + yuck = FixedOffset(-1439, "%z %Z %%z%%Z") + t1 = time(23, 59, tzinfo=yuck) + assert_equal(t1.strftime("%H:%M %%Z='%Z' %%z='%z'"), + "23:59 %Z='%z %Z %%z%%Z' %z='-2359'") + + # Check that an invalid tzname result raises an exception. + class Badtzname(tzinfo): + tz = 42 + def tzname(self, dt): return self.tz + t = time(2, 3, 4, tzinfo=Badtzname()) + assert_equal(t.strftime("%H:%M:%S"), "02:03:04") + assertRaises(TypeError, t.strftime, "%Z") + + # Issue #6697: + if '_Fast' in str(type(self)): + Badtzname.tz = '\ud800' + assertRaises(ValueError, t.strftime, "%Z") + + def test_hash_edge_cases(self): + # Offsets that overflow a basic time. + t1 = self.theclass(0, 1, 2, 3, tzinfo=FixedOffset(1439, "")) + t2 = self.theclass(0, 0, 2, 3, tzinfo=FixedOffset(1438, "")) + assert_equal(hash(t1), hash(t2)) + + t1 = self.theclass(23, 58, 6, 100, tzinfo=FixedOffset(-1000, "")) + t2 = self.theclass(23, 48, 6, 100, tzinfo=FixedOffset(-1010, "")) + assert_equal(hash(t1), hash(t2)) + + def test_pickling(self): + # Try one without a tzinfo. + args = 20, 59, 16, 64**2 + orig = self.theclass(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + # Try one with a tzinfo. + tinfo = PicklableFixedOffset(-300, 'cookie') + orig = self.theclass(5, 6, 7, tzinfo=tinfo) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + assert_isinstance(derived.tzinfo, PicklableFixedOffset) + assert_equal(derived.utcoffset(), timedelta(minutes=-300)) + assert_equal(derived.tzname(), 'cookie') + + def test_more_bool(self): + # Test cases with non-None tzinfo. + cls = self.theclass + + t = cls(0, tzinfo=FixedOffset(-300, "")) + assert_true(t) + + t = cls(5, tzinfo=FixedOffset(-300, "")) + assert_true(t) + + t = cls(5, tzinfo=FixedOffset(300, "")) + assert_false(t) + + t = cls(23, 59, tzinfo=FixedOffset(23*60 + 59, "")) + assert_false(t) + + # Mostly ensuring this doesn't overflow internally. + t = cls(0, tzinfo=FixedOffset(23*60 + 59, "")) + assert_true(t) + + # But this should yield a value error -- the utcoffset is bogus. + t = cls(0, tzinfo=FixedOffset(24*60, "")) + assertRaises(ValueError, lambda: bool(t)) + + # Likewise. + t = cls(0, tzinfo=FixedOffset(-24*60, "")) + assertRaises(ValueError, lambda: bool(t)) + + def test_replace(self): + cls = self.theclass + z100 = FixedOffset(100, "+100") + zm200 = FixedOffset(timedelta(minutes=-200), "-200") + args = [1, 2, 3, 4, z100] + base = cls(*args) + assert_equal(base, base.replace()) + + i = 0 + for name, newval in (("hour", 5), + ("minute", 6), + ("second", 7), + ("microsecond", 8), + ("tzinfo", zm200)): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + assert_equal(expected, got) + i += 1 + + # Ensure we can get rid of a tzinfo. + assert_equal(base.tzname(), "+100") + base2 = base.replace(tzinfo=None) + self.assertIsNone(base2.tzinfo) + self.assertIsNone(base2.tzname()) + + # Ensure we can add one. + base3 = base2.replace(tzinfo=z100) + assert_equal(base, base3) + self.assertIs(base.tzinfo, base3.tzinfo) + + # Out of bounds. + base = cls(1) + assertRaises(ValueError, base.replace, hour=24) + assertRaises(ValueError, base.replace, minute=-1) + assertRaises(ValueError, base.replace, second=100) + assertRaises(ValueError, base.replace, microsecond=1000000) + + def test_mixed_compare(self): + t1 = time(1, 2, 3) + t2 = time(1, 2, 3) + assert_equal(t1, t2) + t2 = t2.replace(tzinfo=None) + assert_equal(t1, t2) + t2 = t2.replace(tzinfo=FixedOffset(None, "")) + assert_equal(t1, t2) + t2 = t2.replace(tzinfo=FixedOffset(0, "")) + self.assertNotEqual(t1, t2) + + # In time w/ identical tzinfo objects, utcoffset is ignored. + class Varies(tzinfo): + def __init__(self): + self.offset = timedelta(minutes=22) + def utcoffset(self, t): + self.offset += timedelta(minutes=1) + return self.offset + + v = Varies() + t1 = t2.replace(tzinfo=v) + t2 = t2.replace(tzinfo=v) + assert_equal(t1.utcoffset(), timedelta(minutes=23)) + assert_equal(t2.utcoffset(), timedelta(minutes=24)) + assert_equal(t1, t2) + + # But if they're not identical, it isn't ignored. + t2 = t2.replace(tzinfo=Varies()) + assert_true(t1 < t2) # t1's offset counter still going up + + def test_subclass_timetz(self): + + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop('extra') + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.hour + self.second + + args = 4, 5, 6, 500, FixedOffset(-300, "EST", 1) + + dt1 = self.theclass(*args) + dt2 = C(*args, **{'extra': 7}) + + assert_equal(dt2.__class__, C) + assert_equal(dt2.theAnswer, 42) + assert_equal(dt2.extra, 7) + assert_equal(dt1.utcoffset(), dt2.utcoffset()) + assert_equal(dt2.newmeth(-7), dt1.hour + dt1.second - 7) + + +# Testing datetime objects with a non-None tzinfo. + +class TestDateTimeTZ(TestDateTime, TZInfoBase, unittest.TestCase): + theclass = datetime + + def test_trivial(self): + dt = self.theclass(1, 2, 3, 4, 5, 6, 7) + assert_equal(dt.year, 1) + assert_equal(dt.month, 2) + assert_equal(dt.day, 3) + assert_equal(dt.hour, 4) + assert_equal(dt.minute, 5) + assert_equal(dt.second, 6) + assert_equal(dt.microsecond, 7) + assert_equal(dt.tzinfo, None) + + def test_even_more_compare(self): + # The test_compare() and test_more_compare() inherited from TestDate + # and TestDateTime covered non-tzinfo cases. + + # Smallest possible after UTC adjustment. + t1 = self.theclass(1, 1, 1, tzinfo=FixedOffset(1439, "")) + # Largest possible after UTC adjustment. + t2 = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999999, + tzinfo=FixedOffset(-1439, "")) + + # Make sure those compare correctly, and w/o overflow. + assert_true(t1 < t2) + assert_true(t1 != t2) + assert_true(t2 > t1) + + assert_equal(t1, t1) + assert_equal(t2, t2) + + # Equal afer adjustment. + t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(1, "")) + t2 = self.theclass(2, 1, 1, 3, 13, tzinfo=FixedOffset(3*60+13+2, "")) + assert_equal(t1, t2) + + # Change t1 not to subtract a minute, and t1 should be larger. + t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(0, "")) + assert_true(t1 > t2) + + # Change t1 to subtract 2 minutes, and t1 should be smaller. + t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(2, "")) + assert_true(t1 < t2) + + # Back to the original t1, but make seconds resolve it. + t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(1, ""), + second=1) + assert_true(t1 > t2) + + # Likewise, but make microseconds resolve it. + t1 = self.theclass(1, 12, 31, 23, 59, tzinfo=FixedOffset(1, ""), + microsecond=1) + assert_true(t1 > t2) + + # Make t2 naive and it should differ. + t2 = self.theclass.min + self.assertNotEqual(t1, t2) + assert_equal(t2, t2) + + # It's also naive if it has tzinfo but tzinfo.utcoffset() is None. + class Naive(tzinfo): + def utcoffset(self, dt): return None + t2 = self.theclass(5, 6, 7, tzinfo=Naive()) + self.assertNotEqual(t1, t2) + assert_equal(t2, t2) + + # OTOH, it's OK to compare two of these mixing the two ways of being + # naive. + t1 = self.theclass(5, 6, 7) + assert_equal(t1, t2) + + # Try a bogus uctoffset. + class Bogus(tzinfo): + def utcoffset(self, dt): + return timedelta(minutes=1440) # out of bounds + t1 = self.theclass(2, 2, 2, tzinfo=Bogus()) + t2 = self.theclass(2, 2, 2, tzinfo=FixedOffset(0, "")) + assertRaises(ValueError, lambda: t1 == t2) + + def test_pickling(self): + # Try one without a tzinfo. + args = 6, 7, 23, 20, 59, 1, 64**2 + orig = self.theclass(*args) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + + # Try one with a tzinfo. + tinfo = PicklableFixedOffset(-300, 'cookie') + orig = self.theclass(*args, **{'tzinfo': tinfo}) + derived = self.theclass(1, 1, 1, tzinfo=FixedOffset(0, "", 0)) + for pickler, unpickler, proto in pickle_choices: + green = pickler.dumps(orig, proto) + derived = unpickler.loads(green) + assert_equal(orig, derived) + assert_isinstance(derived.tzinfo, PicklableFixedOffset) + assert_equal(derived.utcoffset(), timedelta(minutes=-300)) + assert_equal(derived.tzname(), 'cookie') + + def test_extreme_hashes(self): + # If an attempt is made to hash these via subtracting the offset + # then hashing a datetime object, OverflowError results. The + # Python implementation used to blow up here. + t = self.theclass(1, 1, 1, tzinfo=FixedOffset(1439, "")) + hash(t) + t = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999999, + tzinfo=FixedOffset(-1439, "")) + hash(t) + + # OTOH, an OOB offset should blow up. + t = self.theclass(5, 5, 5, tzinfo=FixedOffset(-1440, "")) + assertRaises(ValueError, hash, t) + + def test_zones(self): + est = FixedOffset(-300, "EST") + utc = FixedOffset(0, "UTC") + met = FixedOffset(60, "MET") + t1 = datetime(2002, 3, 19, 7, 47, tzinfo=est) + t2 = datetime(2002, 3, 19, 12, 47, tzinfo=utc) + t3 = datetime(2002, 3, 19, 13, 47, tzinfo=met) + assert_equal(t1.tzinfo, est) + assert_equal(t2.tzinfo, utc) + assert_equal(t3.tzinfo, met) + assert_equal(t1.utcoffset(), timedelta(minutes=-300)) + assert_equal(t2.utcoffset(), timedelta(minutes=0)) + assert_equal(t3.utcoffset(), timedelta(minutes=60)) + assert_equal(t1.tzname(), "EST") + assert_equal(t2.tzname(), "UTC") + assert_equal(t3.tzname(), "MET") + assert_equal(hash(t1), hash(t2)) + assert_equal(hash(t1), hash(t3)) + assert_equal(hash(t2), hash(t3)) + assert_equal(t1, t2) + assert_equal(t1, t3) + assert_equal(t2, t3) + assert_equal(str(t1), "2002-03-19 07:47:00-05:00") + assert_equal(str(t2), "2002-03-19 12:47:00+00:00") + assert_equal(str(t3), "2002-03-19 13:47:00+01:00") + d = 'datetime.datetime(2002, 3, 19, ' + assert_equal(repr(t1), d + "7, 47, tzinfo=est)") + assert_equal(repr(t2), d + "12, 47, tzinfo=utc)") + assert_equal(repr(t3), d + "13, 47, tzinfo=met)") + + def test_combine(self): + met = FixedOffset(60, "MET") + d = date(2002, 3, 4) + tz = time(18, 45, 3, 1234, tzinfo=met) + dt = datetime.combine(d, tz) + assert_equal(dt, datetime(2002, 3, 4, 18, 45, 3, 1234, + tzinfo=met)) + + def test_extract(self): + met = FixedOffset(60, "MET") + dt = self.theclass(2002, 3, 4, 18, 45, 3, 1234, tzinfo=met) + assert_equal(dt.date(), date(2002, 3, 4)) + assert_equal(dt.time(), time(18, 45, 3, 1234)) + assert_equal(dt.timetz(), time(18, 45, 3, 1234, tzinfo=met)) + + def test_tz_aware_arithmetic(self): + import random + + now = self.theclass.now() + tz55 = FixedOffset(-330, "west 5:30") + timeaware = now.time().replace(tzinfo=tz55) + nowaware = self.theclass.combine(now.date(), timeaware) + self.assertIs(nowaware.tzinfo, tz55) + assert_equal(nowaware.timetz(), timeaware) + + # Can't mix aware and non-aware. + assertRaises(TypeError, lambda: now - nowaware) + assertRaises(TypeError, lambda: nowaware - now) + + # And adding datetime's doesn't make sense, aware or not. + assertRaises(TypeError, lambda: now + nowaware) + assertRaises(TypeError, lambda: nowaware + now) + assertRaises(TypeError, lambda: nowaware + nowaware) + + # Subtracting should yield 0. + assert_equal(now - now, timedelta(0)) + assert_equal(nowaware - nowaware, timedelta(0)) + + # Adding a delta should preserve tzinfo. + delta = timedelta(weeks=1, minutes=12, microseconds=5678) + nowawareplus = nowaware + delta + self.assertIs(nowaware.tzinfo, tz55) + nowawareplus2 = delta + nowaware + self.assertIs(nowawareplus2.tzinfo, tz55) + assert_equal(nowawareplus, nowawareplus2) + + # that - delta should be what we started with, and that - what we + # started with should be delta. + diff = nowawareplus - delta + self.assertIs(diff.tzinfo, tz55) + assert_equal(nowaware, diff) + assertRaises(TypeError, lambda: delta - nowawareplus) + assert_equal(nowawareplus - nowaware, delta) + + # Make up a random timezone. + tzr = FixedOffset(random.randrange(-1439, 1440), "randomtimezone") + # Attach it to nowawareplus. + nowawareplus = nowawareplus.replace(tzinfo=tzr) + self.assertIs(nowawareplus.tzinfo, tzr) + # Make sure the difference takes the timezone adjustments into account. + got = nowaware - nowawareplus + # Expected: (nowaware base - nowaware offset) - + # (nowawareplus base - nowawareplus offset) = + # (nowaware base - nowawareplus base) + + # (nowawareplus offset - nowaware offset) = + # -delta + nowawareplus offset - nowaware offset + expected = nowawareplus.utcoffset() - nowaware.utcoffset() - delta + assert_equal(got, expected) + + # Try max possible difference. + min = self.theclass(1, 1, 1, tzinfo=FixedOffset(1439, "min")) + max = self.theclass(MAXYEAR, 12, 31, 23, 59, 59, 999999, + tzinfo=FixedOffset(-1439, "max")) + maxdiff = max - min + assert_equal(maxdiff, self.theclass.max - self.theclass.min + + timedelta(minutes=2*1439)) + # Different tzinfo, but the same offset + tza = timezone(HOUR, 'A') + tzb = timezone(HOUR, 'B') + delta = min.replace(tzinfo=tza) - max.replace(tzinfo=tzb) + assert_equal(delta, self.theclass.min - self.theclass.max) + + def test_tzinfo_now(self): + meth = self.theclass.now + # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). + base = meth() + # Try with and without naming the keyword. + off42 = FixedOffset(42, "42") + another = meth(off42) + again = meth(tz=off42) + self.assertIs(another.tzinfo, again.tzinfo) + assert_equal(another.utcoffset(), timedelta(minutes=42)) + # Bad argument with and w/o naming the keyword. + assertRaises(TypeError, meth, 16) + assertRaises(TypeError, meth, tzinfo=16) + # Bad keyword name. + assertRaises(TypeError, meth, tinfo=off42) + # Too many args. + assertRaises(TypeError, meth, off42, off42) + + # We don't know which time zone we're in, and don't have a tzinfo + # class to represent it, so seeing whether a tz argument actually + # does a conversion is tricky. + utc = FixedOffset(0, "utc", 0) + for weirdtz in [FixedOffset(timedelta(hours=15, minutes=58), "weirdtz", 0), + timezone(timedelta(hours=15, minutes=58), "weirdtz"),]: + for dummy in range(3): + now = datetime.now(weirdtz) + self.assertIs(now.tzinfo, weirdtz) + utcnow = datetime.utcnow().replace(tzinfo=utc) + now2 = utcnow.astimezone(weirdtz) + if abs(now - now2) < timedelta(seconds=30): + break + # Else the code is broken, or more than 30 seconds passed between + # calls; assuming the latter, just try again. + else: + # Three strikes and we're out. + self.fail("utcnow(), now(tz), or astimezone() may be broken") + + def test_tzinfo_fromtimestamp(self): + import time + meth = self.theclass.fromtimestamp + ts = time.time() + # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). + base = meth(ts) + # Try with and without naming the keyword. + off42 = FixedOffset(42, "42") + another = meth(ts, off42) + again = meth(ts, tz=off42) + self.assertIs(another.tzinfo, again.tzinfo) + assert_equal(another.utcoffset(), timedelta(minutes=42)) + # Bad argument with and w/o naming the keyword. + assertRaises(TypeError, meth, ts, 16) + assertRaises(TypeError, meth, ts, tzinfo=16) + # Bad keyword name. + assertRaises(TypeError, meth, ts, tinfo=off42) + # Too many args. + assertRaises(TypeError, meth, ts, off42, off42) + # Too few args. + assertRaises(TypeError, meth) + + # Try to make sure tz= actually does some conversion. + timestamp = 1000000000 + utcdatetime = datetime.utcfromtimestamp(timestamp) + # In POSIX (epoch 1970), that's 2001-09-09 01:46:40 UTC, give or take. + # But on some flavor of Mac, it's nowhere near that. So we can't have + # any idea here what time that actually is, we can only test that + # relative changes match. + utcoffset = timedelta(hours=-15, minutes=39) # arbitrary, but not zero + tz = FixedOffset(utcoffset, "tz", 0) + expected = utcdatetime + utcoffset + got = datetime.fromtimestamp(timestamp, tz) + assert_equal(expected, got.replace(tzinfo=None)) + + def test_tzinfo_utcnow(self): + meth = self.theclass.utcnow + # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). + base = meth() + # Try with and without naming the keyword; for whatever reason, + # utcnow() doesn't accept a tzinfo argument. + off42 = FixedOffset(42, "42") + assertRaises(TypeError, meth, off42) + assertRaises(TypeError, meth, tzinfo=off42) + + def test_tzinfo_utcfromtimestamp(self): + import time + meth = self.theclass.utcfromtimestamp + ts = time.time() + # Ensure it doesn't require tzinfo (i.e., that this doesn't blow up). + base = meth(ts) + # Try with and without naming the keyword; for whatever reason, + # utcfromtimestamp() doesn't accept a tzinfo argument. + off42 = FixedOffset(42, "42") + assertRaises(TypeError, meth, ts, off42) + assertRaises(TypeError, meth, ts, tzinfo=off42) + + def test_tzinfo_timetuple(self): + # TestDateTime tested most of this. datetime adds a twist to the + # DST flag. + class DST(tzinfo): + def __init__(self, dstvalue): + if isinstance(dstvalue, int): + dstvalue = timedelta(minutes=dstvalue) + self.dstvalue = dstvalue + def dst(self, dt): + return self.dstvalue + + cls = self.theclass + for dstvalue, flag in (-33, 1), (33, 1), (0, 0), (None, -1): + d = cls(1, 1, 1, 10, 20, 30, 40, tzinfo=DST(dstvalue)) + t = d.timetuple() + assert_equal(1, t.tm_year) + assert_equal(1, t.tm_mon) + assert_equal(1, t.tm_mday) + assert_equal(10, t.tm_hour) + assert_equal(20, t.tm_min) + assert_equal(30, t.tm_sec) + assert_equal(0, t.tm_wday) + assert_equal(1, t.tm_yday) + assert_equal(flag, t.tm_isdst) + + # dst() returns wrong type. + assertRaises(TypeError, cls(1, 1, 1, tzinfo=DST("x")).timetuple) + + # dst() at the edge. + assert_equal(cls(1,1,1, tzinfo=DST(1439)).timetuple().tm_isdst, 1) + assert_equal(cls(1,1,1, tzinfo=DST(-1439)).timetuple().tm_isdst, 1) + + # dst() out of range. + assertRaises(ValueError, cls(1,1,1, tzinfo=DST(1440)).timetuple) + assertRaises(ValueError, cls(1,1,1, tzinfo=DST(-1440)).timetuple) + + def test_utctimetuple(self): + class DST(tzinfo): + def __init__(self, dstvalue=0): + if isinstance(dstvalue, int): + dstvalue = timedelta(minutes=dstvalue) + self.dstvalue = dstvalue + def dst(self, dt): + return self.dstvalue + + cls = self.theclass + # This can't work: DST didn't implement utcoffset. + assertRaises(NotImplementedError, + cls(1, 1, 1, tzinfo=DST(0)).utcoffset) + + class UOFS(DST): + def __init__(self, uofs, dofs=None): + DST.__init__(self, dofs) + self.uofs = timedelta(minutes=uofs) + def utcoffset(self, dt): + return self.uofs + + for dstvalue in -33, 33, 0, None: + d = cls(1, 2, 3, 10, 20, 30, 40, tzinfo=UOFS(-53, dstvalue)) + t = d.utctimetuple() + assert_equal(d.year, t.tm_year) + assert_equal(d.month, t.tm_mon) + assert_equal(d.day, t.tm_mday) + assert_equal(11, t.tm_hour) # 20mm + 53mm = 1hn + 13mm + assert_equal(13, t.tm_min) + assert_equal(d.second, t.tm_sec) + assert_equal(d.weekday(), t.tm_wday) + assert_equal(d.toordinal() - date(1, 1, 1).toordinal() + 1, + t.tm_yday) + # Ensure tm_isdst is 0 regardless of what dst() says: DST + # is never in effect for a UTC time. + assert_equal(0, t.tm_isdst) + + # For naive datetime, utctimetuple == timetuple except for isdst + d = cls(1, 2, 3, 10, 20, 30, 40) + t = d.utctimetuple() + assert_equal(t[:-1], d.timetuple()[:-1]) + assert_equal(0, t.tm_isdst) + # Same if utcoffset is None + class NOFS(DST): + def utcoffset(self, dt): + return None + d = cls(1, 2, 3, 10, 20, 30, 40, tzinfo=NOFS()) + t = d.utctimetuple() + assert_equal(t[:-1], d.timetuple()[:-1]) + assert_equal(0, t.tm_isdst) + # Check that bad tzinfo is detected + class BOFS(DST): + def utcoffset(self, dt): + return "EST" + d = cls(1, 2, 3, 10, 20, 30, 40, tzinfo=BOFS()) + assertRaises(TypeError, d.utctimetuple) + + # Check that utctimetuple() is the same as + # astimezone(utc).timetuple() + d = cls(2010, 11, 13, 14, 15, 16, 171819) + for tz in [timezone.min, timezone.utc, timezone.max]: + dtz = d.replace(tzinfo=tz) + assert_equal(dtz.utctimetuple()[:-1], + dtz.astimezone(timezone.utc).timetuple()[:-1]) + # At the edges, UTC adjustment can produce years out-of-range + # for a datetime object. Ensure that an OverflowError is + # raised. + tiny = cls(MINYEAR, 1, 1, 0, 0, 37, tzinfo=UOFS(1439)) + # That goes back 1 minute less than a full day. + assertRaises(OverflowError, tiny.utctimetuple) + + huge = cls(MAXYEAR, 12, 31, 23, 59, 37, 999999, tzinfo=UOFS(-1439)) + # That goes forward 1 minute less than a full day. + assertRaises(OverflowError, huge.utctimetuple) + # More overflow cases + tiny = cls.min.replace(tzinfo=timezone(MINUTE)) + assertRaises(OverflowError, tiny.utctimetuple) + huge = cls.max.replace(tzinfo=timezone(-MINUTE)) + assertRaises(OverflowError, huge.utctimetuple) + + def test_tzinfo_isoformat(self): + zero = FixedOffset(0, "+00:00") + plus = FixedOffset(220, "+03:40") + minus = FixedOffset(-231, "-03:51") + unknown = FixedOffset(None, "") + + cls = self.theclass + datestr = '0001-02-03' + for ofs in None, zero, plus, minus, unknown: + for us in 0, 987001: + d = cls(1, 2, 3, 4, 5, 59, us, tzinfo=ofs) + timestr = '04:05:59' + (us and '.987001' or '') + ofsstr = ofs is not None and d.tzname() or '' + tailstr = timestr + ofsstr + iso = d.isoformat() + assert_equal(iso, datestr + 'T' + tailstr) + assert_equal(iso, d.isoformat('T')) + assert_equal(d.isoformat('k'), datestr + 'k' + tailstr) + assert_equal(d.isoformat('\u1234'), datestr + '\u1234' + tailstr) + assert_equal(str(d), datestr + ' ' + tailstr) + + def test_replace(self): + cls = self.theclass + z100 = FixedOffset(100, "+100") + zm200 = FixedOffset(timedelta(minutes=-200), "-200") + args = [1, 2, 3, 4, 5, 6, 7, z100] + base = cls(*args) + assert_equal(base, base.replace()) + + i = 0 + for name, newval in (("year", 2), + ("month", 3), + ("day", 4), + ("hour", 5), + ("minute", 6), + ("second", 7), + ("microsecond", 8), + ("tzinfo", zm200)): + newargs = args[:] + newargs[i] = newval + expected = cls(*newargs) + got = base.replace(**{name: newval}) + assert_equal(expected, got) + i += 1 + + # Ensure we can get rid of a tzinfo. + assert_equal(base.tzname(), "+100") + base2 = base.replace(tzinfo=None) + self.assertIsNone(base2.tzinfo) + self.assertIsNone(base2.tzname()) + + # Ensure we can add one. + base3 = base2.replace(tzinfo=z100) + assert_equal(base, base3) + self.assertIs(base.tzinfo, base3.tzinfo) + + # Out of bounds. + base = cls(2000, 2, 29) + assertRaises(ValueError, base.replace, year=2001) + + def test_more_astimezone(self): + # The inherited test_astimezone covered some trivial and error cases. + fnone = FixedOffset(None, "None") + f44m = FixedOffset(44, "44") + fm5h = FixedOffset(-timedelta(hours=5), "m300") + + dt = self.theclass.now(tz=f44m) + self.assertIs(dt.tzinfo, f44m) + # Replacing with degenerate tzinfo raises an exception. + assertRaises(ValueError, dt.astimezone, fnone) + # Replacing with same tzinfo makes no change. + x = dt.astimezone(dt.tzinfo) + self.assertIs(x.tzinfo, f44m) + assert_equal(x.date(), dt.date()) + assert_equal(x.time(), dt.time()) + + # Replacing with different tzinfo does adjust. + got = dt.astimezone(fm5h) + self.assertIs(got.tzinfo, fm5h) + assert_equal(got.utcoffset(), timedelta(hours=-5)) + expected = dt - dt.utcoffset() # in effect, convert to UTC + expected += fm5h.utcoffset(dt) # and from there to local time + expected = expected.replace(tzinfo=fm5h) # and attach new tzinfo + assert_equal(got.date(), expected.date()) + assert_equal(got.time(), expected.time()) + assert_equal(got.timetz(), expected.timetz()) + self.assertIs(got.tzinfo, expected.tzinfo) + assert_equal(got, expected) + + @support.run_with_tz('UTC') + def test_astimezone_default_utc(self): + dt = self.theclass.now(timezone.utc) + assert_equal(dt.astimezone(None), dt) + assert_equal(dt.astimezone(), dt) + + # Note that offset in TZ variable has the opposite sign to that + # produced by %z directive. + @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0') + def test_astimezone_default_eastern(self): + dt = self.theclass(2012, 11, 4, 6, 30, tzinfo=timezone.utc) + local = dt.astimezone() + assert_equal(dt, local) + assert_equal(local.strftime("%z %Z"), "-0500 EST") + dt = self.theclass(2012, 11, 4, 5, 30, tzinfo=timezone.utc) + local = dt.astimezone() + assert_equal(dt, local) + assert_equal(local.strftime("%z %Z"), "-0400 EDT") + + def test_aware_subtract(self): + cls = self.theclass + + # Ensure that utcoffset() is ignored when the operands have the + # same tzinfo member. + class OperandDependentOffset(tzinfo): + def utcoffset(self, t): + if t.minute < 10: + # d0 and d1 equal after adjustment + return timedelta(minutes=t.minute) + else: + # d2 off in the weeds + return timedelta(minutes=59) + + base = cls(8, 9, 10, 11, 12, 13, 14, tzinfo=OperandDependentOffset()) + d0 = base.replace(minute=3) + d1 = base.replace(minute=9) + d2 = base.replace(minute=11) + for x in d0, d1, d2: + for y in d0, d1, d2: + got = x - y + expected = timedelta(minutes=x.minute - y.minute) + assert_equal(got, expected) + + # OTOH, if the tzinfo members are distinct, utcoffsets aren't + # ignored. + base = cls(8, 9, 10, 11, 12, 13, 14) + d0 = base.replace(minute=3, tzinfo=OperandDependentOffset()) + d1 = base.replace(minute=9, tzinfo=OperandDependentOffset()) + d2 = base.replace(minute=11, tzinfo=OperandDependentOffset()) + for x in d0, d1, d2: + for y in d0, d1, d2: + got = x - y + if (x is d0 or x is d1) and (y is d0 or y is d1): + expected = timedelta(0) + elif x is y is d2: + expected = timedelta(0) + elif x is d2: + expected = timedelta(minutes=(11-59)-0) + else: + assert y is d2 + expected = timedelta(minutes=0-(11-59)) + assert_equal(got, expected) + + def test_mixed_compare(self): + t1 = datetime(1, 2, 3, 4, 5, 6, 7) + t2 = datetime(1, 2, 3, 4, 5, 6, 7) + assert_equal(t1, t2) + t2 = t2.replace(tzinfo=None) + assert_equal(t1, t2) + t2 = t2.replace(tzinfo=FixedOffset(None, "")) + assert_equal(t1, t2) + t2 = t2.replace(tzinfo=FixedOffset(0, "")) + self.assertNotEqual(t1, t2) + + # In datetime w/ identical tzinfo objects, utcoffset is ignored. + class Varies(tzinfo): + def __init__(self): + self.offset = timedelta(minutes=22) + def utcoffset(self, t): + self.offset += timedelta(minutes=1) + return self.offset + + v = Varies() + t1 = t2.replace(tzinfo=v) + t2 = t2.replace(tzinfo=v) + assert_equal(t1.utcoffset(), timedelta(minutes=23)) + assert_equal(t2.utcoffset(), timedelta(minutes=24)) + assert_equal(t1, t2) + + # But if they're not identical, it isn't ignored. + t2 = t2.replace(tzinfo=Varies()) + assert_true(t1 < t2) # t1's offset counter still going up + + def test_subclass_datetimetz(self): + + class C(self.theclass): + theAnswer = 42 + + def __new__(cls, *args, **kws): + temp = kws.copy() + extra = temp.pop('extra') + result = self.theclass.__new__(cls, *args, **temp) + result.extra = extra + return result + + def newmeth(self, start): + return start + self.hour + self.year + + args = 2002, 12, 31, 4, 5, 6, 500, FixedOffset(-300, "EST", 1) + + dt1 = self.theclass(*args) + dt2 = C(*args, **{'extra': 7}) + + assert_equal(dt2.__class__, C) + assert_equal(dt2.theAnswer, 42) + assert_equal(dt2.extra, 7) + assert_equal(dt1.utcoffset(), dt2.utcoffset()) + assert_equal(dt2.newmeth(-7), dt1.hour + dt1.year - 7) + +# Pain to set up DST-aware tzinfo classes. + +def first_sunday_on_or_after(dt): + days_to_go = 6 - dt.weekday() + if days_to_go: + dt += timedelta(days_to_go) + return dt + +ZERO = timedelta(0) +MINUTE = timedelta(minutes=1) +HOUR = timedelta(hours=1) +DAY = timedelta(days=1) +# In the US, DST starts at 2am (standard time) on the first Sunday in April. +DSTSTART = datetime(1, 4, 1, 2) +# and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct, +# which is the first Sunday on or after Oct 25. Because we view 1:MM as +# being standard time on that day, there is no spelling in local time of +# the last hour of DST (that's 1:MM DST, but 1:MM is taken as standard time). +DSTEND = datetime(1, 10, 25, 1) + +class USTimeZone(tzinfo): + + 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. + if start <= dt.replace(tzinfo=None) < end: + return HOUR + else: + return ZERO + +Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") +Central = USTimeZone(-6, "Central", "CST", "CDT") +Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") +Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") +utc_real = FixedOffset(0, "UTC", 0) +# For better test coverage, we want another flavor of UTC that's west of +# the Eastern and Pacific timezones. +utc_fake = FixedOffset(-12*60, "UTCfake", 0) + +class TestTimezoneConversions(unittest.TestCase): + # The DST switch times for 2002, in std time. + dston = datetime(2002, 4, 7, 2) + dstoff = datetime(2002, 10, 27, 1) + + theclass = datetime + + # Check a time that's inside DST. + def checkinside(self, dt, tz, utc, dston, dstoff): + assert_equal(dt.dst(), HOUR) + + # Conversion to our own timezone is always an identity. + assert_equal(dt.astimezone(tz), dt) + + asutc = dt.astimezone(utc) + there_and_back = asutc.astimezone(tz) + + # Conversion to UTC and back isn't always an identity here, + # because there are redundant spellings (in local time) of + # UTC time when DST begins: the clock jumps from 1:59:59 + # to 3:00:00, and a local time of 2:MM:SS doesn't really + # make sense then. The classes above treat 2:MM:SS as + # daylight time then (it's "after 2am"), really an alias + # for 1:MM:SS standard time. The latter form is what + # conversion back from UTC produces. + if dt.date() == dston.date() and dt.hour == 2: + # We're in the redundant hour, and coming back from + # UTC gives the 1:MM:SS standard-time spelling. + assert_equal(there_and_back + HOUR, dt) + # Although during was considered to be in daylight + # time, there_and_back is not. + assert_equal(there_and_back.dst(), ZERO) + # They're the same times in UTC. + assert_equal(there_and_back.astimezone(utc), + dt.astimezone(utc)) + else: + # We're not in the redundant hour. + assert_equal(dt, there_and_back) + + # Because we have a redundant spelling when DST begins, there is + # (unfortunately) an hour when DST ends that can't be spelled at all in + # local time. When DST ends, the clock jumps from 1:59 back to 1:00 + # again. The hour 1:MM DST has no spelling then: 1:MM is taken to be + # standard time. 1:MM DST == 0:MM EST, but 0:MM is taken to be + # daylight time. The hour 1:MM daylight == 0:MM standard can't be + # expressed in local time. Nevertheless, we want conversion back + # from UTC to mimic the local clock's "repeat an hour" behavior. + nexthour_utc = asutc + HOUR + nexthour_tz = nexthour_utc.astimezone(tz) + if dt.date() == dstoff.date() and dt.hour == 0: + # We're in the hour before the last DST hour. The last DST hour + # is ineffable. We want the conversion back to repeat 1:MM. + assert_equal(nexthour_tz, dt.replace(hour=1)) + nexthour_utc += HOUR + nexthour_tz = nexthour_utc.astimezone(tz) + assert_equal(nexthour_tz, dt.replace(hour=1)) + else: + assert_equal(nexthour_tz - dt, HOUR) + + # Check a time that's outside DST. + def checkoutside(self, dt, tz, utc): + assert_equal(dt.dst(), ZERO) + + # Conversion to our own timezone is always an identity. + assert_equal(dt.astimezone(tz), dt) + + # Converting to UTC and back is an identity too. + asutc = dt.astimezone(utc) + there_and_back = asutc.astimezone(tz) + assert_equal(dt, there_and_back) + + def convert_between_tz_and_utc(self, tz, utc): + dston = self.dston.replace(tzinfo=tz) + # Because 1:MM on the day DST ends is taken as being standard time, + # there is no spelling in tz for the last hour of daylight time. + # For purposes of the test, the last hour of DST is 0:MM, which is + # taken as being daylight time (and 1:MM is taken as being standard + # time). + dstoff = self.dstoff.replace(tzinfo=tz) + for delta in (timedelta(weeks=13), + DAY, + HOUR, + timedelta(minutes=1), + timedelta(microseconds=1)): + + self.checkinside(dston, tz, utc, dston, dstoff) + for during in dston + delta, dstoff - delta: + self.checkinside(during, tz, utc, dston, dstoff) + + self.checkoutside(dstoff, tz, utc) + for outside in dston - delta, dstoff + delta: + self.checkoutside(outside, tz, utc) + + def test_easy(self): + # Despite the name of this test, the endcases are excruciating. + self.convert_between_tz_and_utc(Eastern, utc_real) + self.convert_between_tz_and_utc(Pacific, utc_real) + self.convert_between_tz_and_utc(Eastern, utc_fake) + self.convert_between_tz_and_utc(Pacific, utc_fake) + # The next is really dancing near the edge. It works because + # Pacific and Eastern are far enough apart that their "problem + # hours" don't overlap. + self.convert_between_tz_and_utc(Eastern, Pacific) + self.convert_between_tz_and_utc(Pacific, Eastern) + # OTOH, these fail! Don't enable them. The difficulty is that + # the edge case tests assume that every hour is representable in + # the "utc" class. This is always true for a fixed-offset tzinfo + # class (lke utc_real and utc_fake), but not for Eastern or Central. + # For these adjacent DST-aware time zones, the range of time offsets + # tested ends up creating hours in the one that aren't representable + # in the other. For the same reason, we would see failures in the + # Eastern vs Pacific tests too if we added 3*HOUR to the list of + # offset deltas in convert_between_tz_and_utc(). + # + # self.convert_between_tz_and_utc(Eastern, Central) # can't work + # self.convert_between_tz_and_utc(Central, Eastern) # can't work + + def test_tricky(self): + # 22:00 on day before daylight starts. + fourback = self.dston - timedelta(hours=4) + ninewest = FixedOffset(-9*60, "-0900", 0) + fourback = fourback.replace(tzinfo=ninewest) + # 22:00-0900 is 7:00 UTC == 2:00 EST == 3:00 DST. Since it's "after + # 2", we should get the 3 spelling. + # If we plug 22:00 the day before into Eastern, it "looks like std + # time", so its offset is returned as -5, and -5 - -9 = 4. Adding 4 + # to 22:00 lands on 2:00, which makes no sense in local time (the + # local clock jumps from 1 to 3). The point here is to make sure we + # get the 3 spelling. + expected = self.dston.replace(hour=3) + got = fourback.astimezone(Eastern).replace(tzinfo=None) + assert_equal(expected, got) + + # Similar, but map to 6:00 UTC == 1:00 EST == 2:00 DST. In that + # case we want the 1:00 spelling. + sixutc = self.dston.replace(hour=6, tzinfo=utc_real) + # Now 6:00 "looks like daylight", so the offset wrt Eastern is -4, + # and adding -4-0 == -4 gives the 2:00 spelling. We want the 1:00 EST + # spelling. + expected = self.dston.replace(hour=1) + got = sixutc.astimezone(Eastern).replace(tzinfo=None) + assert_equal(expected, got) + + # Now on the day DST ends, we want "repeat an hour" behavior. + # UTC 4:MM 5:MM 6:MM 7:MM checking these + # EST 23:MM 0:MM 1:MM 2:MM + # EDT 0:MM 1:MM 2:MM 3:MM + # wall 0:MM 1:MM 1:MM 2:MM against these + for utc in utc_real, utc_fake: + for tz in Eastern, Pacific: + first_std_hour = self.dstoff - timedelta(hours=2) # 23:MM + # Convert that to UTC. + first_std_hour -= tz.utcoffset(None) + # Adjust for possibly fake UTC. + asutc = first_std_hour + utc.utcoffset(None) + # First UTC hour to convert; this is 4:00 when utc=utc_real & + # tz=Eastern. + asutcbase = asutc.replace(tzinfo=utc) + for tzhour in (0, 1, 1, 2): + expectedbase = self.dstoff.replace(hour=tzhour) + for minute in 0, 30, 59: + expected = expectedbase.replace(minute=minute) + asutc = asutcbase.replace(minute=minute) + astz = asutc.astimezone(tz) + assert_equal(astz.replace(tzinfo=None), expected) + asutcbase += HOUR + + + def test_bogus_dst(self): + class ok(tzinfo): + def utcoffset(self, dt): return HOUR + def dst(self, dt): return HOUR + + now = self.theclass.now().replace(tzinfo=utc_real) + # Doesn't blow up. + now.astimezone(ok()) + + # Does blow up. + class notok(ok): + def dst(self, dt): return None + assertRaises(ValueError, now.astimezone, notok()) + + # Sometimes blow up. In the following, tzinfo.dst() + # implementation may return None or not None depending on + # whether DST is assumed to be in effect. In this situation, + # a ValueError should be raised by astimezone(). + class tricky_notok(ok): + def dst(self, dt): + if dt.year == 2000: + return None + else: + return 10*HOUR + dt = self.theclass(2001, 1, 1).replace(tzinfo=utc_real) + assertRaises(ValueError, dt.astimezone, tricky_notok()) + + def test_fromutc(self): + assertRaises(TypeError, Eastern.fromutc) # not enough args + now = datetime.utcnow().replace(tzinfo=utc_real) + assertRaises(ValueError, Eastern.fromutc, now) # wrong tzinfo + now = now.replace(tzinfo=Eastern) # insert correct tzinfo + enow = Eastern.fromutc(now) # doesn't blow up + assert_equal(enow.tzinfo, Eastern) # has right tzinfo member + assertRaises(TypeError, Eastern.fromutc, now, now) # too many args + assertRaises(TypeError, Eastern.fromutc, date.today()) # wrong type + + # Always converts UTC to standard time. + class FauxUSTimeZone(USTimeZone): + def fromutc(self, dt): + return dt + self.stdoffset + FEastern = FauxUSTimeZone(-5, "FEastern", "FEST", "FEDT") + + # UTC 4:MM 5:MM 6:MM 7:MM 8:MM 9:MM + # EST 23:MM 0:MM 1:MM 2:MM 3:MM 4:MM + # EDT 0:MM 1:MM 2:MM 3:MM 4:MM 5:MM + + # Check around DST start. + start = self.dston.replace(hour=4, tzinfo=Eastern) + fstart = start.replace(tzinfo=FEastern) + for wall in 23, 0, 1, 3, 4, 5: + expected = start.replace(hour=wall) + if wall == 23: + expected -= timedelta(days=1) + got = Eastern.fromutc(start) + assert_equal(expected, got) + + expected = fstart + FEastern.stdoffset + got = FEastern.fromutc(fstart) + assert_equal(expected, got) + + # Ensure astimezone() calls fromutc() too. + got = fstart.replace(tzinfo=utc_real).astimezone(FEastern) + assert_equal(expected, got) + + start += HOUR + fstart += HOUR + + # Check around DST end. + start = self.dstoff.replace(hour=4, tzinfo=Eastern) + fstart = start.replace(tzinfo=FEastern) + for wall in 0, 1, 1, 2, 3, 4: + expected = start.replace(hour=wall) + got = Eastern.fromutc(start) + assert_equal(expected, got) + + expected = fstart + FEastern.stdoffset + got = FEastern.fromutc(fstart) + assert_equal(expected, got) + + # Ensure astimezone() calls fromutc() too. + got = fstart.replace(tzinfo=utc_real).astimezone(FEastern) + assert_equal(expected, got) + + start += HOUR + fstart += HOUR + + +############################################################################# +# oddballs + +# class Oddballs(unittest.TestCase): + +# def test_bug_1028306(self): +# Trying to compare a date to a datetime should act like a mixed- +# type comparison, despite that datetime is a subclass of date. +as_date = date.today() +as_datetime = datetime.combine(as_date, time()) +assert_true(as_date != as_datetime) +assert_true(as_datetime != as_date) +assert_false(as_date == as_datetime) +assert_false(as_datetime == as_date) +assertRaises(TypeError, lambda: as_date < as_datetime) +assertRaises(TypeError, lambda: as_datetime < as_date) +assertRaises(TypeError, lambda: as_date <= as_datetime) +assertRaises(TypeError, lambda: as_datetime <= as_date) +assertRaises(TypeError, lambda: as_date > as_datetime) +assertRaises(TypeError, lambda: as_datetime > as_date) +assertRaises(TypeError, lambda: as_date >= as_datetime) +assertRaises(TypeError, lambda: as_datetime >= as_date) + +# Neverthelss, comparison should work with the base-class (date) +# projection if use of a date method is forced. +assert_equal(as_date.__eq__(as_datetime), True) +different_day = (as_date.day + 1) % 20 + 1 +as_different = as_datetime.replace(day= different_day) +assert_equal(as_date.__eq__(as_different), False) + +# And date should compare with other subclasses of date. If a +# subclass wants to stop this, it's up to the subclass to do so. +date_sc = SubclassDate(as_date.year, as_date.month, as_date.day) +assert_equal(as_date, date_sc) +assert_equal(date_sc, as_date) + +# Ditto for datetimes. +datetime_sc = SubclassDatetime(as_datetime.year, as_datetime.month, + as_date.day, 0, 0, 0) +assert_equal(as_datetime, datetime_sc) +assert_equal(datetime_sc, as_datetime) + +''' diff --git a/tests/snippets/testutils.py b/tests/snippets/testutils.py index 21662c4614..468fbb2c85 100644 --- a/tests/snippets/testutils.py +++ b/tests/snippets/testutils.py @@ -20,7 +20,17 @@ def assert_raises(exc_type, expr, msg=None): assert False, failmsg -class assertRaises: +def assertRaises(expected, *args, **kw): + if not args: + assert not kw + return _assertRaises(expected) + else: + f, f_args = args[0], args[1:] + with _assertRaises(expected): + f(*f_args, **kw) + + +class _assertRaises: def __init__(self, expected): self.expected = expected self.exception = None diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index a78a286b85..232758e819 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -95,7 +95,6 @@ fn print_traceback_entry(vm: &VirtualMachine, tb_entry: &PyObjectRef) { print_source_line(filename, lineno.parse().unwrap()); } else { println!(" File ??"); - return; } } diff --git a/vm/src/frame.rs b/vm/src/frame.rs index c8f93d55e9..a43271f86c 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -907,6 +907,9 @@ impl Frame { bytecode::NameScope::Local => { self.scope.store_name(vm, name, obj); } + bytecode::NameScope::Free => { + self.scope.store_name(vm, name, obj); + } } Ok(None) } @@ -928,7 +931,8 @@ impl Frame { let optional_value = match name_scope { bytecode::NameScope::Global => self.scope.load_global(vm, name), bytecode::NameScope::NonLocal => self.scope.load_cell(vm, name), - bytecode::NameScope::Local => self.scope.load_name(&vm, name), + bytecode::NameScope::Local => self.scope.load_local(&vm, name), + bytecode::NameScope::Free => self.scope.load_name(&vm, name), }; let value = match optional_value { diff --git a/vm/src/macros.rs b/vm/src/macros.rs index ea10abfdc4..9388544fa5 100644 --- a/vm/src/macros.rs +++ b/vm/src/macros.rs @@ -273,3 +273,13 @@ macro_rules! flame_guard { let _guard = ::flame::start_guard($name); }; } + +#[macro_export] +macro_rules! class_or_notimplemented { + ($vm:expr, $t:ty, $obj:expr) => { + match $crate::pyobject::PyObject::downcast::<$t>($obj) { + Ok(pyref) => pyref, + Err(_) => return Ok($vm.ctx.not_implemented()), + } + }; +} diff --git a/vm/src/obj/objbyteinner.rs b/vm/src/obj/objbyteinner.rs index 32a58255f8..2fa5920df9 100644 --- a/vm/src/obj/objbyteinner.rs +++ b/vm/src/obj/objbyteinner.rs @@ -100,12 +100,12 @@ impl ByteInnerNewOptions { if let OptionalArg::Present(eval) = self.val_option { if let Ok(input) = eval.downcast::() { let inner = PyByteInner::from_string(&input.value, enc.as_str(), vm)?; - return Ok(inner); + Ok(inner) } else { - return Err(vm.new_type_error("encoding without a string argument".to_string())); + Err(vm.new_type_error("encoding without a string argument".to_string())) } } else { - return Err(vm.new_type_error("encoding without a string argument".to_string())); + Err(vm.new_type_error("encoding without a string argument".to_string())) } // Only one argument } else { diff --git a/vm/src/obj/objfloat.rs b/vm/src/obj/objfloat.rs index c38fc70413..0a5d5b6f13 100644 --- a/vm/src/obj/objfloat.rs +++ b/vm/src/obj/objfloat.rs @@ -40,6 +40,11 @@ impl IntoPyObject for f64 { Ok(vm.ctx.new_float(self)) } } +impl IntoPyObject for f32 { + fn into_pyobject(self, vm: &VirtualMachine) -> PyResult { + Ok(vm.ctx.new_float(f64::from(self))) + } +} impl TryFromObject for f64 { fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { @@ -66,6 +71,18 @@ pub fn try_float(value: &PyObjectRef, vm: &VirtualMachine) -> PyResult { + $(impl TryFromObject for $t { + fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { + PyFloatRef::try_from_object(vm, obj).map(|f| f.to_f64() as $t) + } + })* + }; +} + +impl_try_from_object_float!(f32, f64); + fn inner_div(v1: f64, v2: f64, vm: &VirtualMachine) -> PyResult { if v2 != 0.0 { Ok(v1 / v2) diff --git a/vm/src/obj/objint.rs b/vm/src/obj/objint.rs index 296f458669..7a478fae91 100644 --- a/vm/src/obj/objint.rs +++ b/vm/src/obj/objint.rs @@ -1,8 +1,9 @@ use std::fmt; +use std::str; use num_bigint::{BigInt, Sign}; use num_integer::Integer; -use num_traits::{One, Pow, Signed, ToPrimitive, Zero}; +use num_traits::{Num, One, Pow, Signed, ToPrimitive, Zero}; use crate::format::FormatSpec; use crate::function::{KwArgs, OptionalArg, PyFuncArgs}; @@ -713,7 +714,9 @@ impl IntOptions { fn get_int_value(self, vm: &VirtualMachine) -> PyResult { if let OptionalArg::Present(val) = self.val_options { let base = if let OptionalArg::Present(base) = self.base { - if !objtype::isinstance(&val, &vm.ctx.str_type()) { + if !(objtype::isinstance(&val, &vm.ctx.str_type()) + || objtype::isinstance(&val, &vm.ctx.bytes_type())) + { return Err(vm.new_type_error( "int() can't convert non-string with explicit base".to_string(), )); @@ -736,21 +739,22 @@ fn int_new(cls: PyClassRef, options: IntOptions, vm: &VirtualMachine) -> PyResul } // Casting function: -pub fn to_int(vm: &VirtualMachine, obj: &PyObjectRef, mut base: u32) -> PyResult { - if base == 0 { - base = 10 - } else if base < 2 || base > 36 { +pub fn to_int(vm: &VirtualMachine, obj: &PyObjectRef, base: u32) -> PyResult { + if base != 0 && (base < 2 || base > 36) { return Err(vm.new_value_error("int() base must be >= 2 and <= 36, or 0".to_string())); } match_class!(obj.clone(), - s @ PyString => { - i32::from_str_radix(s.as_str().trim(), base) - .map(BigInt::from) - .map_err(|_|vm.new_value_error(format!( - "invalid literal for int() with base {}: '{}'", - base, s - ))) + string @ PyString => { + let s = string.value.as_str().trim(); + str_to_int(vm, s, base) + }, + bytes @ PyBytes => { + let bytes = bytes.get_value(); + let s = std::str::from_utf8(bytes) + .map(|s| s.trim()) + .map_err(|e| vm.new_value_error(format!("utf8 decode error: {}", e)))?; + str_to_int(vm, s, base) }, obj => { let method = vm.get_method_or_type_error(obj.clone(), "__int__", || { @@ -766,6 +770,76 @@ pub fn to_int(vm: &VirtualMachine, obj: &PyObjectRef, mut base: u32) -> PyResult ) } +fn str_to_int(vm: &VirtualMachine, literal: &str, mut base: u32) -> PyResult { + let mut buf = validate_literal(vm, literal, base)?; + let is_signed = buf.starts_with('+') || buf.starts_with('-'); + let radix_range = if is_signed { 1..3 } else { 0..2 }; + let radix_candidate = buf.get(radix_range.clone()); + + // try to find base + if let Some(radix_candidate) = radix_candidate { + if let Some(matched_radix) = detect_base(&radix_candidate) { + if base != 0 && base != matched_radix { + return Err(invalid_literal(vm, literal, base)); + } else { + base = matched_radix; + } + + buf.drain(radix_range); + } + } + + // base still not found, try to use default + if base == 0 { + if buf.starts_with('0') { + return Err(invalid_literal(vm, literal, base)); + } + + base = 10; + } + + BigInt::from_str_radix(&buf, base).map_err(|_err| invalid_literal(vm, literal, base)) +} + +fn validate_literal(vm: &VirtualMachine, literal: &str, base: u32) -> PyResult { + if literal.starts_with('_') || literal.ends_with('_') { + return Err(invalid_literal(vm, literal, base)); + } + + let mut buf = String::with_capacity(literal.len()); + let mut last_tok = None; + for c in literal.chars() { + if !(c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-') { + return Err(invalid_literal(vm, literal, base)); + } + + if c == '_' && Some(c) == last_tok { + return Err(invalid_literal(vm, literal, base)); + } + + last_tok = Some(c); + buf.push(c); + } + + Ok(buf) +} + +fn detect_base(literal: &str) -> Option { + match literal { + "0x" | "0X" => Some(16), + "0o" | "0O" => Some(8), + "0b" | "0B" => Some(2), + _ => None, + } +} + +fn invalid_literal(vm: &VirtualMachine, literal: &str, base: u32) -> PyObjectRef { + vm.new_value_error(format!( + "invalid literal for int() with base {}: '{}'", + base, literal + )) +} + // Retrieve inner int value: pub fn get_value(obj: &PyObjectRef) -> &BigInt { &get_py_int(obj).value diff --git a/vm/src/obj/objsequence.rs b/vm/src/obj/objsequence.rs index 4410192ce5..15e3fb655b 100644 --- a/vm/src/obj/objsequence.rs +++ b/vm/src/obj/objsequence.rs @@ -471,7 +471,9 @@ pub fn is_valid_slice_arg( match_class!(value, i @ PyInt => Ok(Some(i.as_bigint().clone())), _obj @ PyNone => Ok(None), - _=> {return Err(vm.new_type_error("slice indices must be integers or None or have an __index__ method".to_string()));} + _=> { + Err(vm.new_type_error("slice indices must be integers or None or have an __index__ method".to_string())) + } // TODO: check for an __index__ method ) } else { diff --git a/vm/src/obj/objstr.rs b/vm/src/obj/objstr.rs index e7c572aef7..b8b973c86a 100644 --- a/vm/src/obj/objstr.rs +++ b/vm/src/obj/objstr.rs @@ -349,21 +349,35 @@ impl PyString { fn split( &self, pattern: OptionalArg, - num: OptionalArg, + num: OptionalArg, vm: &VirtualMachine, ) -> PyObjectRef { let value = &self.value; let pattern = match pattern { - OptionalArg::Present(ref s) => &s.value, - OptionalArg::Missing => " ", + OptionalArg::Present(ref s) => Some(s.as_str()), + OptionalArg::Missing => None, + }; + let num_splits = num.into_option().unwrap_or(-1); + let elements: Vec<_> = match (pattern, num_splits.is_negative()) { + (Some(pattern), true) => value + .split(pattern) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), + (Some(pattern), false) => value + .splitn(num_splits as usize + 1, pattern) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), + (None, true) => value + .split(|c: char| c.is_ascii_whitespace()) + .filter(|s| !s.is_empty()) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), + (None, false) => value + .splitn(num_splits as usize + 1, |c: char| c.is_ascii_whitespace()) + .filter(|s| !s.is_empty()) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), }; - let num_splits = num - .into_option() - .unwrap_or_else(|| value.split(pattern).count()); - let elements = value - .splitn(num_splits + 1, pattern) - .map(|o| vm.ctx.new_str(o.to_string())) - .collect(); vm.ctx.new_list(elements) } @@ -371,21 +385,35 @@ impl PyString { fn rsplit( &self, pattern: OptionalArg, - num: OptionalArg, + num: OptionalArg, vm: &VirtualMachine, ) -> PyObjectRef { let value = &self.value; let pattern = match pattern { - OptionalArg::Present(ref s) => &s.value, - OptionalArg::Missing => " ", + OptionalArg::Present(ref s) => Some(s.as_str()), + OptionalArg::Missing => None, + }; + let num_splits = num.into_option().unwrap_or(-1); + let mut elements: Vec<_> = match (pattern, num_splits.is_negative()) { + (Some(pattern), true) => value + .rsplit(pattern) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), + (Some(pattern), false) => value + .rsplitn(num_splits as usize + 1, pattern) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), + (None, true) => value + .rsplit(|c: char| c.is_ascii_whitespace()) + .filter(|s| !s.is_empty()) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), + (None, false) => value + .rsplitn(num_splits as usize + 1, |c: char| c.is_ascii_whitespace()) + .filter(|s| !s.is_empty()) + .map(|o| vm.ctx.new_str(o.to_string())) + .collect(), }; - let num_splits = num - .into_option() - .unwrap_or_else(|| value.split(pattern).count()); - let mut elements: Vec<_> = value - .rsplitn(num_splits + 1, pattern) - .map(|o| vm.ctx.new_str(o.to_string())) - .collect(); // Unlike Python rsplit, Rust rsplitn returns an iterator that // starts from the end of the string. elements.reverse(); diff --git a/vm/src/obj/objtype.rs b/vm/src/obj/objtype.rs index 68b2feba60..b88fccb009 100644 --- a/vm/src/obj/objtype.rs +++ b/vm/src/obj/objtype.rs @@ -312,6 +312,8 @@ fn type_dict_setter(_instance: PyClassRef, _value: PyObjectRef, vm: &VirtualMach /// This is the internal get_attr implementation for fast lookup on a class. pub fn class_get_attr(class: &PyClassRef, attr_name: &str) -> Option { + flame_guard!(format!("class_get_attr({:?})", attr_name)); + if let Some(item) = class.attributes.borrow().get(attr_name).cloned() { return Some(item); } diff --git a/vm/src/scope.rs b/vm/src/scope.rs index c451e81061..3ba068f6e5 100644 --- a/vm/src/scope.rs +++ b/vm/src/scope.rs @@ -123,6 +123,7 @@ pub trait NameProtocol { fn load_name(&self, vm: &VirtualMachine, name: &str) -> Option; fn store_name(&self, vm: &VirtualMachine, name: &str, value: PyObjectRef); fn delete_name(&self, vm: &VirtualMachine, name: &str) -> PyResult; + fn load_local(&self, vm: &VirtualMachine, name: &str) -> Option; fn load_cell(&self, vm: &VirtualMachine, name: &str) -> Option; fn store_cell(&self, vm: &VirtualMachine, name: &str, value: PyObjectRef); fn load_global(&self, vm: &VirtualMachine, name: &str) -> Option; @@ -142,6 +143,12 @@ impl NameProtocol for Scope { self.load_global(vm, name) } + #[cfg_attr(feature = "flame-it", flame("Scope"))] + /// Load a local name. Only check the local dictionary for the given name. + fn load_local(&self, vm: &VirtualMachine, name: &str) -> Option { + self.get_locals().get_item_option(name, vm).unwrap() + } + #[cfg_attr(feature = "flame-it", flame("Scope"))] fn load_cell(&self, vm: &VirtualMachine, name: &str) -> Option { for dict in self.locals.iter().skip(1) { @@ -170,7 +177,17 @@ impl NameProtocol for Scope { } #[cfg_attr(feature = "flame-it", flame("Scope"))] + /// Load a global name. fn load_global(&self, vm: &VirtualMachine, name: &str) -> Option { + // First, take a look in the outmost local scope (the scope at top level) + let last_local_dict = self.locals.iter().last(); + if let Some(local_dict) = last_local_dict { + if let Some(value) = local_dict.get_item_option(name, vm).unwrap() { + return Some(value); + } + } + + // Now, take a look at the globals or builtins. if let Some(value) = self.globals.get_item_option(name, vm).unwrap() { Some(value) } else { diff --git a/vm/src/stdlib/array.rs b/vm/src/stdlib/array.rs new file mode 100644 index 0000000000..6654a4447d --- /dev/null +++ b/vm/src/stdlib/array.rs @@ -0,0 +1,392 @@ +use crate::function::OptionalArg; +use crate::obj::objbytes::PyBytesRef; +use crate::obj::objstr::PyStringRef; +use crate::obj::objtype::PyClassRef; +use crate::obj::{objbool, objiter}; +use crate::pyobject::{ + IntoPyObject, PyClassImpl, PyIterable, PyObjectRef, PyRef, PyResult, PyValue, TryFromObject, +}; +use crate::VirtualMachine; + +use std::cell::{Cell, RefCell}; +use std::fmt; + +struct ArrayTypeSpecifierError { + _priv: (), +} + +impl fmt::Display for ArrayTypeSpecifierError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "bad typecode (must be b, B, u, h, H, i, I, l, L, q, Q, f or d)" + ) + } +} + +macro_rules! def_array_enum { + ($(($n:ident, $t:ident, $c:literal)),*$(,)?) => { + #[derive(Debug)] + enum ArrayContentType { + $($n(Vec<$t>),)* + } + + #[allow(clippy::naive_bytecount, clippy::float_cmp)] + impl ArrayContentType { + fn from_char(c: char) -> Result { + match c { + $($c => Ok(ArrayContentType::$n(Vec::new())),)* + _ => Err(ArrayTypeSpecifierError { _priv: () }), + } + } + + fn typecode(&self) -> char { + match self { + $(ArrayContentType::$n(_) => $c,)* + } + } + + fn itemsize(&self) -> usize { + match self { + $(ArrayContentType::$n(_) => std::mem::size_of::<$t>(),)* + } + } + + fn addr(&self) -> usize { + match self { + $(ArrayContentType::$n(v) => v.as_ptr() as usize,)* + } + } + + fn len(&self) -> usize { + match self { + $(ArrayContentType::$n(v) => v.len(),)* + } + } + + fn push(&mut self, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + match self { + $(ArrayContentType::$n(v) => { + let val = $t::try_from_object(vm, obj)?; + v.push(val); + })* + } + Ok(()) + } + + fn pop(&mut self, i: usize, vm: &VirtualMachine) -> PyResult { + match self { + $(ArrayContentType::$n(v) => { + v.remove(i).into_pyobject(vm) + })* + } + } + + fn insert(&mut self, i: usize, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + match self { + $(ArrayContentType::$n(v) => { + let val = $t::try_from_object(vm, obj)?; + v.insert(i, val); + })* + } + Ok(()) + } + + fn count(&self, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult { + match self { + $(ArrayContentType::$n(v) => { + let val = $t::try_from_object(vm, obj)?; + Ok(v.iter().filter(|&&a| a == val).count()) + })* + } + } + + fn frombytes(&mut self, b: &[u8]) { + match self { + $(ArrayContentType::$n(v) => { + // safe because every configuration of bytes for the types we + // support are valid + let ptr = b.as_ptr() as *const $t; + let ptr_len = b.len() / std::mem::size_of::<$t>(); + let slice = unsafe { std::slice::from_raw_parts(ptr, ptr_len) }; + v.extend_from_slice(slice); + })* + } + } + + fn tobytes(&self) -> Vec { + match self { + $(ArrayContentType::$n(v) => { + // safe because we're just reading memory as bytes + let ptr = v.as_ptr() as *const u8; + let ptr_len = v.len() * std::mem::size_of::<$t>(); + let slice = unsafe { std::slice::from_raw_parts(ptr, ptr_len) }; + slice.to_vec() + })* + } + } + + fn index(&self, x: PyObjectRef, vm: &VirtualMachine) -> PyResult> { + match self { + $(ArrayContentType::$n(v) => { + let val = $t::try_from_object(vm, x)?; + Ok(v.iter().position(|&a| a == val)) + })* + } + } + + fn reverse(&mut self) { + match self { + $(ArrayContentType::$n(v) => v.reverse(),)* + } + } + + fn getitem(&self, i: usize, vm: &VirtualMachine) -> Option { + match self { + $(ArrayContentType::$n(v) => v.get(i).map(|x| x.into_pyobject(vm)),)* + } + } + + fn iter<'a>(&'a self, vm: &'a VirtualMachine) -> impl Iterator + 'a { + let mut i = 0; + std::iter::from_fn(move || { + let ret = self.getitem(i, vm); + i += 1; + ret + }) + } + } + }; +} + +def_array_enum!( + (SignedByte, i8, 'b'), + (UnsignedByte, u8, 'B'), + // TODO: support unicode char + (SignedShort, i16, 'h'), + (UnsignedShort, u16, 'H'), + (SignedInt, i16, 'i'), + (UnsignedInt, u16, 'I'), + (SignedLong, i32, 'l'), + (UnsignedLong, u32, 'L'), + (SignedLongLong, i64, 'q'), + (UnsignedLongLong, u64, 'Q'), + (Float, f32, 'f'), + (Double, f64, 'd'), +); + +#[pyclass] +#[derive(Debug)] +pub struct PyArray { + array: RefCell, +} +pub type PyArrayRef = PyRef; + +impl PyValue for PyArray { + fn class(vm: &VirtualMachine) -> PyClassRef { + vm.class("array", "array") + } +} + +#[pyimpl] +impl PyArray { + #[pymethod(name = "__new__")] + fn new( + cls: PyClassRef, + spec: PyStringRef, + init: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + let spec = match spec.as_str().len() { + 1 => spec.as_str().chars().next().unwrap(), + _ => { + return Err(vm.new_type_error( + "array() argument 1 must be a unicode character, not str".to_owned(), + )) + } + }; + let array = + ArrayContentType::from_char(spec).map_err(|err| vm.new_value_error(err.to_string()))?; + let zelf = PyArray { + array: RefCell::new(array), + }; + if let OptionalArg::Present(init) = init { + zelf.extend(init, vm)?; + } + zelf.into_ref_with_type(vm, cls) + } + + #[pyproperty] + fn typecode(&self, _vm: &VirtualMachine) -> String { + self.array.borrow().typecode().to_string() + } + + #[pyproperty] + fn itemsize(&self, _vm: &VirtualMachine) -> usize { + self.array.borrow().itemsize() + } + + #[pymethod] + fn append(&self, x: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + self.array.borrow_mut().push(x, vm) + } + + #[pymethod] + fn buffer_info(&self, _vm: &VirtualMachine) -> (usize, usize) { + let array = self.array.borrow(); + (array.addr(), array.len()) + } + + #[pymethod] + fn count(&self, x: PyObjectRef, vm: &VirtualMachine) -> PyResult { + self.array.borrow().count(x, vm) + } + + fn idx(&self, i: isize, vm: &VirtualMachine) -> PyResult { + let len = self.array.borrow().len(); + if len == 0 { + return Err(vm.new_index_error("pop from empty array".to_owned())); + } + let i = if i.is_negative() { + len - i.abs() as usize + } else { + i as usize + }; + if i > len - 1 { + return Err(vm.new_index_error("pop index out of range".to_owned())); + } + Ok(i) + } + + #[pymethod] + fn extend(&self, iter: PyIterable, vm: &VirtualMachine) -> PyResult<()> { + let mut array = self.array.borrow_mut(); + for elem in iter.iter(vm)? { + array.push(elem?, vm)?; + } + Ok(()) + } + + #[pymethod] + fn frombytes(&self, b: PyBytesRef, vm: &VirtualMachine) -> PyResult<()> { + let b = b.get_value(); + let itemsize = self.array.borrow().itemsize(); + if b.len() % itemsize != 0 { + return Err(vm.new_value_error("bytes length not a multiple of item size".to_owned())); + } + if b.len() / itemsize > 0 { + self.array.borrow_mut().frombytes(&b); + } + Ok(()) + } + + #[pymethod] + fn index(&self, x: PyObjectRef, vm: &VirtualMachine) -> PyResult { + self.array + .borrow() + .index(x, vm)? + .ok_or_else(|| vm.new_value_error("x not in array".to_owned())) + } + + #[pymethod] + fn insert(&self, i: isize, x: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let i = self.idx(i, vm)?; + self.array.borrow_mut().insert(i, x, vm) + } + + #[pymethod] + fn pop(&self, i: OptionalArg, vm: &VirtualMachine) -> PyResult { + let i = self.idx(i.unwrap_or(-1), vm)?; + self.array.borrow_mut().pop(i, vm) + } + + #[pymethod] + fn tobytes(&self, _vm: &VirtualMachine) -> Vec { + self.array.borrow().tobytes() + } + + #[pymethod] + fn reverse(&self, _vm: &VirtualMachine) { + self.array.borrow_mut().reverse() + } + + #[pymethod(name = "__getitem__")] + fn getitem(&self, i: isize, vm: &VirtualMachine) -> PyResult { + let i = self.idx(i, vm)?; + self.array + .borrow() + .getitem(i, vm) + .unwrap_or_else(|| Err(vm.new_index_error("array index out of range".to_owned()))) + } + + #[pymethod(name = "__eq__")] + fn eq(lhs: PyObjectRef, rhs: PyObjectRef, vm: &VirtualMachine) -> PyResult { + let lhs = class_or_notimplemented!(vm, Self, lhs); + let rhs = class_or_notimplemented!(vm, Self, rhs); + let lhs = lhs.array.borrow(); + let rhs = rhs.array.borrow(); + if lhs.len() != rhs.len() { + Ok(vm.new_bool(false)) + } else { + for (a, b) in lhs.iter(vm).zip(rhs.iter(vm)) { + let ne = objbool::boolval(vm, vm._ne(a?, b?)?)?; + if ne { + return Ok(vm.new_bool(false)); + } + } + Ok(vm.new_bool(true)) + } + } + + #[pymethod(name = "__iter__")] + fn iter(zelf: PyRef, _vm: &VirtualMachine) -> PyArrayIter { + PyArrayIter { + position: Cell::new(0), + array: zelf, + } + } +} + +#[pyclass] +#[derive(Debug)] +pub struct PyArrayIter { + position: Cell, + array: PyArrayRef, +} + +impl PyValue for PyArrayIter { + fn class(vm: &VirtualMachine) -> PyClassRef { + vm.class("array", "arrayiterator") + } +} + +#[pyimpl] +impl PyArrayIter { + #[pymethod(name = "__next__")] + fn next(&self, vm: &VirtualMachine) -> PyResult { + if self.position.get() < self.array.array.borrow().len() { + let ret = self + .array + .array + .borrow() + .getitem(self.position.get(), vm) + .unwrap()?; + self.position.set(self.position.get() + 1); + Ok(ret) + } else { + Err(objiter::new_stop_iteration(vm)) + } + } + + #[pymethod(name = "__iter__")] + fn iter(zelf: PyRef, _vm: &VirtualMachine) -> PyRef { + zelf + } +} + +pub fn make_module(vm: &VirtualMachine) -> PyObjectRef { + py_module!(vm, "array", { + "array" => PyArray::make_class(&vm.ctx), + "arrayiterator" => PyArrayIter::make_class(&vm.ctx), + }) +} diff --git a/vm/src/stdlib/mod.rs b/vm/src/stdlib/mod.rs index 076274dc3f..51eb10bd9e 100644 --- a/vm/src/stdlib/mod.rs +++ b/vm/src/stdlib/mod.rs @@ -1,3 +1,4 @@ +mod array; #[cfg(feature = "rustpython-parser")] mod ast; mod binascii; @@ -54,7 +55,8 @@ pub type StdlibInitFunc = Box PyObjectRef>; pub fn get_module_inits() -> HashMap { #[allow(unused_mut)] let mut modules = hashmap! { - "binascii".to_string() => Box::new(binascii::make_module) as StdlibInitFunc, + "array".to_string() => Box::new(array::make_module) as StdlibInitFunc, + "binascii".to_string() => Box::new(binascii::make_module), "dis".to_string() => Box::new(dis::make_module), "_codecs".to_string() => Box::new(codecs::make_module), "_collections".to_string() => Box::new(collections::make_module), diff --git a/vm/src/stdlib/symtable.rs b/vm/src/stdlib/symtable.rs index b3ba19ffa8..a6cf985046 100644 --- a/vm/src/stdlib/symtable.rs +++ b/vm/src/stdlib/symtable.rs @@ -34,7 +34,7 @@ fn symtable_symtable( let symtable = source_to_symtable(&source.value, mode).map_err(|err| vm.new_syntax_error(&err))?; - let py_symbol_table = to_py_symbol_table("top".to_string(), symtable); + let py_symbol_table = to_py_symbol_table(symtable); Ok(py_symbol_table.into_ref(vm)) } @@ -56,8 +56,8 @@ fn source_to_symtable( Ok(symtable) } -fn to_py_symbol_table(name: String, symtable: symboltable::SymbolTable) -> PySymbolTable { - PySymbolTable { name, symtable } +fn to_py_symbol_table(symtable: symboltable::SymbolTable) -> PySymbolTable { + PySymbolTable { symtable } } type PySymbolTableRef = PyRef; @@ -65,7 +65,6 @@ type PySymbolRef = PyRef; #[pyclass(name = "SymbolTable")] struct PySymbolTable { - name: String, symtable: symboltable::SymbolTable, } @@ -85,7 +84,7 @@ impl PyValue for PySymbolTable { impl PySymbolTable { #[pymethod(name = "get_name")] fn get_name(&self, vm: &VirtualMachine) -> PyResult { - Ok(vm.ctx.new_str(self.name.clone())) + Ok(vm.ctx.new_str(self.symtable.name.clone())) } #[pymethod(name = "lookup")] @@ -118,11 +117,7 @@ impl PySymbolTable { .symtable .sub_tables .iter() - .map(|s| { - to_py_symbol_table("bla".to_string(), s.clone()) - .into_ref(vm) - .into_object() - }) + .map(|t| to_py_symbol_table(t.clone()).into_ref(vm).into_object()) .collect(); Ok(vm.ctx.new_list(children)) } diff --git a/vm/src/vm.rs b/vm/src/vm.rs index 751b0a5828..e3db2d31f1 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm.rs @@ -534,6 +534,8 @@ impl VirtualMachine { where T: Into, { + flame_guard!(format!("call_method({:?})", method_name)); + // This is only used in the vm for magic methods, which use a greatly simplified attribute lookup. let cls = obj.class(); match objtype::class_get_attr(&cls, method_name) { @@ -552,7 +554,6 @@ impl VirtualMachine { } } - #[cfg_attr(feature = "flame-it", flame("VirtualMachine"))] fn _invoke(&self, func_ref: &PyObjectRef, args: PyFuncArgs) -> PyResult { vm_trace!("Invoke: {:?} {:?}", func_ref, args);