By default, today's date is highlighted in color and can be
:ref:`controlled using environment variables <using-on-controlling-color>`.
+.. versionchanged:: next
+ By default, the month is now also highlighted in color, and
+ the days of the week are also in color. This behavior can be
+ :ref:`controlled using environment variables <using-on-controlling-color>`.
+
*HTML-mode options:*
.. option:: --css CSS, -c CSS
calendar
--------
-* Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
- dark mode and have been migrated to the HTML5 standard for improved accessibility.
- (Contributed by Jiahao Li and Hugo van Kemenade in :gh:`137634`.)
+* :mod:`calendar`'s :ref:`command-line <calendar-cli>` text output has more
+ color. This can be controlled with :ref:`environment variables
+ <using-on-controlling-color>`.
+ (Contributed by Hugo van Kemenade in :gh:`148352`.)
* The :mod:`calendar`'s :ref:`command-line <calendar-cli>` HTML output now
accepts the year-month option: ``python -m calendar -t html 2009 06``.
(Contributed by Pål Grønås Drange in :gh:`140212`.)
+* Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
+ dark mode and have been migrated to the HTML5 standard for improved accessibility.
+ (Contributed by Jiahao Li and Hugo van Kemenade in :gh:`137634`.)
+
collections
-----------
reset: str = ANSIColors.RESET
+@dataclass(frozen=True, kw_only=True)
+class Calendar(ThemeSection):
+ header: str = ANSIColors.BOLD
+ highlight: str = ANSIColors.BLACK + ANSIColors.BACKGROUND_YELLOW
+ weekday: str = ANSIColors.CYAN
+ reset: str = ANSIColors.RESET
+
+
@dataclass(frozen=True, kw_only=True)
class Difflib(ThemeSection):
"""A 'git diff'-like theme for `difflib.unified_diff`."""
"""
argparse: Argparse = field(default_factory=Argparse)
ast: Ast = field(default_factory=Ast)
+ calendar: Calendar = field(default_factory=Calendar)
difflib: Difflib = field(default_factory=Difflib)
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
http_server: HttpServer = field(default_factory=HttpServer)
*,
argparse: Argparse | None = None,
ast: Ast | None = None,
+ calendar: Calendar | None = None,
difflib: Difflib | None = None,
fancycompleter: FancyCompleter | None = None,
http_server: HttpServer | None = None,
return type(self)(
argparse=argparse or self.argparse,
ast=ast or self.ast,
+ calendar=calendar or self.calendar,
difflib=difflib or self.difflib,
fancycompleter=fancycompleter or self.fancycompleter,
http_server=http_server or self.http_server,
return cls(
argparse=Argparse.no_colors(),
ast=Ast.no_colors(),
+ calendar=Calendar.no_colors(),
difflib=Difflib.no_colors(),
fancycompleter=FancyCompleter.no_colors(),
http_server=HttpServer.no_colors(),
super().__init__(*args, **kwargs)
self.highlight_day = highlight_day
- def formatweek(self, theweek, width, *, highlight_day=None):
+ def _get_theme(self):
+ from _colorize import get_theme
+
+ return get_theme(tty_file=sys.stdout)
+
+ def formatday(self, day, weekday, width, *, highlight_day=None):
"""
- Returns a single week in a string (no newline).
+ Returns a formatted day.
"""
- if highlight_day:
- from _colorize import get_colors
-
- ansi = get_colors()
- highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}"
- reset = ansi.RESET
+ if day == 0:
+ s = ''
else:
- highlight = reset = ""
+ s = f'{day:2}'
+ s = s.center(width)
+ if day == highlight_day:
+ theme = self._get_theme().calendar
+ s = f"{theme.highlight}{s}{theme.reset}"
+ return s
+ def formatweek(self, theweek, width, *, highlight_day=None):
+ """
+ Returns a single week in a string (no newline).
+ """
return ' '.join(
- (
- f"{highlight}{self.formatday(d, wd, width)}{reset}"
- if d == highlight_day
- else self.formatday(d, wd, width)
- )
+ self.formatday(d, wd, width, highlight_day=highlight_day)
for (d, wd) in theweek
)
+ def formatweekheader(self, width):
+ """
+ Return a header for a week.
+ """
+ header = super().formatweekheader(width)
+ theme = self._get_theme().calendar
+ return f"{theme.weekday}{header}{theme.reset}"
+
+ def formatmonthname(self, theyear, themonth, width, withyear=True):
+ """
+ Return a formatted month name.
+ """
+ name = super().formatmonthname(theyear, themonth, width, withyear)
+ theme = self._get_theme().calendar
+ if (
+ self.highlight_day
+ and self.highlight_day.year == theyear
+ and self.highlight_day.month == themonth
+ ):
+ color = theme.highlight
+ name_only = name.strip()
+ colored_name = f"{color}{name_only}{theme.reset}"
+ return name.replace(name_only, colored_name, 1)
+ else:
+ color = theme.header
+ return f"{color}{name}{theme.reset}"
+
def formatmonth(self, theyear, themonth, w=0, l=0):
"""
Return a month's calendar string (multi-line).
colwidth = (w + 1) * 7 - 1
v = []
a = v.append
- a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
+ theme = self._get_theme().calendar
+ year = repr(theyear).center(colwidth*m+c*(m-1)).rstrip()
+ a(f"{theme.header}{year}{theme.reset}")
a('\n'*l)
header = self.formatweekheader(w)
for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
def main(args=None):
import argparse
- parser = argparse.ArgumentParser(color=True)
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
textgroup = parser.add_argument_group('text only arguments')
htmlgroup = parser.add_argument_group('html only arguments')
textgroup.add_argument(
"-w", "--width",
type=int, default=2,
- help="width of date column (default 2)"
+ help="width of date column"
)
textgroup.add_argument(
"-l", "--lines",
type=int, default=1,
- help="number of lines for each week (default 1)"
+ help="number of lines for each week"
)
textgroup.add_argument(
"-s", "--spacing",
type=int, default=6,
- help="spacing between months (default 6)"
+ help="spacing between months"
)
textgroup.add_argument(
"-m", "--months",
type=int, default=3,
- help="months per row (default 3)"
+ help="months per row"
)
htmlgroup.add_argument(
"-c", "--css",
parser.add_argument(
"-e", "--encoding",
default=None,
- help="encoding to use for output (default utf-8)"
+ help="encoding to use for output"
)
parser.add_argument(
"-t", "--type",
parser.add_argument(
"-f", "--first-weekday",
type=int, default=0,
- help="weekday (0 is Monday, 6 is Sunday) to start each week (default 0)"
+ help="weekday (0 is Monday, 6 is Sunday) to start each week"
)
parser.add_argument(
"year",
def conv(s):
return s.replace('\n', os.linesep).encode()
+@support.force_not_colorized_test_class
class CommandLineTestCase(unittest.TestCase):
def setUp(self):
self.runners = [self.run_cli_ok, self.run_cmd_ok]
self.assertCLIFails(*args)
self.assertCmdFails(*args)
- @support.force_not_colorized
def test_help(self):
stdout = self.run_cmd_ok('-h')
self.assertIn(b'usage:', stdout)
self.assertIn(b'<link rel="stylesheet" href="custom.css">', output)
+@support.force_colorized_test_class
+class ColorTestCase(unittest.TestCase):
+ def test_formatmonth_color(self):
+ today = datetime.date(2026, 5, 4)
+ cal = calendar._CLIDemoCalendar(highlight_day=today)
+ output = cal.formatmonth(2026, 5)
+ self.assertIn("\x1b[30m\x1b[43mMay 2026\x1b[0m\n\x1b[36m", output)
+
+
class MiscTestCase(unittest.TestCase):
def test__all__(self):
not_exported = {
--- /dev/null
+Add more color to :mod:`calendar`'s CLI output. Patch by Hugo van Kemenade.