diff --git a/python-stdlib/calendar/calendar.py b/python-stdlib/calendar/calendar.py new file mode 100644 index 000000000..e6486a98b --- /dev/null +++ b/python-stdlib/calendar/calendar.py @@ -0,0 +1,683 @@ +"""Calendar printing functions + +Note when comparing these calendars to the ones printed by cal(1): By +default, these calendars have Monday as the first day of the week, and +Sunday as the last (the European convention). Use setfirstweekday() to +set the first day of the week (0=Monday, 6=Sunday).""" + +import sys +import datetime +import locale as _locale +from itertools import repeat + +__all__ = [ + "IllegalMonthError", + "IllegalWeekdayError", + "setfirstweekday", + "firstweekday", + "isleap", + "leapdays", + "weekday", + "monthrange", + "monthcalendar", + "prmonth", + "month", + "prcal", + "calendar", + "timegm", + "month_name", + "month_abbr", + "day_name", + "day_abbr", + "Calendar", + "TextCalendar", + "HTMLCalendar", + "weekheader", + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY", + "error", + "repeat", +] + +# Exception raised for bad input (with string parameter for details) +error = ValueError + +# Exceptions raised for bad input +class IllegalMonthError(ValueError): + def __init__(self, month): + self.month = month + + def __str__(self): + return "bad month number %r; must be 1-12" % self.month + + +class IllegalWeekdayError(ValueError): + def __init__(self, weekday): + self.weekday = weekday + + def __str__(self): + return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday + + +# Constants for months referenced later +January = 1 +February = 2 + +# Number of days per month (except for February in leap years) +mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + +# This module used to have hard-coded lists of day and month names, as +# English strings. The classes following emulate a read-only version of +# that, but supply localized names. Note that the values are computed +# fresh on each call, in case the user changes locale between calls. + + +class _localized_month: + + _months = [datetime.date(2001, i + 1, 1).strftime for i in range(12)] + _months.insert(0, lambda x: "") + + def __init__(self, format): + self.format = format + + def __getitem__(self, i): + funcs = self._months[i] + if isinstance(i, slice): + return [f(self.format) for f in funcs] + else: + return funcs(self.format) + + def __len__(self): + return 13 + + +class _localized_day: + + # January 1, 2001, was a Monday. + _days = [datetime.date(2001, 1, i + 1).strftime for i in range(7)] + + def __init__(self, format): + self.format = format + + def __getitem__(self, i): + funcs = self._days[i] + if isinstance(i, slice): + return [f(self.format) for f in funcs] + else: + return funcs(self.format) + + def __len__(self): + return 7 + + +# Full and abbreviated names of weekdays +day_name = _localized_day("%A") +day_abbr = _localized_day("%a") + +# Full and abbreviated names of months (1-based arrays!!!) +month_name = _localized_month("%B") +month_abbr = _localized_month("%b") + +# Constants for weekdays +(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7) + + +def isleap(year): + """Return True for leap years, False for non-leap years.""" + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + +def leapdays(y1, y2): + """Return number of leap years in range [y1, y2). + Assume y1 <= y2.""" + y1 -= 1 + y2 -= 1 + return (y2 // 4 - y1 // 4) - (y2 // 100 - y1 // 100) + (y2 // 400 - y1 // 400) + + +def weekday(year, month, day): + """Return weekday (0-6 ~ Mon-Sun) for year, month (1-12), day (1-31).""" + if not datetime.MINYEAR <= year <= datetime.MAXYEAR: + year = 2000 + year % 400 + return datetime.date(year, month, day).weekday() + + +def monthrange(year, month): + """Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for + year, month.""" + if not 1 <= month <= 12: + raise IllegalMonthError(month) + day1 = weekday(year, month, 1) + ndays = mdays[month] + (month == February and isleap(year)) + return day1, ndays + + +def _monthlen(year, month): + return mdays[month] + (month == February and isleap(year)) + + +def _prevmonth(year, month): + if month == 1: + return year - 1, 12 + else: + return year, month - 1 + + +def _nextmonth(year, month): + if month == 12: + return year + 1, 1 + else: + return year, month + 1 + + +class Calendar(object): + """ + Base calendar class. This class doesn't do any formatting. It simply + provides data to subclasses. + """ + + def __init__(self, firstweekday=0): + self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday + + def getfirstweekday(self): + return self._firstweekday % 7 + + def setfirstweekday(self, firstweekday): + self._firstweekday = firstweekday + + firstweekday = property(getfirstweekday, setfirstweekday) + + def iterweekdays(self): + """ + Return an iterator for one week of weekday numbers starting with the + configured first one. + """ + for i in range(self.firstweekday, self.firstweekday + 7): + yield i % 7 + + def itermonthdates(self, year, month): + """ + Return an iterator for one month. The iterator will yield datetime.date + values and will always iterate through complete weeks, so it will yield + dates outside the specified month. + """ + for y, m, d in self.itermonthdays3(year, month): + yield datetime.date(y, m, d) + + def itermonthdays(self, year, month): + """ + Like itermonthdates(), but will yield day numbers. For days outside + the specified month the day number is 0. + """ + day1, ndays = monthrange(year, month) + days_before = (day1 - self.firstweekday) % 7 + yield from repeat(0, days_before) + yield from range(1, ndays + 1) + days_after = (self.firstweekday - day1 - ndays) % 7 + yield from repeat(0, days_after) + + def itermonthdays2(self, year, month): + """ + Like itermonthdates(), but will yield (day number, weekday number) + tuples. For days outside the specified month the day number is 0. + """ + for i, d in enumerate(self.itermonthdays(year, month), self.firstweekday): + yield d, i % 7 + + def itermonthdays3(self, year, month): + """ + Like itermonthdates(), but will yield (year, month, day) tuples. Can be + used for dates outside of datetime.date range. + """ + day1, ndays = monthrange(year, month) + days_before = (day1 - self.firstweekday) % 7 + days_after = (self.firstweekday - day1 - ndays) % 7 + y, m = _prevmonth(year, month) + end = _monthlen(y, m) + 1 + for d in range(end - days_before, end): + yield y, m, d + for d in range(1, ndays + 1): + yield year, month, d + y, m = _nextmonth(year, month) + for d in range(1, days_after + 1): + yield y, m, d + + def itermonthdays4(self, year, month): + """ + Like itermonthdates(), but will yield (year, month, day, day_of_week) tuples. + Can be used for dates outside of datetime.date range. + """ + for i, (y, m, d) in enumerate(self.itermonthdays3(year, month)): + yield y, m, d, (self.firstweekday + i) % 7 + + def monthdatescalendar(self, year, month): + """ + Return a matrix (list of lists) representing a month's calendar. + Each row represents a week; week entries are datetime.date values. + """ + dates = list(self.itermonthdates(year, month)) + return [dates[i : i + 7] for i in range(0, len(dates), 7)] + + def monthdays2calendar(self, year, month): + """ + Return a matrix representing a month's calendar. + Each row represents a week; week entries are + (day number, weekday number) tuples. Day numbers outside this month + are zero. + """ + days = list(self.itermonthdays2(year, month)) + return [days[i : i + 7] for i in range(0, len(days), 7)] + + def monthdayscalendar(self, year, month): + """ + Return a matrix representing a month's calendar. + Each row represents a week; days outside this month are zero. + """ + days = list(self.itermonthdays(year, month)) + return [days[i : i + 7] for i in range(0, len(days), 7)] + + def yeardatescalendar(self, year, width=3): + """ + Return the data for the specified year ready for formatting. The return + value is a list of month rows. Each month row contains up to width months. + Each month contains between 4 and 6 weeks and each week contains 1-7 + days. Days are datetime.date objects. + """ + months = [self.monthdatescalendar(year, i) for i in range(January, January + 12)] + return [months[i : i + width] for i in range(0, len(months), width)] + + def yeardays2calendar(self, year, width=3): + """ + Return the data for the specified year ready for formatting (similar to + yeardatescalendar()). Entries in the week lists are + (day number, weekday number) tuples. Day numbers outside this month are + zero. + """ + months = [self.monthdays2calendar(year, i) for i in range(January, January + 12)] + return [months[i : i + width] for i in range(0, len(months), width)] + + def yeardayscalendar(self, year, width=3): + """ + Return the data for the specified year ready for formatting (similar to + yeardatescalendar()). Entries in the week lists are day numbers. + Day numbers outside this month are zero. + """ + months = [self.monthdayscalendar(year, i) for i in range(January, January + 12)] + return [months[i : i + width] for i in range(0, len(months), width)] + + +class TextCalendar(Calendar): + """ + Subclass of Calendar that outputs a calendar as a simple plain text + similar to the UNIX program cal. + """ + + def prweek(self, theweek, width): + """ + Print a single week (no newline). + """ + print(self.formatweek(theweek, width), end="") + + def formatday(self, day, weekday, width): + """ + Returns a formatted day. + """ + if day == 0: + s = "" + else: + s = "%2i" % day # right-align single-digit days + return s.center(width) + + def formatweek(self, theweek, width): + """ + Returns a single week in a string (no newline). + """ + return " ".join(self.formatday(d, wd, width) for (d, wd) in theweek) + + def formatweekday(self, day, width): + """ + Returns a formatted week day name. + """ + if width >= 9: + names = day_name + else: + names = day_abbr + return names[day][:width].center(width) + + def formatweekheader(self, width): + """ + Return a header for a week. + """ + return " ".join(self.formatweekday(i, width) for i in self.iterweekdays()) + + def formatmonthname(self, theyear, themonth, width, withyear=True): + """ + Return a formatted month name. + """ + s = month_name[themonth] + if withyear: + s = "%s %r" % (s, theyear) + return s.center(width) + + def prmonth(self, theyear, themonth, w=0, l=0): + """ + Print a month's calendar. + """ + print(self.formatmonth(theyear, themonth, w, l), end="") + + def formatmonth(self, theyear, themonth, w=0, l=0): + """ + Return a month's calendar string (multi-line). + """ + w = max(2, w) + l = max(1, l) + s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1) + s = s.rstrip() + s += "\n" * l + s += self.formatweekheader(w).rstrip() + s += "\n" * l + for week in self.monthdays2calendar(theyear, themonth): + s += self.formatweek(week, w).rstrip() + s += "\n" * l + return s + + def formatyear(self, theyear, w=2, l=1, c=6, m=3): + """ + Returns a year's calendar as a multi-line string. + """ + w = int(w) + l = int(l) + c = int(c) + m = int(m) + w = max(2, w) + l = max(1, l) + c = max(2, c) + colwidth = (w + 1) * 7 - 1 + v = [] + a = v.append + a(repr(theyear).center(colwidth * m + c * (m - 1)).rstrip()) + a("\n" * l) + header = self.formatweekheader(w) + for (i, row) in enumerate(self.yeardays2calendar(theyear, m)): + # months in this row + months = range(m * i + 1, min(m * (i + 1) + 1, 13)) + a("\n" * l) + names = (self.formatmonthname(theyear, k, colwidth, False) for k in months) + a(formatstring(names, colwidth, c).rstrip()) + a("\n" * l) + headers = (header for k in months) + a(formatstring(headers, colwidth, c).rstrip()) + a("\n" * l) + # max number of weeks for this row + height = max(len(cal) for cal in row) + for j in range(height): + weeks = [] + for cal in row: + if j >= len(cal): + weeks.append("") + else: + weeks.append(self.formatweek(cal[j], w)) + a(formatstring(weeks, colwidth, c).rstrip()) + a("\n" * l) + return "".join(v) + + def pryear(self, theyear, w=0, l=0, c=6, m=3): + """Print a year's calendar.""" + print(self.formatyear(theyear, w, l, c, m), end="") + + +class HTMLCalendar(Calendar): + """ + This calendar returns complete HTML pages. + """ + + # CSS classes for the day s + cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] + + # CSS classes for the day s + cssclasses_weekday_head = cssclasses + + # CSS class for the days before and after current month + cssclass_noday = "noday" + + # CSS class for the month's head + cssclass_month_head = "month" + + # CSS class for the month + cssclass_month = "month" + + # CSS class for the year's table head + cssclass_year_head = "year" + + # CSS class for the whole year table + cssclass_year = "year" + + def formatday(self, day, weekday): + """ + Return a day as a table cell. + """ + if day == 0: + # day outside month + return ' ' % self.cssclass_noday + else: + return '%d' % (self.cssclasses[weekday], day) + + def formatweek(self, theweek): + """ + Return a complete week as a table row. + """ + s = "".join(self.formatday(d, wd) for (d, wd) in theweek) + return "%s" % s + + def formatweekday(self, day): + """ + Return a weekday name as a table header. + """ + return '%s' % (self.cssclasses_weekday_head[day], day_abbr[day]) + + def formatweekheader(self): + """ + Return a header for a week as a table row. + """ + s = "".join(self.formatweekday(i) for i in self.iterweekdays()) + return "%s" % s + + def formatmonthname(self, theyear, themonth, withyear=True): + """ + Return a month name as a table row. + """ + if withyear: + s = "%s %s" % (month_name[themonth], theyear) + else: + s = "%s" % month_name[themonth] + return '%s' % (self.cssclass_month_head, s) + + def formatmonth(self, theyear, themonth, withyear=True): + """ + Return a formatted month as a table. + """ + v = [] + a = v.append + a('' % (self.cssclass_month)) + a("\n") + a(self.formatmonthname(theyear, themonth, withyear=withyear)) + a("\n") + a(self.formatweekheader()) + a("\n") + for week in self.monthdays2calendar(theyear, themonth): + a(self.formatweek(week)) + a("\n") + a("
") + a("\n") + return "".join(v) + + def formatyear(self, theyear, width=3): + """ + Return a formatted year as a table of tables. + """ + v = [] + a = v.append + width = max(width, 1) + a('' % self.cssclass_year) + a("\n") + a( + '' + % (width, self.cssclass_year_head, theyear) + ) + for i in range(January, January + 12, width): + # months in this row + months = range(i, min(i + width, 13)) + a("") + for m in months: + a("") + a("") + a("
%s
") + a(self.formatmonth(theyear, m, withyear=False)) + a("
") + return "".join(v) + + def formatyearpage(self, theyear, width=3, css="calendar.css", encoding=None): + """ + Return a formatted year as a complete HTML page. + """ + if encoding is None: + encoding = _locale.getpreferredencoding() + v = [] + a = v.append + a('\n' % encoding) + a( + '\n' + ) + a("\n") + a("\n") + a('\n' % encoding) + if css is not None: + a('\n' % css) + a("Codestin Search App\n" % theyear) + a("\n") + a("\n") + a(self.formatyear(theyear, width)) + a("\n") + a("\n") + return "".join(v).encode(encoding) + + +# Support for old module level interface +c = TextCalendar() + +firstweekday = c.getfirstweekday + + +def setfirstweekday(firstweekday): + if not MONDAY <= firstweekday <= SUNDAY: + raise IllegalWeekdayError(firstweekday) + c.firstweekday = firstweekday + + +monthcalendar = c.monthdayscalendar +prweek = c.prweek +week = c.formatweek +weekheader = c.formatweekheader +prmonth = c.prmonth +month = c.formatmonth +calendar = c.formatyear +prcal = c.pryear + + +# Spacing of month columns for multi-column year calendar +_colwidth = 7 * 3 - 1 # Amount printed by prweek() +_spacing = 6 # Number of spaces between columns + + +def format(cols, colwidth=_colwidth, spacing=_spacing): + """Prints multi-column formatting for year calendars""" + print(formatstring(cols, colwidth, spacing)) + + +def formatstring(cols, colwidth=_colwidth, spacing=_spacing): + """Returns a string formatted from n strings, centered within n columns.""" + spacing = spacing * " " + return spacing.join(c.center(colwidth) for c in cols) + + +EPOCH = 1970 +_EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal() + + +def timegm(tuple): + """Unrelated but handy function to calculate Unix timestamp from GMT.""" + year, month, day, hour, minute, second = tuple[:6] + days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1 + hours = days * 24 + hour + minutes = hours * 60 + minute + seconds = minutes * 60 + second + return seconds + + +def main(args): + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-w", "--width", type=int, default=2, help="\twidth of date column (default 2)" + ) + parser.add_argument( + "-l", "--lines", type=int, default=1, help="\tnumber of lines for each week (default 1)" + ) + parser.add_argument( + "-s", "--spacing", type=int, default=6, help="\tspacing between months (default 6)" + ) + parser.add_argument("-m", "--months", type=int, default=3, help="\tmonths per row (default 3)") + parser.add_argument("-c", "--css", default="calendar.css", help="\tCSS to use for page") + parser.add_argument("-e", "--encoding", default=None, help="\tencoding to use for output") + parser.add_argument( + "-t", + "--type", + default="text", + choices=("text", "html"), + help="\toutput type (text or html)", + ) + parser.add_argument("year", nargs="?", type=int, help="year number (1-9999)") + parser.add_argument("month", nargs="?", type=int, help="month number (1-12, text only)") + + options = parser.parse_args(args[1:]) + + if options.type == "html": + cal = HTMLCalendar() + encoding = options.encoding + if encoding is None: + encoding = _locale.getpreferredencoding() + optdict = dict(encoding=encoding, css=options.css) + write = sys.stdout.write + if options.year is None: + write(cal.formatyearpage(datetime.date.today().year, **optdict)) + elif options.month is None: + write(cal.formatyearpage(int(options.year), **optdict)) + else: + parser.error("incorrect number of arguments") + sys.exit(1) + else: + cal = TextCalendar() + optdict = dict(w=options.width, l=options.lines) + if options.month is None: + optdict["c"] = options.spacing + optdict["m"] = options.months + if options.year is None: + result = cal.formatyear(datetime.date.today().year, **optdict) + elif options.month is None: + result = cal.formatyear(int(options.year), **optdict) + else: + result = cal.formatmonth(int(options.year), int(options.month), **optdict) + write = sys.stdout.write + if options.encoding: + result = result.encode(options.encoding) + write(result) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/python-stdlib/calendar/manifest.py b/python-stdlib/calendar/manifest.py new file mode 100644 index 000000000..0b806a22a --- /dev/null +++ b/python-stdlib/calendar/manifest.py @@ -0,0 +1,3 @@ +metadata(version="1.0.0") + +module("calendar.py") diff --git a/python-stdlib/calendar/test_calendar.py b/python-stdlib/calendar/test_calendar.py new file mode 100644 index 000000000..fa71ad5db --- /dev/null +++ b/python-stdlib/calendar/test_calendar.py @@ -0,0 +1,912 @@ +import calendar +import unittest + +import time +import locale as _locale +import datetime +import os +import types + +# From https://en.wikipedia.org/wiki/Leap_year_starting_on_Saturday +result_0_02_text = """\ + February 0 +Mo Tu We Th Fr Sa Su + 1 2 3 4 5 6 + 7 8 9 10 11 12 13 +14 15 16 17 18 19 20 +21 22 23 24 25 26 27 +28 29 +""" + +result_0_text = """\ + 0 + + January February March +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 1 2 3 4 5 6 1 2 3 4 5 + 3 4 5 6 7 8 9 7 8 9 10 11 12 13 6 7 8 9 10 11 12 +10 11 12 13 14 15 16 14 15 16 17 18 19 20 13 14 15 16 17 18 19 +17 18 19 20 21 22 23 21 22 23 24 25 26 27 20 21 22 23 24 25 26 +24 25 26 27 28 29 30 28 29 27 28 29 30 31 +31 + + April May June +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 1 2 3 4 5 6 7 1 2 3 4 + 3 4 5 6 7 8 9 8 9 10 11 12 13 14 5 6 7 8 9 10 11 +10 11 12 13 14 15 16 15 16 17 18 19 20 21 12 13 14 15 16 17 18 +17 18 19 20 21 22 23 22 23 24 25 26 27 28 19 20 21 22 23 24 25 +24 25 26 27 28 29 30 29 30 31 26 27 28 29 30 + + July August September +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 1 2 3 4 5 6 1 2 3 + 3 4 5 6 7 8 9 7 8 9 10 11 12 13 4 5 6 7 8 9 10 +10 11 12 13 14 15 16 14 15 16 17 18 19 20 11 12 13 14 15 16 17 +17 18 19 20 21 22 23 21 22 23 24 25 26 27 18 19 20 21 22 23 24 +24 25 26 27 28 29 30 28 29 30 31 25 26 27 28 29 30 +31 + + October November December +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 1 2 3 4 5 1 2 3 + 2 3 4 5 6 7 8 6 7 8 9 10 11 12 4 5 6 7 8 9 10 + 9 10 11 12 13 14 15 13 14 15 16 17 18 19 11 12 13 14 15 16 17 +16 17 18 19 20 21 22 20 21 22 23 24 25 26 18 19 20 21 22 23 24 +23 24 25 26 27 28 29 27 28 29 30 25 26 27 28 29 30 31 +30 31 +""" + +result_2004_01_text = """\ + January 2004 +Mo Tu We Th Fr Sa Su + 1 2 3 4 + 5 6 7 8 9 10 11 +12 13 14 15 16 17 18 +19 20 21 22 23 24 25 +26 27 28 29 30 31 +""" + +result_2004_text = """\ + 2004 + + January February March +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 1 1 2 3 4 5 6 7 + 5 6 7 8 9 10 11 2 3 4 5 6 7 8 8 9 10 11 12 13 14 +12 13 14 15 16 17 18 9 10 11 12 13 14 15 15 16 17 18 19 20 21 +19 20 21 22 23 24 25 16 17 18 19 20 21 22 22 23 24 25 26 27 28 +26 27 28 29 30 31 23 24 25 26 27 28 29 29 30 31 + + April May June +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 1 2 1 2 3 4 5 6 + 5 6 7 8 9 10 11 3 4 5 6 7 8 9 7 8 9 10 11 12 13 +12 13 14 15 16 17 18 10 11 12 13 14 15 16 14 15 16 17 18 19 20 +19 20 21 22 23 24 25 17 18 19 20 21 22 23 21 22 23 24 25 26 27 +26 27 28 29 30 24 25 26 27 28 29 30 28 29 30 + 31 + + July August September +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 4 1 1 2 3 4 5 + 5 6 7 8 9 10 11 2 3 4 5 6 7 8 6 7 8 9 10 11 12 +12 13 14 15 16 17 18 9 10 11 12 13 14 15 13 14 15 16 17 18 19 +19 20 21 22 23 24 25 16 17 18 19 20 21 22 20 21 22 23 24 25 26 +26 27 28 29 30 31 23 24 25 26 27 28 29 27 28 29 30 + 30 31 + + October November December +Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su Mo Tu We Th Fr Sa Su + 1 2 3 1 2 3 4 5 6 7 1 2 3 4 5 + 4 5 6 7 8 9 10 8 9 10 11 12 13 14 6 7 8 9 10 11 12 +11 12 13 14 15 16 17 15 16 17 18 19 20 21 13 14 15 16 17 18 19 +18 19 20 21 22 23 24 22 23 24 25 26 27 28 20 21 22 23 24 25 26 +25 26 27 28 29 30 31 29 30 27 28 29 30 31 +""" + + +default_format = dict(year="year", month="month", encoding="ascii") + +result_2004_html = """\ + + + + + + +Codestin Search App + + + +
2004
+ + + + + + + +
January
MonTueWedThuFriSatSun
   1234
567891011
12131415161718
19202122232425
262728293031 
+
+ + + + + + + +
February
MonTueWedThuFriSatSun
      1
2345678
9101112131415
16171819202122
23242526272829
+
+ + + + + + + +
March
MonTueWedThuFriSatSun
1234567
891011121314
15161718192021
22232425262728
293031    
+
+ + + + + + + +
April
MonTueWedThuFriSatSun
   1234
567891011
12131415161718
19202122232425
2627282930  
+
+ + + + + + + + +
May
MonTueWedThuFriSatSun
     12
3456789
10111213141516
17181920212223
24252627282930
31      
+
+ + + + + + + +
June
MonTueWedThuFriSatSun
 123456
78910111213
14151617181920
21222324252627
282930    
+
+ + + + + + + +
July
MonTueWedThuFriSatSun
   1234
567891011
12131415161718
19202122232425
262728293031 
+
+ + + + + + + + +
August
MonTueWedThuFriSatSun
      1
2345678
9101112131415
16171819202122
23242526272829
3031     
+
+ + + + + + + +
September
MonTueWedThuFriSatSun
  12345
6789101112
13141516171819
20212223242526
27282930   
+
+ + + + + + + +
October
MonTueWedThuFriSatSun
    123
45678910
11121314151617
18192021222324
25262728293031
+
+ + + + + + + +
November
MonTueWedThuFriSatSun
1234567
891011121314
15161718192021
22232425262728
2930     
+
+ + + + + + + +
December
MonTueWedThuFriSatSun
  12345
6789101112
13141516171819
20212223242526
2728293031  
+
+ +""" + +result_2004_days = [ + [ + [ + [0, 0, 0, 1, 2, 3, 4], + [5, 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17, 18], + [19, 20, 21, 22, 23, 24, 25], + [26, 27, 28, 29, 30, 31, 0], + ], + [ + [0, 0, 0, 0, 0, 0, 1], + [2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15], + [16, 17, 18, 19, 20, 21, 22], + [23, 24, 25, 26, 27, 28, 29], + ], + [ + [1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13, 14], + [15, 16, 17, 18, 19, 20, 21], + [22, 23, 24, 25, 26, 27, 28], + [29, 30, 31, 0, 0, 0, 0], + ], + ], + [ + [ + [0, 0, 0, 1, 2, 3, 4], + [5, 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17, 18], + [19, 20, 21, 22, 23, 24, 25], + [26, 27, 28, 29, 30, 0, 0], + ], + [ + [0, 0, 0, 0, 0, 1, 2], + [3, 4, 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29, 30], + [31, 0, 0, 0, 0, 0, 0], + ], + [ + [0, 1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12, 13], + [14, 15, 16, 17, 18, 19, 20], + [21, 22, 23, 24, 25, 26, 27], + [28, 29, 30, 0, 0, 0, 0], + ], + ], + [ + [ + [0, 0, 0, 1, 2, 3, 4], + [5, 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17, 18], + [19, 20, 21, 22, 23, 24, 25], + [26, 27, 28, 29, 30, 31, 0], + ], + [ + [0, 0, 0, 0, 0, 0, 1], + [2, 3, 4, 5, 6, 7, 8], + [9, 10, 11, 12, 13, 14, 15], + [16, 17, 18, 19, 20, 21, 22], + [23, 24, 25, 26, 27, 28, 29], + [30, 31, 0, 0, 0, 0, 0], + ], + [ + [0, 0, 1, 2, 3, 4, 5], + [6, 7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18, 19], + [20, 21, 22, 23, 24, 25, 26], + [27, 28, 29, 30, 0, 0, 0], + ], + ], + [ + [ + [0, 0, 0, 0, 1, 2, 3], + [4, 5, 6, 7, 8, 9, 10], + [11, 12, 13, 14, 15, 16, 17], + [18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, 31], + ], + [ + [1, 2, 3, 4, 5, 6, 7], + [8, 9, 10, 11, 12, 13, 14], + [15, 16, 17, 18, 19, 20, 21], + [22, 23, 24, 25, 26, 27, 28], + [29, 30, 0, 0, 0, 0, 0], + ], + [ + [0, 0, 1, 2, 3, 4, 5], + [6, 7, 8, 9, 10, 11, 12], + [13, 14, 15, 16, 17, 18, 19], + [20, 21, 22, 23, 24, 25, 26], + [27, 28, 29, 30, 31, 0, 0], + ], + ], +] + +result_2004_dates = [ + [ + [ + "12/29/03 12/30/03 12/31/03 01/01/04 01/02/04 01/03/04 01/04/04", + "01/05/04 01/06/04 01/07/04 01/08/04 01/09/04 01/10/04 01/11/04", + "01/12/04 01/13/04 01/14/04 01/15/04 01/16/04 01/17/04 01/18/04", + "01/19/04 01/20/04 01/21/04 01/22/04 01/23/04 01/24/04 01/25/04", + "01/26/04 01/27/04 01/28/04 01/29/04 01/30/04 01/31/04 02/01/04", + ], + [ + "01/26/04 01/27/04 01/28/04 01/29/04 01/30/04 01/31/04 02/01/04", + "02/02/04 02/03/04 02/04/04 02/05/04 02/06/04 02/07/04 02/08/04", + "02/09/04 02/10/04 02/11/04 02/12/04 02/13/04 02/14/04 02/15/04", + "02/16/04 02/17/04 02/18/04 02/19/04 02/20/04 02/21/04 02/22/04", + "02/23/04 02/24/04 02/25/04 02/26/04 02/27/04 02/28/04 02/29/04", + ], + [ + "03/01/04 03/02/04 03/03/04 03/04/04 03/05/04 03/06/04 03/07/04", + "03/08/04 03/09/04 03/10/04 03/11/04 03/12/04 03/13/04 03/14/04", + "03/15/04 03/16/04 03/17/04 03/18/04 03/19/04 03/20/04 03/21/04", + "03/22/04 03/23/04 03/24/04 03/25/04 03/26/04 03/27/04 03/28/04", + "03/29/04 03/30/04 03/31/04 04/01/04 04/02/04 04/03/04 04/04/04", + ], + ], + [ + [ + "03/29/04 03/30/04 03/31/04 04/01/04 04/02/04 04/03/04 04/04/04", + "04/05/04 04/06/04 04/07/04 04/08/04 04/09/04 04/10/04 04/11/04", + "04/12/04 04/13/04 04/14/04 04/15/04 04/16/04 04/17/04 04/18/04", + "04/19/04 04/20/04 04/21/04 04/22/04 04/23/04 04/24/04 04/25/04", + "04/26/04 04/27/04 04/28/04 04/29/04 04/30/04 05/01/04 05/02/04", + ], + [ + "04/26/04 04/27/04 04/28/04 04/29/04 04/30/04 05/01/04 05/02/04", + "05/03/04 05/04/04 05/05/04 05/06/04 05/07/04 05/08/04 05/09/04", + "05/10/04 05/11/04 05/12/04 05/13/04 05/14/04 05/15/04 05/16/04", + "05/17/04 05/18/04 05/19/04 05/20/04 05/21/04 05/22/04 05/23/04", + "05/24/04 05/25/04 05/26/04 05/27/04 05/28/04 05/29/04 05/30/04", + "05/31/04 06/01/04 06/02/04 06/03/04 06/04/04 06/05/04 06/06/04", + ], + [ + "05/31/04 06/01/04 06/02/04 06/03/04 06/04/04 06/05/04 06/06/04", + "06/07/04 06/08/04 06/09/04 06/10/04 06/11/04 06/12/04 06/13/04", + "06/14/04 06/15/04 06/16/04 06/17/04 06/18/04 06/19/04 06/20/04", + "06/21/04 06/22/04 06/23/04 06/24/04 06/25/04 06/26/04 06/27/04", + "06/28/04 06/29/04 06/30/04 07/01/04 07/02/04 07/03/04 07/04/04", + ], + ], + [ + [ + "06/28/04 06/29/04 06/30/04 07/01/04 07/02/04 07/03/04 07/04/04", + "07/05/04 07/06/04 07/07/04 07/08/04 07/09/04 07/10/04 07/11/04", + "07/12/04 07/13/04 07/14/04 07/15/04 07/16/04 07/17/04 07/18/04", + "07/19/04 07/20/04 07/21/04 07/22/04 07/23/04 07/24/04 07/25/04", + "07/26/04 07/27/04 07/28/04 07/29/04 07/30/04 07/31/04 08/01/04", + ], + [ + "07/26/04 07/27/04 07/28/04 07/29/04 07/30/04 07/31/04 08/01/04", + "08/02/04 08/03/04 08/04/04 08/05/04 08/06/04 08/07/04 08/08/04", + "08/09/04 08/10/04 08/11/04 08/12/04 08/13/04 08/14/04 08/15/04", + "08/16/04 08/17/04 08/18/04 08/19/04 08/20/04 08/21/04 08/22/04", + "08/23/04 08/24/04 08/25/04 08/26/04 08/27/04 08/28/04 08/29/04", + "08/30/04 08/31/04 09/01/04 09/02/04 09/03/04 09/04/04 09/05/04", + ], + [ + "08/30/04 08/31/04 09/01/04 09/02/04 09/03/04 09/04/04 09/05/04", + "09/06/04 09/07/04 09/08/04 09/09/04 09/10/04 09/11/04 09/12/04", + "09/13/04 09/14/04 09/15/04 09/16/04 09/17/04 09/18/04 09/19/04", + "09/20/04 09/21/04 09/22/04 09/23/04 09/24/04 09/25/04 09/26/04", + "09/27/04 09/28/04 09/29/04 09/30/04 10/01/04 10/02/04 10/03/04", + ], + ], + [ + [ + "09/27/04 09/28/04 09/29/04 09/30/04 10/01/04 10/02/04 10/03/04", + "10/04/04 10/05/04 10/06/04 10/07/04 10/08/04 10/09/04 10/10/04", + "10/11/04 10/12/04 10/13/04 10/14/04 10/15/04 10/16/04 10/17/04", + "10/18/04 10/19/04 10/20/04 10/21/04 10/22/04 10/23/04 10/24/04", + "10/25/04 10/26/04 10/27/04 10/28/04 10/29/04 10/30/04 10/31/04", + ], + [ + "11/01/04 11/02/04 11/03/04 11/04/04 11/05/04 11/06/04 11/07/04", + "11/08/04 11/09/04 11/10/04 11/11/04 11/12/04 11/13/04 11/14/04", + "11/15/04 11/16/04 11/17/04 11/18/04 11/19/04 11/20/04 11/21/04", + "11/22/04 11/23/04 11/24/04 11/25/04 11/26/04 11/27/04 11/28/04", + "11/29/04 11/30/04 12/01/04 12/02/04 12/03/04 12/04/04 12/05/04", + ], + [ + "11/29/04 11/30/04 12/01/04 12/02/04 12/03/04 12/04/04 12/05/04", + "12/06/04 12/07/04 12/08/04 12/09/04 12/10/04 12/11/04 12/12/04", + "12/13/04 12/14/04 12/15/04 12/16/04 12/17/04 12/18/04 12/19/04", + "12/20/04 12/21/04 12/22/04 12/23/04 12/24/04 12/25/04 12/26/04", + "12/27/04 12/28/04 12/29/04 12/30/04 12/31/04 01/01/05 01/02/05", + ], + ], +] + + +class OutputTestCase(unittest.TestCase): + def normalize_calendar(self, s): + # Filters out locale dependent strings + def neitherspacenordigit(c): + return not c.isspace() and not c.isdigit() + + lines = [] + for line in s.splitlines(keepends=False): + # Drop texts, as they are locale dependent + if line and not filter(neitherspacenordigit, line): + lines.append(line) + return lines + + def check_htmlcalendar_encoding(self, req, res): + cal = calendar.HTMLCalendar() + format_ = default_format.copy() + format_["encoding"] = req or "utf-8" + output = cal.formatyearpage(2004, encoding=req) + self.assertEqual(output, result_2004_html.format(**format_).encode(res)) + + def test_output(self): + self.assertEqual( + self.normalize_calendar(calendar.calendar(2004)), + self.normalize_calendar(result_2004_text), + ) + self.assertEqual( + self.normalize_calendar(calendar.calendar(0)), self.normalize_calendar(result_0_text) + ) + + def test_output_textcalendar(self): + self.assertEqual(calendar.TextCalendar().formatyear(2004), result_2004_text) + self.assertEqual(calendar.TextCalendar().formatyear(0), result_0_text) + + def test_output_htmlcalendar_encoding_ascii(self): + self.check_htmlcalendar_encoding("ascii", "ascii") + + def test_output_htmlcalendar_encoding_utf8(self): + self.check_htmlcalendar_encoding("utf-8", "utf-8") + + def test_output_htmlcalendar_encoding_default(self): + self.check_htmlcalendar_encoding(None, _locale.getpreferredencoding()) + + def test_yeardatescalendar(self): + def shrink(cal): + return [ + [ + [ + " ".join( + "{:02d}/{:02d}/{}".format(d.month, d.day, str(d.year)[-2:]) for d in z + ) + for z in y + ] + for y in x + ] + for x in cal + ] + + self.assertEqual(shrink(calendar.Calendar().yeardatescalendar(2004)), result_2004_dates) + + def test_yeardayscalendar(self): + self.assertEqual(calendar.Calendar().yeardayscalendar(2004), result_2004_days) + + def test_formatweekheader_short(self): + self.assertEqual(calendar.TextCalendar().formatweekheader(2), "Mo Tu We Th Fr Sa Su") + + def test_formatweekheader_long(self): + # NOTE: center function for a string behaves differently + # in micropython affecting the expected result + # slightly. Try the following at command prompt. + # e.g 'Monday'.center(9) + # Python : ' Monday ' + # uPython: ' Monday ' + # Therefore the expected output was altered to match what + # MicroPython produces. + self.assertEqual( + calendar.TextCalendar().formatweekheader(9), + " Monday Tuesday Wednesday Thursday " " Friday Saturday Sunday ", + ) + + def test_formatmonth(self): + self.assertEqual(calendar.TextCalendar().formatmonth(2004, 1), result_2004_01_text) + self.assertEqual(calendar.TextCalendar().formatmonth(0, 2), result_0_02_text) + + def test_formatmonthname_with_year(self): + self.assertEqual( + calendar.HTMLCalendar().formatmonthname(2004, 1, withyear=True), + 'January 2004', + ) + + def test_formatmonthname_without_year(self): + self.assertEqual( + calendar.HTMLCalendar().formatmonthname(2004, 1, withyear=False), + 'January', + ) + + +class CalendarTestCase(unittest.TestCase): + def test_isleap(self): + # Make sure that the return is right for a few years, and + # ensure that the return values are 1 or 0, not just true or + # false (see SF bug #485794). Specific additional tests may + # be appropriate; this tests a single "cycle". + self.assertEqual(calendar.isleap(2000), 1) + self.assertEqual(calendar.isleap(2001), 0) + self.assertEqual(calendar.isleap(2002), 0) + self.assertEqual(calendar.isleap(2003), 0) + + def test_setfirstweekday(self): + self.assertRaises(TypeError, calendar.setfirstweekday, "flabber") + self.assertRaises(ValueError, calendar.setfirstweekday, -1) + self.assertRaises(ValueError, calendar.setfirstweekday, 200) + orig = calendar.firstweekday() + calendar.setfirstweekday(calendar.SUNDAY) + self.assertEqual(calendar.firstweekday(), calendar.SUNDAY) + calendar.setfirstweekday(calendar.MONDAY) + self.assertEqual(calendar.firstweekday(), calendar.MONDAY) + calendar.setfirstweekday(orig) + + def test_illegal_weekday_reported(self): + with self.assertRaisesRegex(calendar.IllegalWeekdayError, "123"): + calendar.setfirstweekday(123) + + def test_enumerate_weekdays(self): + self.assertRaises(IndexError, calendar.day_abbr.__getitem__, -10) + self.assertRaises(IndexError, calendar.day_name.__getitem__, 10) + self.assertEqual(len([d for d in calendar.day_abbr]), 7) + + def test_days(self): + for attr in "day_name", "day_abbr": + value = getattr(calendar, attr) + self.assertEqual(len(value), 7) + self.assertEqual(len(value[:]), 7) + # ensure they're all unique + self.assertEqual(len(set(value)), 7) + # verify it "acts like a sequence" in two forms of iteration + self.assertEqual(value[::-1], list(reversed(value))) + + def test_months(self): + for attr in "month_name", "month_abbr": + value = getattr(calendar, attr) + self.assertEqual(len(value), 13) + self.assertEqual(len(value[:]), 13) + self.assertEqual(value[0], "") + # ensure they're all unique + self.assertEqual(len(set(value)), 13) + # verify it "acts like a sequence" in two forms of iteration + self.assertEqual(value[::-1], list(reversed(value))) + + def test_itermonthdays3(self): + # ensure itermonthdays3 doesn't overflow after datetime.MAXYEAR + list(calendar.Calendar().itermonthdays3(datetime.MAXYEAR, 12)) + + def test_itermonthdays4(self): + cal = calendar.Calendar(firstweekday=3) + days = list(cal.itermonthdays4(2001, 2)) + self.assertEqual(days[0], (2001, 2, 1, 3)) + self.assertEqual(days[-1], (2001, 2, 28, 2)) + + def test_itermonthdays(self): + for firstweekday in range(7): + cal = calendar.Calendar(firstweekday) + # Test the extremes, see #28253 and #26650 + for y, m in [(1, 1), (9999, 12)]: + days = list(cal.itermonthdays(y, m)) + self.assertIn(len(days), (35, 42)) + # Test a short month + cal = calendar.Calendar(firstweekday=3) + days = list(cal.itermonthdays(2001, 2)) + self.assertEqual(days, list(range(1, 29))) + + def test_itermonthdays2(self): + for firstweekday in range(7): + cal = calendar.Calendar(firstweekday) + # Test the extremes, see #28253 and #26650 + for y, m in [(1, 1), (9999, 12)]: + days = list(cal.itermonthdays2(y, m)) + self.assertEqual(days[0][1], firstweekday) + self.assertEqual(days[-1][1], (firstweekday - 1) % 7) + + def test_iterweekdays(self): + week0 = list(range(7)) + for firstweekday in range(7): + cal = calendar.Calendar(firstweekday) + week = list(cal.iterweekdays()) + expected = week0[firstweekday:] + week0[:firstweekday] + self.assertEqual(week, expected) + + +class MonthCalendarTestCase(unittest.TestCase): + def setUp(self): + self.oldfirstweekday = calendar.firstweekday() + calendar.setfirstweekday(self.firstweekday) + + def tearDown(self): + calendar.setfirstweekday(self.oldfirstweekday) + + def check_weeks(self, year, month, weeks): + cal = calendar.monthcalendar(year, month) + self.assertEqual(len(cal), len(weeks)) + for i in range(len(weeks)): + self.assertEqual(weeks[i], sum(day != 0 for day in cal[i])) + + +class MondayTestCase(MonthCalendarTestCase): + firstweekday = calendar.MONDAY + + def test_february(self): + # A 28-day february starting on monday (7+7+7+7 days) + self.check_weeks(1999, 2, (7, 7, 7, 7)) + + # A 28-day february starting on tuesday (6+7+7+7+1 days) + self.check_weeks(2005, 2, (6, 7, 7, 7, 1)) + + # A 28-day february starting on sunday (1+7+7+7+6 days) + self.check_weeks(1987, 2, (1, 7, 7, 7, 6)) + + # A 29-day february starting on monday (7+7+7+7+1 days) + self.check_weeks(1988, 2, (7, 7, 7, 7, 1)) + + # A 29-day february starting on tuesday (6+7+7+7+2 days) + self.check_weeks(1972, 2, (6, 7, 7, 7, 2)) + + # A 29-day february starting on sunday (1+7+7+7+7 days) + self.check_weeks(2004, 2, (1, 7, 7, 7, 7)) + + def test_april(self): + # A 30-day april starting on monday (7+7+7+7+2 days) + self.check_weeks(1935, 4, (7, 7, 7, 7, 2)) + + # A 30-day april starting on tuesday (6+7+7+7+3 days) + self.check_weeks(1975, 4, (6, 7, 7, 7, 3)) + + # A 30-day april starting on sunday (1+7+7+7+7+1 days) + self.check_weeks(1945, 4, (1, 7, 7, 7, 7, 1)) + + # A 30-day april starting on saturday (2+7+7+7+7 days) + self.check_weeks(1995, 4, (2, 7, 7, 7, 7)) + + # A 30-day april starting on friday (3+7+7+7+6 days) + self.check_weeks(1994, 4, (3, 7, 7, 7, 6)) + + def test_december(self): + # A 31-day december starting on monday (7+7+7+7+3 days) + self.check_weeks(1980, 12, (7, 7, 7, 7, 3)) + + # A 31-day december starting on tuesday (6+7+7+7+4 days) + self.check_weeks(1987, 12, (6, 7, 7, 7, 4)) + + # A 31-day december starting on sunday (1+7+7+7+7+2 days) + self.check_weeks(1968, 12, (1, 7, 7, 7, 7, 2)) + + # A 31-day december starting on thursday (4+7+7+7+6 days) + self.check_weeks(1988, 12, (4, 7, 7, 7, 6)) + + # A 31-day december starting on friday (3+7+7+7+7 days) + self.check_weeks(2017, 12, (3, 7, 7, 7, 7)) + + # A 31-day december starting on saturday (2+7+7+7+7+1 days) + self.check_weeks(2068, 12, (2, 7, 7, 7, 7, 1)) + + +class SundayTestCase(MonthCalendarTestCase): + firstweekday = calendar.SUNDAY + + def test_february(self): + # A 28-day february starting on sunday (7+7+7+7 days) + self.check_weeks(2009, 2, (7, 7, 7, 7)) + + # A 28-day february starting on monday (6+7+7+7+1 days) + self.check_weeks(1999, 2, (6, 7, 7, 7, 1)) + + # A 28-day february starting on saturday (1+7+7+7+6 days) + self.check_weeks(1997, 2, (1, 7, 7, 7, 6)) + + # A 29-day february starting on sunday (7+7+7+7+1 days) + self.check_weeks(2004, 2, (7, 7, 7, 7, 1)) + + # A 29-day february starting on monday (6+7+7+7+2 days) + self.check_weeks(1960, 2, (6, 7, 7, 7, 2)) + + # A 29-day february starting on saturday (1+7+7+7+7 days) + self.check_weeks(1964, 2, (1, 7, 7, 7, 7)) + + def test_april(self): + # A 30-day april starting on sunday (7+7+7+7+2 days) + self.check_weeks(1923, 4, (7, 7, 7, 7, 2)) + + # A 30-day april starting on monday (6+7+7+7+3 days) + self.check_weeks(1918, 4, (6, 7, 7, 7, 3)) + + # A 30-day april starting on saturday (1+7+7+7+7+1 days) + self.check_weeks(1950, 4, (1, 7, 7, 7, 7, 1)) + + # A 30-day april starting on friday (2+7+7+7+7 days) + self.check_weeks(1960, 4, (2, 7, 7, 7, 7)) + + # A 30-day april starting on thursday (3+7+7+7+6 days) + self.check_weeks(1909, 4, (3, 7, 7, 7, 6)) + + def test_december(self): + # A 31-day december starting on sunday (7+7+7+7+3 days) + self.check_weeks(2080, 12, (7, 7, 7, 7, 3)) + + # A 31-day december starting on monday (6+7+7+7+4 days) + self.check_weeks(1941, 12, (6, 7, 7, 7, 4)) + + # A 31-day december starting on saturday (1+7+7+7+7+2 days) + self.check_weeks(1923, 12, (1, 7, 7, 7, 7, 2)) + + # A 31-day december starting on wednesday (4+7+7+7+6 days) + self.check_weeks(1948, 12, (4, 7, 7, 7, 6)) + + # A 31-day december starting on thursday (3+7+7+7+7 days) + self.check_weeks(1927, 12, (3, 7, 7, 7, 7)) + + # A 31-day december starting on friday (2+7+7+7+7+1 days) + self.check_weeks(1995, 12, (2, 7, 7, 7, 7, 1)) + + +class TimegmTestCase(unittest.TestCase): + TIMESTAMPS = [ + 0, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 1234567890, + 1262304000, + 1275785153, + ] + + def test_timegm(self): + for secs in self.TIMESTAMPS: + tuple = time.gmtime(secs) + self.assertEqual(secs, calendar.timegm(tuple)) + + +class MonthRangeTestCase(unittest.TestCase): + def test_january(self): + # Tests valid lower boundary case. + self.assertEqual(calendar.monthrange(2004, 1), (3, 31)) + + def test_february_leap(self): + # Tests February during leap year. + self.assertEqual(calendar.monthrange(2004, 2), (6, 29)) + + def test_february_nonleap(self): + # Tests February in non-leap year. + self.assertEqual(calendar.monthrange(2010, 2), (0, 28)) + + def test_december(self): + # Tests valid upper boundary case. + self.assertEqual(calendar.monthrange(2004, 12), (2, 31)) + + def test_zeroth_month(self): + # Tests low invalid boundary case. + with self.assertRaises(calendar.IllegalMonthError): + calendar.monthrange(2004, 0) + + def test_thirteenth_month(self): + # Tests high invalid boundary case. + with self.assertRaises(calendar.IllegalMonthError): + calendar.monthrange(2004, 13) + + def test_illegal_month_reported(self): + with self.assertRaisesRegex(calendar.IllegalMonthError, "65"): + calendar.monthrange(2004, 65) + + +class LeapdaysTestCase(unittest.TestCase): + def test_no_range(self): + # test when no range i.e. two identical years as args + self.assertEqual(calendar.leapdays(2010, 2010), 0) + + def test_no_leapdays(self): + # test when no leap years in range + self.assertEqual(calendar.leapdays(2010, 2011), 0) + + def test_no_leapdays_upper_boundary(self): + # test no leap years in range, when upper boundary is a leap year + self.assertEqual(calendar.leapdays(2010, 2012), 0) + + def test_one_leapday_lower_boundary(self): + # test when one leap year in range, lower boundary is leap year + self.assertEqual(calendar.leapdays(2012, 2013), 1) + + def test_several_leapyears_in_range(self): + self.assertEqual(calendar.leapdays(1997, 2020), 5) + + +def conv(s): + return s.replace("\n", os.linesep).encode() + + +def check__all__(test_case, module, name_of_module=None, extra=(), not_exported=()): + if name_of_module is None: + name_of_module = (module.__name__,) + elif isinstance(name_of_module, str): + name_of_module = (name_of_module,) + + expected = set(extra) + + for name in dir(module): + if name.startswith("_") or name in not_exported: + continue + obj = getattr(module, name) + if getattr(obj, "__module__", None) in name_of_module or ( + not hasattr(obj, "__module__") and not isinstance(obj, types.ModuleType) + ): + expected.add(name) + test_case.assertCountEqual(module.__all__, expected) + + +class MiscTestCase(unittest.TestCase): + def test__all__(self): + not_exported = { + "mdays", + "January", + "February", + "EPOCH", + "different_locale", + "c", + "prweek", + "week", + "format", + "formatstring", + "main", + "monthlen", + "prevmonth", + "nextmonth", + } + check__all__(self, calendar, not_exported=not_exported) + + +class TestSubClassingCase(unittest.TestCase): + def setUp(self): + class CustomHTMLCal(calendar.HTMLCalendar): + cssclasses = [style + " text-nowrap" for style in calendar.HTMLCalendar.cssclasses] + cssclasses_weekday_head = ["red", "blue", "green", "lilac", "yellow", "orange", "pink"] + cssclass_month_head = "text-center month-head" + cssclass_month = "text-center month" + cssclass_year = "text-italic " + cssclass_year_head = "lead " + + self.cal = CustomHTMLCal() + + def test_formatmonthname(self): + self.assertIn('class="text-center month-head"', self.cal.formatmonthname(2017, 5)) + + def test_formatmonth(self): + self.assertIn('class="text-center month"', self.cal.formatmonth(2017, 5)) + + def test_formatweek(self): + weeks = self.cal.monthdays2calendar(2017, 5) + self.assertIn('class="wed text-nowrap"', self.cal.formatweek(weeks[0])) + + def test_formatweek_head(self): + header = self.cal.formatweekheader() + for color in self.cal.cssclasses_weekday_head: + self.assertIn('' % color, header) + + def test_format_year(self): + self.assertIn( + ( + '' + % self.cal.cssclass_year + ), + self.cal.formatyear(2017), + ) + + def test_format_year_head(self): + self.assertIn( + '' + % (3, self.cal.cssclass_year_head, 2017), + self.cal.formatyear(2017), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/python-stdlib/datetime/datetime.py b/python-stdlib/datetime/datetime.py index b3cd9b94f..ee5f14bc6 100644 --- a/python-stdlib/datetime/datetime.py +++ b/python-stdlib/datetime/datetime.py @@ -8,6 +8,37 @@ _DIM = (0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) _TIME_SPEC = ("auto", "hours", "minutes", "seconds", "milliseconds", "microseconds") +_SHORT_WEEKDAY = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +_LONG_WEEKDAY = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] +_FULL_MONTH_NAME = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +] +_ABBREVIATED_MONTH = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +] + def _leap(y): return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) @@ -56,6 +87,89 @@ def _o2ymd(n): return y, m, n + 1 +def _strftime(newformat, timetuple): + # No strftime function in built-ins. Implement basic formatting on an as-needed + # basis here. + if newformat == "%a": + return _SHORT_WEEKDAY[timetuple[6]] + if newformat == "%A": + return _LONG_WEEKDAY[timetuple[6]] + if newformat == "%b": + return _ABBREVIATED_MONTH[timetuple[1] - 1] + if newformat == "%B": + return _FULL_MONTH_NAME[timetuple[1] - 1] + + # Format not implemented. + raise NotImplementedError( + f"Unknown format for strftime, format={newformat}, timetuple={timetuple}" + ) + + +# 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 _strftime(newformat, timetuple) + + MINYEAR = 1 MAXYEAR = 9_999 @@ -395,6 +509,10 @@ def isoformat(self): def __repr__(self): return "datetime.date(0, 0, {})".format(self._ord) + def strftime(self, fmt): + "Format using strftime()." + return _wrap_strftime(self, fmt, self.timetuple()) + __str__ = isoformat def __hash__(self): diff --git a/python-stdlib/datetime/manifest.py b/python-stdlib/datetime/manifest.py index 017189cec..962010549 100644 --- a/python-stdlib/datetime/manifest.py +++ b/python-stdlib/datetime/manifest.py @@ -1,4 +1,4 @@ -metadata(version="4.0.0") +metadata(version="4.0.1") # Originally written by Lorenzo Cappelletti. diff --git a/python-stdlib/unittest/manifest.py b/python-stdlib/unittest/manifest.py index 4e32360da..8e419b63c 100644 --- a/python-stdlib/unittest/manifest.py +++ b/python-stdlib/unittest/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.10.1") +metadata(version="0.10.2") package("unittest") diff --git a/python-stdlib/unittest/tests/test_assertions.py b/python-stdlib/unittest/tests/test_assertions.py index 089a528aa..44ed538e0 100644 --- a/python-stdlib/unittest/tests/test_assertions.py +++ b/python-stdlib/unittest/tests/test_assertions.py @@ -142,6 +142,13 @@ def testInner(): else: self.fail("Unexpected success was not detected") + @unittest.skip("test because it was found to be failing out of the box.") + def test_NotChangedByOtherTest(self): + # TODO: This has been noticed to be failing from master, so added a skip and needs to be fixed in the future. + global global_context + assert global_context is None + global_context = True + def test_subtest_even(self): """ Test that numbers between 0 and 5 are all even. @@ -150,6 +157,124 @@ def test_subtest_even(self): with self.subTest("Should only pass for even numbers", i=i): self.assertEqual(i % 2, 0) + def testAssertCountEqual(self): + a = object() + self.assertCountEqual([1, 2, 3], [3, 2, 1]) + self.assertCountEqual(["foo", "bar", "baz"], ["bar", "baz", "foo"]) + self.assertCountEqual([a, a, 2, 2, 3], (a, 2, 3, a, 2)) + self.assertCountEqual([1, "2", "a", "a"], ["a", "2", True, "a"]) + self.assertRaises( + self.failureException, self.assertCountEqual, [1, 2] + [3] * 100, [1] * 100 + [2, 3] + ) + self.assertRaises( + self.failureException, self.assertCountEqual, [1, "2", "a", "a"], ["a", "2", True, 1] + ) + self.assertRaises(self.failureException, self.assertCountEqual, [10], [10, 11]) + self.assertRaises(self.failureException, self.assertCountEqual, [10, 11], [10]) + self.assertRaises(self.failureException, self.assertCountEqual, [10, 11, 10], [10, 11]) + + # Test that sequences of unhashable objects can be tested for sameness: + self.assertCountEqual([[1, 2], [3, 4], 0], [False, [3, 4], [1, 2]]) + # Test that iterator of unhashable objects can be tested for sameness: + self.assertCountEqual(iter([1, 2, [], 3, 4]), iter([1, 2, [], 3, 4])) + + # hashable types, but not orderable + self.assertRaises( + self.failureException, self.assertCountEqual, [], [divmod, "x", 1, 5j, 2j, frozenset()] + ) + # comparing dicts + self.assertCountEqual([{"a": 1}, {"b": 2}], [{"b": 2}, {"a": 1}]) + # comparing heterogeneous non-hashable sequences + self.assertCountEqual([1, "x", divmod, []], [divmod, [], "x", 1]) + self.assertRaises( + self.failureException, self.assertCountEqual, [], [divmod, [], "x", 1, 5j, 2j, set()] + ) + self.assertRaises(self.failureException, self.assertCountEqual, [[1]], [[2]]) + + # Same elements, but not same sequence length + self.assertRaises(self.failureException, self.assertCountEqual, [1, 1, 2], [2, 1]) + self.assertRaises( + self.failureException, + self.assertCountEqual, + [1, 1, "2", "a", "a"], + ["2", "2", True, "a"], + ) + self.assertRaises( + self.failureException, + self.assertCountEqual, + [1, {"b": 2}, None, True], + [{"b": 2}, True, None], + ) + + # Same elements which don't reliably compare, in + # different order, see issue 10242 + a = [{2, 4}, {1, 2}] + b = a[::-1] + self.assertCountEqual(a, b) + + # test utility functions supporting assertCountEqual() + + diffs = set(unittest.TestCase()._count_diff_all_purpose("aaabccd", "abbbcce")) + expected = {(3, 1, "a"), (1, 3, "b"), (1, 0, "d"), (0, 1, "e")} + self.assertEqual(diffs, expected) + + diffs = unittest.TestCase()._count_diff_all_purpose([[]], []) + self.assertEqual(diffs, [(1, 0, [])]) + + def testAssertRaisesRegex(self): + class ExceptionMock(Exception): + pass + + def Stub(): + raise ExceptionMock("We expect") + + self.assertRaisesRegex(ExceptionMock, "expect$", Stub) + + def testAssertNotRaisesRegex(self): + self.assertRaisesRegex( + self.failureException, + "^ not raised$", + self.assertRaisesRegex, + Exception, + "x", + lambda: None, + ) + # NOTE: Chosen not to support a custom message. + + def testAssertRaisesRegexInvalidRegex(self): + # Issue 20145. + class MyExc(Exception): + pass + + self.assertRaises(TypeError, self.assertRaisesRegex, MyExc, lambda: True) + + def testAssertRaisesRegexMismatch(self): + def Stub(): + raise Exception("Unexpected") + + self.assertRaisesRegex( + self.failureException, + r'"\^Expected\$" does not match "Unexpected"', + self.assertRaisesRegex, + Exception, + "^Expected$", + Stub, + ) + + def testAssertRaisesRegexNoExceptionType(self): + with self.assertRaises(TypeError): + self.assertRaisesRegex() + with self.assertRaises(TypeError): + self.assertRaisesRegex(ValueError) + with self.assertRaises(TypeError): + self.assertRaisesRegex(1, "expect") + with self.assertRaises(TypeError): + self.assertRaisesRegex(object, "expect") + with self.assertRaises(TypeError): + self.assertRaisesRegex((ValueError, 1), "expect") + with self.assertRaises(TypeError): + self.assertRaisesRegex((ValueError, object), "expect") + if __name__ == "__main__": unittest.main() diff --git a/python-stdlib/unittest/unittest/__init__.py b/python-stdlib/unittest/unittest/__init__.py index 81b244ddc..e35ff5b8c 100644 --- a/python-stdlib/unittest/unittest/__init__.py +++ b/python-stdlib/unittest/unittest/__init__.py @@ -1,6 +1,7 @@ import io import os import sys +import ure try: import traceback @@ -67,7 +68,12 @@ def __exit__(self, exc_type, exc_value, traceback): pass +DIFF_OMITTED = "\nDiff is %s characters long. " "Set self.maxDiff to None to see it." + + class TestCase: + failureException = AssertionError + def __init__(self): pass @@ -202,6 +208,108 @@ def assertRaises(self, exc, func=None, *args, **kwargs): assert False, "%r not raised" % exc + def assertRaisesRegex(self, exc, expected_val, func=None, *args, **kwargs): + """ + Check for the expected exception with the expected text. + + Args: + exc (Exception): Exception expected to be raised. + expected_val (str): Regex string that will be compiled and used to search the exception value. + func (function): Function to call. Defaults to None. + + Raises: + TypeError: when the input types don't match expectations. + self.failureException: _description_ + + Returns: + _type_: _description_ + """ + if not issubclass(exc, Exception): + raise TypeError("exc not of type Exception") + + if type(expected_val) is not str: + raise TypeError("expected_val not of type str or type ure") + + if func is None: + return AssertRaisesContext(exc) + + try: + func(*args, **kwargs) + except Exception as e: + if isinstance(e, exc): + if ure.search(expected_val, e.value): + return + else: + raise self.failureException( + '"{}" does not match "{}"'.format(expected_val, e.value) + ) + + assert False, "%r not raised" % exc + + def _count_diff_all_purpose(self, actual, expected): + "Returns list of (cnt_act, cnt_exp, elem) triples where the counts differ" + # elements need not be hashable + s, t = list(actual), list(expected) + m, n = len(s), len(t) + NULL = object() + result = [] + for i, elem in enumerate(s): + if elem is NULL: + continue + cnt_s = cnt_t = 0 + for j in range(i, m): + if s[j] == elem: + cnt_s += 1 + s[j] = NULL + for j, other_elem in enumerate(t): + if other_elem == elem: + cnt_t += 1 + t[j] = NULL + if cnt_s != cnt_t: + diff = (cnt_s, cnt_t, elem) + result.append(diff) + + for i, elem in enumerate(t): + if elem is NULL: + continue + cnt_t = 0 + for j in range(i, n): + if t[j] == elem: + cnt_t += 1 + t[j] = NULL + diff = (0, cnt_t, elem) + result.append(diff) + return result + + def _truncateMessage(self, message, diff): + if len(diff) <= 640: + return message + diff + + def _formatMessage(self, msg, standardMsg): + if msg is None: + return standardMsg + return "%s : %s" % (standardMsg, msg) + + def assertCountEqual(self, first, second, msg=None): + """Asserts that two iterables have the same elements, the same number of + times, without regard to order. + + Example: + - [0, 1, 1] and [1, 0, 1] compare equal. + - [0, 0, 1] and [0, 1] compare unequal. + + """ + first_seq, second_seq = list(first), list(second) + differences = self._count_diff_all_purpose(first_seq, second_seq) + + if differences: + standardMsg = "Element counts were not equal:\n" + lines = ["First has %d, Second has %d: %r" % diff for diff in differences] + diffMsg = "\n".join(lines) + standardMsg = self._truncateMessage(standardMsg, diffMsg) + msg = self._formatMessage(msg, standardMsg) + raise self.failureException(msg) + def assertWarns(self, warn): return NullContext()
%s