Skip to content

Commit 2ba0a81

Browse files
hugovksharktide
andauthored
gh-148352: Add more colour to calendar CLI output (#148354)
Co-authored-by: Rihaan Meher <sharktidedev@gmail.com>
1 parent 1504bd6 commit 2ba0a81

6 files changed

Lines changed: 95 additions & 26 deletions

File tree

Doc/library/calendar.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,11 @@ The following options are accepted:
756756
By default, today's date is highlighted in color and can be
757757
:ref:`controlled using environment variables <using-on-controlling-color>`.
758758

759+
.. versionchanged:: next
760+
By default, the month is now also highlighted in color, and
761+
the days of the week are also in color. This behavior can be
762+
:ref:`controlled using environment variables <using-on-controlling-color>`.
763+
759764
*HTML-mode options:*
760765

761766
.. option:: --css CSS, -c CSS

Doc/whatsnew/3.15.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -832,14 +832,19 @@ binascii
832832
calendar
833833
--------
834834

835-
* Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
836-
dark mode and have been migrated to the HTML5 standard for improved accessibility.
837-
(Contributed by Jiahao Li and Hugo van Kemenade in :gh:`137634`.)
835+
* :mod:`calendar`'s :ref:`command-line <calendar-cli>` text output has more
836+
color. This can be controlled with :ref:`environment variables
837+
<using-on-controlling-color>`.
838+
(Contributed by Hugo van Kemenade in :gh:`148352`.)
838839

839840
* The :mod:`calendar`'s :ref:`command-line <calendar-cli>` HTML output now
840841
accepts the year-month option: ``python -m calendar -t html 2009 06``.
841842
(Contributed by Pål Grønås Drange in :gh:`140212`.)
842843

844+
* Calendar pages generated by the :class:`calendar.HTMLCalendar` class now support
845+
dark mode and have been migrated to the HTML5 standard for improved accessibility.
846+
(Contributed by Jiahao Li and Hugo van Kemenade in :gh:`137634`.)
847+
843848

844849
collections
845850
-----------

Lib/_colorize.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ class Ast(ThemeSection):
200200
reset: str = ANSIColors.RESET
201201

202202

203+
@dataclass(frozen=True, kw_only=True)
204+
class Calendar(ThemeSection):
205+
header: str = ANSIColors.BOLD
206+
highlight: str = ANSIColors.BLACK + ANSIColors.BACKGROUND_YELLOW
207+
weekday: str = ANSIColors.CYAN
208+
reset: str = ANSIColors.RESET
209+
210+
203211
@dataclass(frozen=True, kw_only=True)
204212
class Difflib(ThemeSection):
205213
"""A 'git diff'-like theme for `difflib.unified_diff`."""
@@ -459,6 +467,7 @@ class Theme:
459467
"""
460468
argparse: Argparse = field(default_factory=Argparse)
461469
ast: Ast = field(default_factory=Ast)
470+
calendar: Calendar = field(default_factory=Calendar)
462471
difflib: Difflib = field(default_factory=Difflib)
463472
fancycompleter: FancyCompleter = field(default_factory=FancyCompleter)
464473
http_server: HttpServer = field(default_factory=HttpServer)
@@ -476,6 +485,7 @@ def copy_with(
476485
*,
477486
argparse: Argparse | None = None,
478487
ast: Ast | None = None,
488+
calendar: Calendar | None = None,
479489
difflib: Difflib | None = None,
480490
fancycompleter: FancyCompleter | None = None,
481491
http_server: HttpServer | None = None,
@@ -496,6 +506,7 @@ def copy_with(
496506
return type(self)(
497507
argparse=argparse or self.argparse,
498508
ast=ast or self.ast,
509+
calendar=calendar or self.calendar,
499510
difflib=difflib or self.difflib,
500511
fancycompleter=fancycompleter or self.fancycompleter,
501512
http_server=http_server or self.http_server,
@@ -520,6 +531,7 @@ def no_colors(cls) -> Self:
520531
return cls(
521532
argparse=Argparse.no_colors(),
522533
ast=Ast.no_colors(),
534+
calendar=Calendar.no_colors(),
523535
difflib=Difflib.no_colors(),
524536
fancycompleter=FancyCompleter.no_colors(),
525537
http_server=HttpServer.no_colors(),

Lib/calendar.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -686,28 +686,61 @@ def __init__(self, highlight_day=None, *args, **kwargs):
686686
super().__init__(*args, **kwargs)
687687
self.highlight_day = highlight_day
688688

689-
def formatweek(self, theweek, width, *, highlight_day=None):
689+
def _get_theme(self):
690+
from _colorize import get_theme
691+
692+
return get_theme(tty_file=sys.stdout)
693+
694+
def formatday(self, day, weekday, width, *, highlight_day=None):
690695
"""
691-
Returns a single week in a string (no newline).
696+
Returns a formatted day.
692697
"""
693-
if highlight_day:
694-
from _colorize import get_colors
695-
696-
ansi = get_colors()
697-
highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}"
698-
reset = ansi.RESET
698+
if day == 0:
699+
s = ''
699700
else:
700-
highlight = reset = ""
701+
s = f'{day:2}'
702+
s = s.center(width)
703+
if day == highlight_day:
704+
theme = self._get_theme().calendar
705+
s = f"{theme.highlight}{s}{theme.reset}"
706+
return s
701707

708+
def formatweek(self, theweek, width, *, highlight_day=None):
709+
"""
710+
Returns a single week in a string (no newline).
711+
"""
702712
return ' '.join(
703-
(
704-
f"{highlight}{self.formatday(d, wd, width)}{reset}"
705-
if d == highlight_day
706-
else self.formatday(d, wd, width)
707-
)
713+
self.formatday(d, wd, width, highlight_day=highlight_day)
708714
for (d, wd) in theweek
709715
)
710716

717+
def formatweekheader(self, width):
718+
"""
719+
Return a header for a week.
720+
"""
721+
header = super().formatweekheader(width)
722+
theme = self._get_theme().calendar
723+
return f"{theme.weekday}{header}{theme.reset}"
724+
725+
def formatmonthname(self, theyear, themonth, width, withyear=True):
726+
"""
727+
Return a formatted month name.
728+
"""
729+
name = super().formatmonthname(theyear, themonth, width, withyear)
730+
theme = self._get_theme().calendar
731+
if (
732+
self.highlight_day
733+
and self.highlight_day.year == theyear
734+
and self.highlight_day.month == themonth
735+
):
736+
color = theme.highlight
737+
name_only = name.strip()
738+
colored_name = f"{color}{name_only}{theme.reset}"
739+
return name.replace(name_only, colored_name, 1)
740+
else:
741+
color = theme.header
742+
return f"{color}{name}{theme.reset}"
743+
711744
def formatmonth(self, theyear, themonth, w=0, l=0):
712745
"""
713746
Return a month's calendar string (multi-line).
@@ -742,7 +775,9 @@ def formatyear(self, theyear, w=2, l=1, c=6, m=3):
742775
colwidth = (w + 1) * 7 - 1
743776
v = []
744777
a = v.append
745-
a(repr(theyear).center(colwidth*m+c*(m-1)).rstrip())
778+
theme = self._get_theme().calendar
779+
year = repr(theyear).center(colwidth*m+c*(m-1)).rstrip()
780+
a(f"{theme.header}{year}{theme.reset}")
746781
a('\n'*l)
747782
header = self.formatweekheader(w)
748783
for (i, row) in enumerate(self.yeardays2calendar(theyear, m)):
@@ -843,28 +878,30 @@ def timegm(tuple):
843878

844879
def main(args=None):
845880
import argparse
846-
parser = argparse.ArgumentParser(color=True)
881+
parser = argparse.ArgumentParser(
882+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
883+
)
847884
textgroup = parser.add_argument_group('text only arguments')
848885
htmlgroup = parser.add_argument_group('html only arguments')
849886
textgroup.add_argument(
850887
"-w", "--width",
851888
type=int, default=2,
852-
help="width of date column (default 2)"
889+
help="width of date column"
853890
)
854891
textgroup.add_argument(
855892
"-l", "--lines",
856893
type=int, default=1,
857-
help="number of lines for each week (default 1)"
894+
help="number of lines for each week"
858895
)
859896
textgroup.add_argument(
860897
"-s", "--spacing",
861898
type=int, default=6,
862-
help="spacing between months (default 6)"
899+
help="spacing between months"
863900
)
864901
textgroup.add_argument(
865902
"-m", "--months",
866903
type=int, default=3,
867-
help="months per row (default 3)"
904+
help="months per row"
868905
)
869906
htmlgroup.add_argument(
870907
"-c", "--css",
@@ -879,7 +916,7 @@ def main(args=None):
879916
parser.add_argument(
880917
"-e", "--encoding",
881918
default=None,
882-
help="encoding to use for output (default utf-8)"
919+
help="encoding to use for output"
883920
)
884921
parser.add_argument(
885922
"-t", "--type",
@@ -890,7 +927,7 @@ def main(args=None):
890927
parser.add_argument(
891928
"-f", "--first-weekday",
892929
type=int, default=0,
893-
help="weekday (0 is Monday, 6 is Sunday) to start each week (default 0)"
930+
help="weekday (0 is Monday, 6 is Sunday) to start each week"
894931
)
895932
parser.add_argument(
896933
"year",

Lib/test/test_calendar.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,7 @@ def test_several_leapyears_in_range(self):
10681068
def conv(s):
10691069
return s.replace('\n', os.linesep).encode()
10701070

1071+
@support.force_not_colorized_test_class
10711072
class CommandLineTestCase(unittest.TestCase):
10721073
def setUp(self):
10731074
self.runners = [self.run_cli_ok, self.run_cmd_ok]
@@ -1121,7 +1122,6 @@ def assertFailure(self, *args):
11211122
self.assertCLIFails(*args)
11221123
self.assertCmdFails(*args)
11231124

1124-
@support.force_not_colorized
11251125
def test_help(self):
11261126
stdout = self.run_cmd_ok('-h')
11271127
self.assertIn(b'usage:', stdout)
@@ -1256,6 +1256,15 @@ def test_html_output_year_css(self):
12561256
self.assertIn(b'<link rel="stylesheet" href="custom.css">', output)
12571257

12581258

1259+
@support.force_colorized_test_class
1260+
class ColorTestCase(unittest.TestCase):
1261+
def test_formatmonth_color(self):
1262+
today = datetime.date(2026, 5, 4)
1263+
cal = calendar._CLIDemoCalendar(highlight_day=today)
1264+
output = cal.formatmonth(2026, 5)
1265+
self.assertIn("\x1b[30m\x1b[43mMay 2026\x1b[0m\n\x1b[36m", output)
1266+
1267+
12591268
class MiscTestCase(unittest.TestCase):
12601269
def test__all__(self):
12611270
not_exported = {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add more color to :mod:`calendar`'s CLI output. Patch by Hugo van Kemenade.

0 commit comments

Comments
 (0)