• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Calendar printing functions
2
3Note when comparing these calendars to the ones printed by cal(1): By
4default, these calendars have Monday as the first day of the week, and
5Sunday as the last (the European convention). Use setfirstweekday() to
6set the first day of the week (0=Monday, 6=Sunday)."""
7
8import sys
9import datetime
10from enum import IntEnum, global_enum
11import locale as _locale
12from itertools import repeat
13
14__all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
15           "firstweekday", "isleap", "leapdays", "weekday", "monthrange",
16           "monthcalendar", "prmonth", "month", "prcal", "calendar",
17           "timegm", "month_name", "month_abbr", "day_name", "day_abbr",
18           "Calendar", "TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
19           "LocaleHTMLCalendar", "weekheader",
20           "Day", "Month", "JANUARY", "FEBRUARY", "MARCH",
21           "APRIL", "MAY", "JUNE", "JULY",
22           "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER",
23           "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY",
24           "SATURDAY", "SUNDAY"]
25
26# Exception raised for bad input (with string parameter for details)
27error = ValueError
28
29# Exceptions raised for bad input
30# This is trick for backward compatibility. Since 3.13, we will raise IllegalMonthError instead of
31# IndexError for bad month number(out of 1-12). But we can't remove IndexError for backward compatibility.
32class IllegalMonthError(ValueError, IndexError):
33    def __init__(self, month):
34        self.month = month
35    def __str__(self):
36        return "bad month number %r; must be 1-12" % self.month
37
38
39class IllegalWeekdayError(ValueError):
40    def __init__(self, weekday):
41        self.weekday = weekday
42    def __str__(self):
43        return "bad weekday number %r; must be 0 (Monday) to 6 (Sunday)" % self.weekday
44
45
46def __getattr__(name):
47    if name in ('January', 'February'):
48        import warnings
49        warnings.warn(f"The '{name}' attribute is deprecated, use '{name.upper()}' instead",
50                      DeprecationWarning, stacklevel=2)
51        if name == 'January':
52            return 1
53        else:
54            return 2
55
56    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
57
58
59# Constants for months
60@global_enum
61class Month(IntEnum):
62    JANUARY = 1
63    FEBRUARY = 2
64    MARCH = 3
65    APRIL = 4
66    MAY = 5
67    JUNE = 6
68    JULY = 7
69    AUGUST = 8
70    SEPTEMBER = 9
71    OCTOBER = 10
72    NOVEMBER = 11
73    DECEMBER = 12
74
75
76# Constants for days
77@global_enum
78class Day(IntEnum):
79    MONDAY = 0
80    TUESDAY = 1
81    WEDNESDAY = 2
82    THURSDAY = 3
83    FRIDAY = 4
84    SATURDAY = 5
85    SUNDAY = 6
86
87
88# Number of days per month (except for February in leap years)
89mdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
90
91# This module used to have hard-coded lists of day and month names, as
92# English strings.  The classes following emulate a read-only version of
93# that, but supply localized names.  Note that the values are computed
94# fresh on each call, in case the user changes locale between calls.
95
96class _localized_month:
97
98    _months = [datetime.date(2001, i+1, 1).strftime for i in range(12)]
99    _months.insert(0, lambda x: "")
100
101    def __init__(self, format):
102        self.format = format
103
104    def __getitem__(self, i):
105        funcs = self._months[i]
106        if isinstance(i, slice):
107            return [f(self.format) for f in funcs]
108        else:
109            return funcs(self.format)
110
111    def __len__(self):
112        return 13
113
114
115class _localized_day:
116
117    # January 1, 2001, was a Monday.
118    _days = [datetime.date(2001, 1, i+1).strftime for i in range(7)]
119
120    def __init__(self, format):
121        self.format = format
122
123    def __getitem__(self, i):
124        funcs = self._days[i]
125        if isinstance(i, slice):
126            return [f(self.format) for f in funcs]
127        else:
128            return funcs(self.format)
129
130    def __len__(self):
131        return 7
132
133
134# Full and abbreviated names of weekdays
135day_name = _localized_day('%A')
136day_abbr = _localized_day('%a')
137
138# Full and abbreviated names of months (1-based arrays!!!)
139month_name = _localized_month('%B')
140month_abbr = _localized_month('%b')
141
142
143def isleap(year):
144    """Return True for leap years, False for non-leap years."""
145    return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
146
147
148def leapdays(y1, y2):
149    """Return number of leap years in range [y1, y2).
150       Assume y1 <= y2."""
151    y1 -= 1
152    y2 -= 1
153    return (y2//4 - y1//4) - (y2//100 - y1//100) + (y2//400 - y1//400)
154
155
156def weekday(year, month, day):
157    """Return weekday (0-6 ~ Mon-Sun) for year, month (1-12), day (1-31)."""
158    if not datetime.MINYEAR <= year <= datetime.MAXYEAR:
159        year = 2000 + year % 400
160    return Day(datetime.date(year, month, day).weekday())
161
162
163def _validate_month(month):
164    if not 1 <= month <= 12:
165        raise IllegalMonthError(month)
166
167def monthrange(year, month):
168    """Return weekday of first day of month (0-6 ~ Mon-Sun)
169       and number of days (28-31) for year, month."""
170    _validate_month(month)
171    day1 = weekday(year, month, 1)
172    ndays = mdays[month] + (month == FEBRUARY and isleap(year))
173    return day1, ndays
174
175
176def _monthlen(year, month):
177    return mdays[month] + (month == FEBRUARY and isleap(year))
178
179
180def _prevmonth(year, month):
181    if month == 1:
182        return year-1, 12
183    else:
184        return year, month-1
185
186
187def _nextmonth(year, month):
188    if month == 12:
189        return year+1, 1
190    else:
191        return year, month+1
192
193
194class Calendar(object):
195    """
196    Base calendar class. This class doesn't do any formatting. It simply
197    provides data to subclasses.
198    """
199
200    def __init__(self, firstweekday=0):
201        self.firstweekday = firstweekday # 0 = Monday, 6 = Sunday
202
203    def getfirstweekday(self):
204        return self._firstweekday % 7
205
206    def setfirstweekday(self, firstweekday):
207        self._firstweekday = firstweekday
208
209    firstweekday = property(getfirstweekday, setfirstweekday)
210
211    def iterweekdays(self):
212        """
213        Return an iterator for one week of weekday numbers starting with the
214        configured first one.
215        """
216        for i in range(self.firstweekday, self.firstweekday + 7):
217            yield i%7
218
219    def itermonthdates(self, year, month):
220        """
221        Return an iterator for one month. The iterator will yield datetime.date
222        values and will always iterate through complete weeks, so it will yield
223        dates outside the specified month.
224        """
225        for y, m, d in self.itermonthdays3(year, month):
226            yield datetime.date(y, m, d)
227
228    def itermonthdays(self, year, month):
229        """
230        Like itermonthdates(), but will yield day numbers. For days outside
231        the specified month the day number is 0.
232        """
233        day1, ndays = monthrange(year, month)
234        days_before = (day1 - self.firstweekday) % 7
235        yield from repeat(0, days_before)
236        yield from range(1, ndays + 1)
237        days_after = (self.firstweekday - day1 - ndays) % 7
238        yield from repeat(0, days_after)
239
240    def itermonthdays2(self, year, month):
241        """
242        Like itermonthdates(), but will yield (day number, weekday number)
243        tuples. For days outside the specified month the day number is 0.
244        """
245        for i, d in enumerate(self.itermonthdays(year, month), self.firstweekday):
246            yield d, i % 7
247
248    def itermonthdays3(self, year, month):
249        """
250        Like itermonthdates(), but will yield (year, month, day) tuples.  Can be
251        used for dates outside of datetime.date range.
252        """
253        day1, ndays = monthrange(year, month)
254        days_before = (day1 - self.firstweekday) % 7
255        days_after = (self.firstweekday - day1 - ndays) % 7
256        y, m = _prevmonth(year, month)
257        end = _monthlen(y, m) + 1
258        for d in range(end-days_before, end):
259            yield y, m, d
260        for d in range(1, ndays + 1):
261            yield year, month, d
262        y, m = _nextmonth(year, month)
263        for d in range(1, days_after + 1):
264            yield y, m, d
265
266    def itermonthdays4(self, year, month):
267        """
268        Like itermonthdates(), but will yield (year, month, day, day_of_week) tuples.
269        Can be used for dates outside of datetime.date range.
270        """
271        for i, (y, m, d) in enumerate(self.itermonthdays3(year, month)):
272            yield y, m, d, (self.firstweekday + i) % 7
273
274    def monthdatescalendar(self, year, month):
275        """
276        Return a matrix (list of lists) representing a month's calendar.
277        Each row represents a week; week entries are datetime.date values.
278        """
279        dates = list(self.itermonthdates(year, month))
280        return [ dates[i:i+7] for i in range(0, len(dates), 7) ]
281
282    def monthdays2calendar(self, year, month):
283        """
284        Return a matrix representing a month's calendar.
285        Each row represents a week; week entries are
286        (day number, weekday number) tuples. Day numbers outside this month
287        are zero.
288        """
289        days = list(self.itermonthdays2(year, month))
290        return [ days[i:i+7] for i in range(0, len(days), 7) ]
291
292    def monthdayscalendar(self, year, month):
293        """
294        Return a matrix representing a month's calendar.
295        Each row represents a week; days outside this month are zero.
296        """
297        days = list(self.itermonthdays(year, month))
298        return [ days[i:i+7] for i in range(0, len(days), 7) ]
299
300    def yeardatescalendar(self, year, width=3):
301        """
302        Return the data for the specified year ready for formatting. The return
303        value is a list of month rows. Each month row contains up to width months.
304        Each month contains between 4 and 6 weeks and each week contains 1-7
305        days. Days are datetime.date objects.
306        """
307        months = [self.monthdatescalendar(year, m) for m in Month]
308        return [months[i:i+width] for i in range(0, len(months), width) ]
309
310    def yeardays2calendar(self, year, width=3):
311        """
312        Return the data for the specified year ready for formatting (similar to
313        yeardatescalendar()). Entries in the week lists are
314        (day number, weekday number) tuples. Day numbers outside this month are
315        zero.
316        """
317        months = [self.monthdays2calendar(year, m) for m in Month]
318        return [months[i:i+width] for i in range(0, len(months), width) ]
319
320    def yeardayscalendar(self, year, width=3):
321        """
322        Return the data for the specified year ready for formatting (similar to
323        yeardatescalendar()). Entries in the week lists are day numbers.
324        Day numbers outside this month are zero.
325        """
326        months = [self.monthdayscalendar(year, m) for m in Month]
327        return [months[i:i+width] for i in range(0, len(months), width) ]
328
329
330class TextCalendar(Calendar):
331    """
332    Subclass of Calendar that outputs a calendar as a simple plain text
333    similar to the UNIX program cal.
334    """
335
336    def prweek(self, theweek, width):
337        """
338        Print a single week (no newline).
339        """
340        print(self.formatweek(theweek, width), end='')
341
342    def formatday(self, day, weekday, width):
343        """
344        Returns a formatted day.
345        """
346        if day == 0:
347            s = ''
348        else:
349            s = '%2i' % day             # right-align single-digit days
350        return s.center(width)
351
352    def formatweek(self, theweek, width):
353        """
354        Returns a single week in a string (no newline).
355        """
356        return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek)
357
358    def formatweekday(self, day, width):
359        """
360        Returns a formatted week day name.
361        """
362        if width >= 9:
363            names = day_name
364        else:
365            names = day_abbr
366        return names[day][:width].center(width)
367
368    def formatweekheader(self, width):
369        """
370        Return a header for a week.
371        """
372        return ' '.join(self.formatweekday(i, width) for i in self.iterweekdays())
373
374    def formatmonthname(self, theyear, themonth, width, withyear=True):
375        """
376        Return a formatted month name.
377        """
378        _validate_month(themonth)
379
380        s = month_name[themonth]
381        if withyear:
382            s = "%s %r" % (s, theyear)
383        return s.center(width)
384
385    def prmonth(self, theyear, themonth, w=0, l=0):
386        """
387        Print a month's calendar.
388        """
389        print(self.formatmonth(theyear, themonth, w, l), end='')
390
391    def formatmonth(self, theyear, themonth, w=0, l=0):
392        """
393        Return a month's calendar string (multi-line).
394        """
395        w = max(2, w)
396        l = max(1, l)
397        s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
398        s = s.rstrip()
399        s += '\n' * l
400        s += self.formatweekheader(w).rstrip()
401        s += '\n' * l
402        for week in self.monthdays2calendar(theyear, themonth):
403            s += self.formatweek(week, w).rstrip()
404            s += '\n' * l
405        return s
406
407    def formatyear(self, theyear, w=2, l=1, c=6, m=3):
408        """
409        Returns a year's calendar as a multi-line string.
410        """
411        w = max(2, w)
412        l = max(1, l)
413        c = max(2, c)
414        colwidth = (w + 1) * 7 - 1
415        v = []
416        a = v.append
417        a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
418        a('\n'*l)
419        header = self.formatweekheader(w)
420        for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
421            # months in this row
422            months = range(m*i+1, min(m*(i+1)+1, 13))
423            a('\n'*l)
424            names = (self.formatmonthname(theyear, k, colwidth, False)
425                     for k in months)
426            a(formatstring(names, colwidth, c).rstrip())
427            a('\n'*l)
428            headers = (header for k in months)
429            a(formatstring(headers, colwidth, c).rstrip())
430            a('\n'*l)
431            # max number of weeks for this row
432            height = max(len(cal) for cal in row)
433            for j in range(height):
434                weeks = []
435                for cal in row:
436                    if j >= len(cal):
437                        weeks.append('')
438                    else:
439                        weeks.append(self.formatweek(cal[j], w))
440                a(formatstring(weeks, colwidth, c).rstrip())
441                a('\n' * l)
442        return ''.join(v)
443
444    def pryear(self, theyear, w=0, l=0, c=6, m=3):
445        """Print a year's calendar."""
446        print(self.formatyear(theyear, w, l, c, m), end='')
447
448
449class HTMLCalendar(Calendar):
450    """
451    This calendar returns complete HTML pages.
452    """
453
454    # CSS classes for the day <td>s
455    cssclasses = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]
456
457    # CSS classes for the day <th>s
458    cssclasses_weekday_head = cssclasses
459
460    # CSS class for the days before and after current month
461    cssclass_noday = "noday"
462
463    # CSS class for the month's head
464    cssclass_month_head = "month"
465
466    # CSS class for the month
467    cssclass_month = "month"
468
469    # CSS class for the year's table head
470    cssclass_year_head = "year"
471
472    # CSS class for the whole year table
473    cssclass_year = "year"
474
475    def formatday(self, day, weekday):
476        """
477        Return a day as a table cell.
478        """
479        if day == 0:
480            # day outside month
481            return '<td class="%s">&nbsp;</td>' % self.cssclass_noday
482        else:
483            return '<td class="%s">%d</td>' % (self.cssclasses[weekday], day)
484
485    def formatweek(self, theweek):
486        """
487        Return a complete week as a table row.
488        """
489        s = ''.join(self.formatday(d, wd) for (d, wd) in theweek)
490        return '<tr>%s</tr>' % s
491
492    def formatweekday(self, day):
493        """
494        Return a weekday name as a table header.
495        """
496        return '<th class="%s">%s</th>' % (
497            self.cssclasses_weekday_head[day], day_abbr[day])
498
499    def formatweekheader(self):
500        """
501        Return a header for a week as a table row.
502        """
503        s = ''.join(self.formatweekday(i) for i in self.iterweekdays())
504        return '<tr>%s</tr>' % s
505
506    def formatmonthname(self, theyear, themonth, withyear=True):
507        """
508        Return a month name as a table row.
509        """
510        _validate_month(themonth)
511        if withyear:
512            s = '%s %s' % (month_name[themonth], theyear)
513        else:
514            s = '%s' % month_name[themonth]
515        return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
516            self.cssclass_month_head, s)
517
518    def formatmonth(self, theyear, themonth, withyear=True):
519        """
520        Return a formatted month as a table.
521        """
522        v = []
523        a = v.append
524        a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' % (
525            self.cssclass_month))
526        a('\n')
527        a(self.formatmonthname(theyear, themonth, withyear=withyear))
528        a('\n')
529        a(self.formatweekheader())
530        a('\n')
531        for week in self.monthdays2calendar(theyear, themonth):
532            a(self.formatweek(week))
533            a('\n')
534        a('</table>')
535        a('\n')
536        return ''.join(v)
537
538    def formatyear(self, theyear, width=3):
539        """
540        Return a formatted year as a table of tables.
541        """
542        v = []
543        a = v.append
544        width = max(width, 1)
545        a('<table border="0" cellpadding="0" cellspacing="0" class="%s">' %
546          self.cssclass_year)
547        a('\n')
548        a('<tr><th colspan="%d" class="%s">%s</th></tr>' % (
549            width, self.cssclass_year_head, theyear))
550        for i in range(JANUARY, JANUARY+12, width):
551            # months in this row
552            months = range(i, min(i+width, 13))
553            a('<tr>')
554            for m in months:
555                a('<td>')
556                a(self.formatmonth(theyear, m, withyear=False))
557                a('</td>')
558            a('</tr>')
559        a('</table>')
560        return ''.join(v)
561
562    def formatyearpage(self, theyear, width=3, css='calendar.css', encoding=None):
563        """
564        Return a formatted year as a complete HTML page.
565        """
566        if encoding is None:
567            encoding = sys.getdefaultencoding()
568        v = []
569        a = v.append
570        a('<?xml version="1.0" encoding="%s"?>\n' % encoding)
571        a('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n')
572        a('<html>\n')
573        a('<head>\n')
574        a('<meta http-equiv="Content-Type" content="text/html; charset=%s" />\n' % encoding)
575        if css is not None:
576            a('<link rel="stylesheet" type="text/css" href="%s" />\n' % css)
577        a('<title>Calendar for %d</title>\n' % theyear)
578        a('</head>\n')
579        a('<body>\n')
580        a(self.formatyear(theyear, width))
581        a('</body>\n')
582        a('</html>\n')
583        return ''.join(v).encode(encoding, "xmlcharrefreplace")
584
585
586class different_locale:
587    def __init__(self, locale):
588        self.locale = locale
589        self.oldlocale = None
590
591    def __enter__(self):
592        self.oldlocale = _locale.setlocale(_locale.LC_TIME, None)
593        _locale.setlocale(_locale.LC_TIME, self.locale)
594
595    def __exit__(self, *args):
596        _locale.setlocale(_locale.LC_TIME, self.oldlocale)
597
598
599def _get_default_locale():
600    locale = _locale.setlocale(_locale.LC_TIME, None)
601    if locale == "C":
602        with different_locale(""):
603            # The LC_TIME locale does not seem to be configured:
604            # get the user preferred locale.
605            locale = _locale.setlocale(_locale.LC_TIME, None)
606    return locale
607
608
609class LocaleTextCalendar(TextCalendar):
610    """
611    This class can be passed a locale name in the constructor and will return
612    month and weekday names in the specified locale.
613    """
614
615    def __init__(self, firstweekday=0, locale=None):
616        TextCalendar.__init__(self, firstweekday)
617        if locale is None:
618            locale = _get_default_locale()
619        self.locale = locale
620
621    def formatweekday(self, day, width):
622        with different_locale(self.locale):
623            return super().formatweekday(day, width)
624
625    def formatmonthname(self, theyear, themonth, width, withyear=True):
626        with different_locale(self.locale):
627            return super().formatmonthname(theyear, themonth, width, withyear)
628
629
630class LocaleHTMLCalendar(HTMLCalendar):
631    """
632    This class can be passed a locale name in the constructor and will return
633    month and weekday names in the specified locale.
634    """
635    def __init__(self, firstweekday=0, locale=None):
636        HTMLCalendar.__init__(self, firstweekday)
637        if locale is None:
638            locale = _get_default_locale()
639        self.locale = locale
640
641    def formatweekday(self, day):
642        with different_locale(self.locale):
643            return super().formatweekday(day)
644
645    def formatmonthname(self, theyear, themonth, withyear=True):
646        with different_locale(self.locale):
647            return super().formatmonthname(theyear, themonth, withyear)
648
649# Support for old module level interface
650c = TextCalendar()
651
652firstweekday = c.getfirstweekday
653
654def setfirstweekday(firstweekday):
655    if not MONDAY <= firstweekday <= SUNDAY:
656        raise IllegalWeekdayError(firstweekday)
657    c.firstweekday = firstweekday
658
659monthcalendar = c.monthdayscalendar
660prweek = c.prweek
661week = c.formatweek
662weekheader = c.formatweekheader
663prmonth = c.prmonth
664month = c.formatmonth
665calendar = c.formatyear
666prcal = c.pryear
667
668
669# Spacing of month columns for multi-column year calendar
670_colwidth = 7*3 - 1         # Amount printed by prweek()
671_spacing = 6                # Number of spaces between columns
672
673
674def format(cols, colwidth=_colwidth, spacing=_spacing):
675    """Prints multi-column formatting for year calendars"""
676    print(formatstring(cols, colwidth, spacing))
677
678
679def formatstring(cols, colwidth=_colwidth, spacing=_spacing):
680    """Returns a string formatted from n strings, centered within n columns."""
681    spacing *= ' '
682    return spacing.join(c.center(colwidth) for c in cols)
683
684
685EPOCH = 1970
686_EPOCH_ORD = datetime.date(EPOCH, 1, 1).toordinal()
687
688
689def timegm(tuple):
690    """Unrelated but handy function to calculate Unix timestamp from GMT."""
691    year, month, day, hour, minute, second = tuple[:6]
692    days = datetime.date(year, month, 1).toordinal() - _EPOCH_ORD + day - 1
693    hours = days*24 + hour
694    minutes = hours*60 + minute
695    seconds = minutes*60 + second
696    return seconds
697
698
699def main(args=None):
700    import argparse
701    parser = argparse.ArgumentParser()
702    textgroup = parser.add_argument_group('text only arguments')
703    htmlgroup = parser.add_argument_group('html only arguments')
704    textgroup.add_argument(
705        "-w", "--width",
706        type=int, default=2,
707        help="width of date column (default 2)"
708    )
709    textgroup.add_argument(
710        "-l", "--lines",
711        type=int, default=1,
712        help="number of lines for each week (default 1)"
713    )
714    textgroup.add_argument(
715        "-s", "--spacing",
716        type=int, default=6,
717        help="spacing between months (default 6)"
718    )
719    textgroup.add_argument(
720        "-m", "--months",
721        type=int, default=3,
722        help="months per row (default 3)"
723    )
724    htmlgroup.add_argument(
725        "-c", "--css",
726        default="calendar.css",
727        help="CSS to use for page"
728    )
729    parser.add_argument(
730        "-L", "--locale",
731        default=None,
732        help="locale to use for month and weekday names"
733    )
734    parser.add_argument(
735        "-e", "--encoding",
736        default=None,
737        help="encoding to use for output"
738    )
739    parser.add_argument(
740        "-t", "--type",
741        default="text",
742        choices=("text", "html"),
743        help="output type (text or html)"
744    )
745    parser.add_argument(
746        "-f", "--first-weekday",
747        type=int, default=0,
748        help="weekday (0 is Monday, 6 is Sunday) to start each week (default 0)"
749    )
750    parser.add_argument(
751        "year",
752        nargs='?', type=int,
753        help="year number"
754    )
755    parser.add_argument(
756        "month",
757        nargs='?', type=int,
758        help="month number (1-12, text only)"
759    )
760
761    options = parser.parse_args(args)
762
763    if options.locale and not options.encoding:
764        parser.error("if --locale is specified --encoding is required")
765        sys.exit(1)
766
767    locale = options.locale, options.encoding
768
769    if options.type == "html":
770        if options.month:
771            parser.error("incorrect number of arguments")
772            sys.exit(1)
773        if options.locale:
774            cal = LocaleHTMLCalendar(locale=locale)
775        else:
776            cal = HTMLCalendar()
777        cal.setfirstweekday(options.first_weekday)
778        encoding = options.encoding
779        if encoding is None:
780            encoding = sys.getdefaultencoding()
781        optdict = dict(encoding=encoding, css=options.css)
782        write = sys.stdout.buffer.write
783        if options.year is None:
784            write(cal.formatyearpage(datetime.date.today().year, **optdict))
785        else:
786            write(cal.formatyearpage(options.year, **optdict))
787    else:
788        if options.locale:
789            cal = LocaleTextCalendar(locale=locale)
790        else:
791            cal = TextCalendar()
792        cal.setfirstweekday(options.first_weekday)
793        optdict = dict(w=options.width, l=options.lines)
794        if options.month is None:
795            optdict["c"] = options.spacing
796            optdict["m"] = options.months
797        if options.month is not None:
798            _validate_month(options.month)
799        if options.year is None:
800            result = cal.formatyear(datetime.date.today().year, **optdict)
801        elif options.month is None:
802            result = cal.formatyear(options.year, **optdict)
803        else:
804            result = cal.formatmonth(options.year, options.month, **optdict)
805        write = sys.stdout.write
806        if options.encoding:
807            result = result.encode(options.encoding)
808            write = sys.stdout.buffer.write
809        write(result)
810
811
812if __name__ == "__main__":
813    main()
814