]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-131146: Fix month names in a "standalone form" in calendar module (GH-131147)
authorDzmitry Plashchynski <plashchynski@gmail.com>
Thu, 31 Jul 2025 12:06:33 +0000 (15:06 +0300)
committerGitHub <noreply@github.com>
Thu, 31 Jul 2025 12:06:33 +0000 (14:06 +0200)
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.

Doc/library/calendar.rst
Lib/calendar.py
Lib/test/test_calendar.py
Misc/NEWS.d/next/Library/2025-03-17-21-21-06.gh-issue-131146.A5Obgv.rst [new file with mode: 0644]

index b292d828841f2f34ae77ab441edc1a24085e72e7..fd397547a0443735ff79f325edbe24aeedd01a51 100644 (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
index 3be1b50500eb070f8f0e272865e9f0d3e2fbbc37..45bb265a65602c2f6ef6debfd521a940f9b0c609 100644 (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)
 
index bc39c86b8cf62dbddbd03a6f7eaea8b46faa531e..589a21baf7bd6175652ce9b2db2f7e9836c6e005 100644 (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='')
diff --git a/Misc/NEWS.d/next/Library/2025-03-17-21-21-06.gh-issue-131146.A5Obgv.rst b/Misc/NEWS.d/next/Library/2025-03-17-21-21-06.gh-issue-131146.A5Obgv.rst
new file mode 100644 (file)
index 0000000..6d8bc09
--- /dev/null
@@ -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.