gh-128317: Highlight today in colour in calendar CLI output (#128318)

Co-authored-by: Peter Bierma <zintensitydev@gmail.com>
This commit is contained in:
Hugo van Kemenade 2025-01-03 14:56:24 +02:00 committed by GitHub
parent fa985bee61
commit f21af186bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 85 additions and 13 deletions

View file

@ -146,26 +146,34 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
the specified width, representing an empty day. The *weekday* parameter the specified width, representing an empty day. The *weekday* parameter
is unused. is unused.
.. method:: formatweek(theweek, w=0) .. method:: formatweek(theweek, w=0, highlight_day=None)
Return a single week in a string with no newline. If *w* is provided, it Return a single week in a string with no newline. If *w* is provided, it
specifies the width of the date columns, which are centered. Depends specifies the width of the date columns, which are centered. Depends
on the first weekday as specified in the constructor or set by the on the first weekday as specified in the constructor or set by the
:meth:`setfirstweekday` method. :meth:`setfirstweekday` method.
.. versionchanged:: next
If *highlight_day* is given, this date is highlighted in color.
This can be :ref:`controlled using environment variables
<using-on-controlling-color>`.
.. method:: formatweekday(weekday, width) .. method:: formatweekday(weekday, width)
Return a string representing the name of a single weekday formatted to Return a string representing the name of a single weekday formatted to
the specified *width*. The *weekday* parameter is an integer representing the specified *width*. The *weekday* parameter is an integer representing
the day of the week, where ``0`` is Monday and ``6`` is Sunday. the day of the week, where ``0`` is Monday and ``6`` is Sunday.
.. method:: formatweekheader(width) .. method:: formatweekheader(width)
Return a string containing the header row of weekday names, formatted Return a string containing the header row of weekday names, formatted
with the given *width* for each column. The names depend on the locale with the given *width* for each column. The names depend on the locale
settings and are padded to the specified width. settings and are padded to the specified width.
.. method:: formatmonth(theyear, themonth, w=0, l=0)
.. method:: formatmonth(theyear, themonth, w=0, l=0, highlight_day=None)
Return a month's calendar in a multi-line string. If *w* is provided, it Return a month's calendar in a multi-line string. If *w* is provided, it
specifies the width of the date columns, which are centered. If *l* is specifies the width of the date columns, which are centered. If *l* is
@ -173,6 +181,12 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
on the first weekday as specified in the constructor or set by the on the first weekday as specified in the constructor or set by the
:meth:`setfirstweekday` method. :meth:`setfirstweekday` method.
.. versionchanged:: next
If *highlight_day* is given, this date is highlighted in color.
This can be :ref:`controlled using environment variables
<using-on-controlling-color>`.
.. method:: formatmonthname(theyear, themonth, width=0, withyear=True) .. method:: formatmonthname(theyear, themonth, width=0, withyear=True)
Return a string representing the month's name centered within the Return a string representing the month's name centered within the
@ -180,12 +194,13 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
output. The *theyear* and *themonth* parameters specify the year output. The *theyear* and *themonth* parameters specify the year
and month for the name to be formatted respectively. and month for the name to be formatted respectively.
.. method:: prmonth(theyear, themonth, w=0, l=0) .. method:: prmonth(theyear, themonth, w=0, l=0)
Print a month's calendar as returned by :meth:`formatmonth`. Print a month's calendar as returned by :meth:`formatmonth`.
.. method:: formatyear(theyear, w=2, l=1, c=6, m=3) .. method:: formatyear(theyear, w=2, l=1, c=6, m=3, highlight_day=None)
Return a *m*-column calendar for an entire year as a multi-line string. Return a *m*-column calendar for an entire year as a multi-line string.
Optional parameters *w*, *l*, and *c* are for date column width, lines per Optional parameters *w*, *l*, and *c* are for date column width, lines per
@ -194,6 +209,11 @@ interpreted as prescribed by the ISO 8601 standard. Year 0 is 1 BC, year -1 is
:meth:`setfirstweekday` method. The earliest year for which a calendar :meth:`setfirstweekday` method. The earliest year for which a calendar
can be generated is platform-dependent. can be generated is platform-dependent.
.. versionchanged:: next
If *highlight_day* is given, this date is highlighted in color.
This can be :ref:`controlled using environment variables
<using-on-controlling-color>`.
.. method:: pryear(theyear, w=2, l=1, c=6, m=3) .. method:: pryear(theyear, w=2, l=1, c=6, m=3)
@ -549,7 +569,7 @@ The :mod:`calendar` module defines the following exceptions:
.. _calendar-cli: .. _calendar-cli:
Command-Line Usage Command-line usage
------------------ ------------------
.. versionadded:: 2.5 .. versionadded:: 2.5
@ -687,6 +707,9 @@ The following options are accepted:
The number of months printed per row. The number of months printed per row.
Defaults to 3. Defaults to 3.
.. versionchanged:: next
By default, today's date is highlighted in color and can be
:ref:`controlled using environment variables <using-on-controlling-color>`.
*HTML-mode options:* *HTML-mode options:*

View file

@ -296,6 +296,19 @@ ast
* The ``repr()`` output for AST nodes now includes more information. * The ``repr()`` output for AST nodes now includes more information.
(Contributed by Tomas R in :gh:`116022`.) (Contributed by Tomas R in :gh:`116022`.)
calendar
--------
* By default, today's date is highlighted in color in :mod:`calendar`'s
:ref:`command-line <calendar-cli>` text output.
This can be controlled via the :envvar:`PYTHON_COLORS` environment
variable as well as the canonical |NO_COLOR|_
and |FORCE_COLOR|_ environment variables.
See also :ref:`using-on-controlling-color`.
(Contributed by Hugo van Kemenade in :gh:`128317`.)
concurrent.futures concurrent.futures
------------------ ------------------

View file

@ -6,9 +6,11 @@ COLORIZE = True
class ANSIColors: class ANSIColors:
BACKGROUND_YELLOW = "\x1b[43m"
BOLD_GREEN = "\x1b[1;32m" BOLD_GREEN = "\x1b[1;32m"
BOLD_MAGENTA = "\x1b[1;35m" BOLD_MAGENTA = "\x1b[1;35m"
BOLD_RED = "\x1b[1;31m" BOLD_RED = "\x1b[1;31m"
BLACK = "\x1b[30m"
GREEN = "\x1b[32m" GREEN = "\x1b[32m"
GREY = "\x1b[90m" GREY = "\x1b[90m"
MAGENTA = "\x1b[35m" MAGENTA = "\x1b[35m"

View file

@ -349,11 +349,27 @@ class TextCalendar(Calendar):
s = '%2i' % day # right-align single-digit days s = '%2i' % day # right-align single-digit days
return s.center(width) return s.center(width)
def formatweek(self, theweek, width): def formatweek(self, theweek, width, *, highlight_day=None):
""" """
Returns a single week in a string (no newline). Returns a single week in a string (no newline).
""" """
return ' '.join(self.formatday(d, wd, width) for (d, wd) in theweek) if highlight_day:
from _colorize import get_colors
ansi = get_colors()
highlight = f"{ansi.BLACK}{ansi.BACKGROUND_YELLOW}"
reset = ansi.RESET
else:
highlight = reset = ""
return ' '.join(
(
f"{highlight}{self.formatday(d, wd, width)}{reset}"
if d == highlight_day
else self.formatday(d, wd, width)
)
for (d, wd) in theweek
)
def formatweekday(self, day, width): def formatweekday(self, day, width):
""" """
@ -388,10 +404,11 @@ class TextCalendar(Calendar):
""" """
print(self.formatmonth(theyear, themonth, w, l), end='') print(self.formatmonth(theyear, themonth, w, l), end='')
def formatmonth(self, theyear, themonth, w=0, l=0): def formatmonth(self, theyear, themonth, w=0, l=0, *, highlight_day=None):
""" """
Return a month's calendar string (multi-line). Return a month's calendar string (multi-line).
""" """
highlight_day = highlight_day.day if highlight_day else None
w = max(2, w) w = max(2, w)
l = max(1, l) l = max(1, l)
s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1) s = self.formatmonthname(theyear, themonth, 7 * (w + 1) - 1)
@ -400,11 +417,11 @@ class TextCalendar(Calendar):
s += self.formatweekheader(w).rstrip() s += self.formatweekheader(w).rstrip()
s += '\n' * l s += '\n' * l
for week in self.monthdays2calendar(theyear, themonth): for week in self.monthdays2calendar(theyear, themonth):
s += self.formatweek(week, w).rstrip() s += self.formatweek(week, w, highlight_day=highlight_day).rstrip()
s += '\n' * l s += '\n' * l
return s return s
def formatyear(self, theyear, w=2, l=1, c=6, m=3): def formatyear(self, theyear, w=2, l=1, c=6, m=3, *, highlight_day=None):
""" """
Returns a year's calendar as a multi-line string. Returns a year's calendar as a multi-line string.
""" """
@ -428,15 +445,24 @@ class TextCalendar(Calendar):
headers = (header for k in months) headers = (header for k in months)
a(formatstring(headers, colwidth, c).rstrip()) a(formatstring(headers, colwidth, c).rstrip())
a('\n'*l) a('\n'*l)
if highlight_day and highlight_day.month in months:
month_pos = months.index(highlight_day.month)
else:
month_pos = None
# max number of weeks for this row # max number of weeks for this row
height = max(len(cal) for cal in row) height = max(len(cal) for cal in row)
for j in range(height): for j in range(height):
weeks = [] weeks = []
for cal in row: for k, cal in enumerate(row):
if j >= len(cal): if j >= len(cal):
weeks.append('') weeks.append('')
else: else:
weeks.append(self.formatweek(cal[j], w)) day = highlight_day.day if k == month_pos else None
weeks.append(
self.formatweek(cal[j], w, highlight_day=day)
)
a(formatstring(weeks, colwidth, c).rstrip()) a(formatstring(weeks, colwidth, c).rstrip())
a('\n' * l) a('\n' * l)
return ''.join(v) return ''.join(v)
@ -765,6 +791,7 @@ def main(args=None):
sys.exit(1) sys.exit(1)
locale = options.locale, options.encoding locale = options.locale, options.encoding
today = datetime.date.today()
if options.type == "html": if options.type == "html":
if options.month: if options.month:
@ -781,7 +808,7 @@ def main(args=None):
optdict = dict(encoding=encoding, css=options.css) optdict = dict(encoding=encoding, css=options.css)
write = sys.stdout.buffer.write write = sys.stdout.buffer.write
if options.year is None: if options.year is None:
write(cal.formatyearpage(datetime.date.today().year, **optdict)) write(cal.formatyearpage(today.year, **optdict))
else: else:
write(cal.formatyearpage(options.year, **optdict)) write(cal.formatyearpage(options.year, **optdict))
else: else:
@ -797,10 +824,15 @@ def main(args=None):
if options.month is not None: if options.month is not None:
_validate_month(options.month) _validate_month(options.month)
if options.year is None: if options.year is None:
result = cal.formatyear(datetime.date.today().year, **optdict) optdict["highlight_day"] = today
result = cal.formatyear(today.year, **optdict)
elif options.month is None: elif options.month is None:
if options.year == today.year:
optdict["highlight_day"] = today
result = cal.formatyear(options.year, **optdict) result = cal.formatyear(options.year, **optdict)
else: else:
if options.year == today.year and options.month == today.month:
optdict["highlight_day"] = today
result = cal.formatmonth(options.year, options.month, **optdict) result = cal.formatmonth(options.year, options.month, **optdict)
write = sys.stdout.write write = sys.stdout.write
if options.encoding: if options.encoding:

View file

@ -0,0 +1,2 @@
Highlight today in colour in :mod:`calendar`'s CLI output. Patch by Hugo van
Kemenade.