gh-131146: Fix month names in a "standalone form" in calendar module (GH-131147)

The calendar module displays month names in some locales using the genitive case.
This is grammatically incorrect, as the nominative case should be used when the month
is named by itself. To address this issue, this change introduces new lists
`standalone_month_name` and `standalone_month_abbr` that contain month names in
the nominative case -- or more generally, in the form that should be used to
name the month itself, rather than form a date.

The module now uses the `%OB` format specifier to get month names in this form
where available.
This commit is contained in:
Dzmitry Plashchynski 2025-07-31 15:06:33 +03:00 committed by GitHub
parent 0282eef880
commit 438cbd857a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 6 deletions

View file

@ -501,6 +501,14 @@ The :mod:`calendar` module exports the following data attributes:
>>> list(calendar.month_name)
['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
.. caution::
In locales with alternative month names forms, the :data:`!month_name` sequence
may not be suitable when a month name stands by itself and not as part of a date.
For instance, in Greek and in many Slavic and Baltic languages, :data:`!month_name`
will produce the month in genitive case. Use :data:`standalone_month_name` for a form
suitable for standalone use.
.. data:: month_abbr
@ -512,6 +520,31 @@ The :mod:`calendar` module exports the following data attributes:
>>> list(calendar.month_abbr)
['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
.. caution::
In locales with alternative month names forms, the :data:`!month_abbr` sequence
may not be suitable when a month name stands by itself and not as part of a date.
Use :data:`standalone_month_abbr` for a form suitable for standalone use.
.. data:: standalone_month_name
A sequence that represents the months of the year in the current locale
in the standalone form if the locale provides one. Else it is equivalent
to :data:`month_name`.
.. versionadded:: next
.. data:: standalone_month_abbr
A sequence that represents the abbreviated months of the year in the current
locale in the standalone form if the locale provides one. Else it is
equivalent to :data:`month_abbr`.
.. versionadded:: next
.. data:: JANUARY
FEBRUARY
MARCH

View file

@ -14,8 +14,9 @@ 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", "LocaleTextCalendar",
"timegm", "month_name", "month_abbr", "standalone_month_name",
"standalone_month_abbr", "day_name", "day_abbr", "Calendar",
"TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
"LocaleHTMLCalendar", "weekheader",
"Day", "Month", "JANUARY", "FEBRUARY", "MARCH",
"APRIL", "MAY", "JUNE", "JULY",
@ -139,6 +140,16 @@ day_abbr = _localized_day('%a')
month_name = _localized_month('%B')
month_abbr = _localized_month('%b')
# On platforms that support the %OB and %Ob specifiers, they are used
# to get the standalone form of the month name. This is required for
# some languages such as Greek, Slavic, and Baltic languages.
try:
standalone_month_name = _localized_month('%OB')
standalone_month_abbr = _localized_month('%Ob')
except ValueError:
standalone_month_name = month_name
standalone_month_abbr = month_abbr
def isleap(year):
"""Return True for leap years, False for non-leap years."""
@ -377,7 +388,7 @@ class TextCalendar(Calendar):
"""
_validate_month(themonth)
s = month_name[themonth]
s = standalone_month_name[themonth]
if withyear:
s = "%s %r" % (s, theyear)
return s.center(width)
@ -510,9 +521,9 @@ class HTMLCalendar(Calendar):
"""
_validate_month(themonth)
if withyear:
s = '%s %s' % (month_name[themonth], theyear)
s = '%s %s' % (standalone_month_name[themonth], theyear)
else:
s = '%s' % month_name[themonth]
s = standalone_month_name[themonth]
return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
self.cssclass_month_head, s)

View file

@ -8,6 +8,7 @@ import datetime
import io
import locale
import os
import platform
import sys
import time
@ -546,7 +547,8 @@ class CalendarTestCase(unittest.TestCase):
self.assertEqual(value[::-1], list(reversed(value)))
def test_months(self):
for attr in "month_name", "month_abbr":
for attr in ("month_name", "month_abbr", "standalone_month_name",
"standalone_month_abbr"):
value = getattr(calendar, attr)
self.assertEqual(len(value), 13)
self.assertEqual(len(value[:]), 13)
@ -556,6 +558,38 @@ class CalendarTestCase(unittest.TestCase):
# verify it "acts like a sequence" in two forms of iteration
self.assertEqual(value[::-1], list(reversed(value)))
@support.run_with_locale('LC_ALL', 'pl_PL')
@unittest.skipUnless(sys.platform == 'darwin' or platform.libc_ver()[0] == 'glibc',
"Guaranteed to work with glibc and macOS")
def test_standalone_month_name_and_abbr_pl_locale(self):
expected_standalone_month_names = [
"", "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec",
"lipiec", "sierpień", "wrzesień", "październik", "listopad",
"grudzień"
]
expected_standalone_month_abbr = [
"", "sty", "lut", "mar", "kwi", "maj", "cze",
"lip", "sie", "wrz", "paź", "lis", "gru"
]
self.assertEqual(
list(calendar.standalone_month_name),
expected_standalone_month_names
)
self.assertEqual(
list(calendar.standalone_month_abbr),
expected_standalone_month_abbr
)
def test_standalone_month_name_and_abbr_C_locale(self):
# Ensure that the standalone month names and abbreviations are
# equal to the regular month names and abbreviations for
# the "C" locale.
with calendar.different_locale("C"):
self.assertListEqual(list(calendar.month_name),
list(calendar.standalone_month_name))
self.assertListEqual(list(calendar.month_abbr),
list(calendar.standalone_month_abbr))
def test_locale_text_calendar(self):
try:
cal = calendar.LocaleTextCalendar(locale='')

View file

@ -0,0 +1,6 @@
Fix :class:`calendar.TextCalendar`, :class:`calendar.HTMLCalendar`,
and the :mod:`calendar` CLI to display month names in the nominative
case by adding :data:`calendar.standalone_month_name` and
:data:`calendar.standalone_month_abbr`, which provide month names and
abbreviations in the grammatical form used when a month name stands by
itself, if the locale supports it.