Skip to content

Commit 438cbd8

Browse files
authored
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.
1 parent 0282eef commit 438cbd8

File tree

4 files changed

+90
-6
lines changed

4 files changed

+90
-6
lines changed

Doc/library/calendar.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,14 @@ The :mod:`calendar` module exports the following data attributes:
501501
>>> list(calendar.month_name)
502502
['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
503503

504+
.. caution::
505+
506+
In locales with alternative month names forms, the :data:`!month_name` sequence
507+
may not be suitable when a month name stands by itself and not as part of a date.
508+
For instance, in Greek and in many Slavic and Baltic languages, :data:`!month_name`
509+
will produce the month in genitive case. Use :data:`standalone_month_name` for a form
510+
suitable for standalone use.
511+
504512

505513
.. data:: month_abbr
506514

@@ -512,6 +520,31 @@ The :mod:`calendar` module exports the following data attributes:
512520
>>> list(calendar.month_abbr)
513521
['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
514522

523+
.. caution::
524+
525+
In locales with alternative month names forms, the :data:`!month_abbr` sequence
526+
may not be suitable when a month name stands by itself and not as part of a date.
527+
Use :data:`standalone_month_abbr` for a form suitable for standalone use.
528+
529+
530+
.. data:: standalone_month_name
531+
532+
A sequence that represents the months of the year in the current locale
533+
in the standalone form if the locale provides one. Else it is equivalent
534+
to :data:`month_name`.
535+
536+
.. versionadded:: next
537+
538+
539+
.. data:: standalone_month_abbr
540+
541+
A sequence that represents the abbreviated months of the year in the current
542+
locale in the standalone form if the locale provides one. Else it is
543+
equivalent to :data:`month_abbr`.
544+
545+
.. versionadded:: next
546+
547+
515548
.. data:: JANUARY
516549
FEBRUARY
517550
MARCH

Lib/calendar.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
__all__ = ["IllegalMonthError", "IllegalWeekdayError", "setfirstweekday",
1515
"firstweekday", "isleap", "leapdays", "weekday", "monthrange",
1616
"monthcalendar", "prmonth", "month", "prcal", "calendar",
17-
"timegm", "month_name", "month_abbr", "day_name", "day_abbr",
18-
"Calendar", "TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
17+
"timegm", "month_name", "month_abbr", "standalone_month_name",
18+
"standalone_month_abbr", "day_name", "day_abbr", "Calendar",
19+
"TextCalendar", "HTMLCalendar", "LocaleTextCalendar",
1920
"LocaleHTMLCalendar", "weekheader",
2021
"Day", "Month", "JANUARY", "FEBRUARY", "MARCH",
2122
"APRIL", "MAY", "JUNE", "JULY",
@@ -139,6 +140,16 @@ def __len__(self):
139140
month_name = _localized_month('%B')
140141
month_abbr = _localized_month('%b')
141142

143+
# On platforms that support the %OB and %Ob specifiers, they are used
144+
# to get the standalone form of the month name. This is required for
145+
# some languages such as Greek, Slavic, and Baltic languages.
146+
try:
147+
standalone_month_name = _localized_month('%OB')
148+
standalone_month_abbr = _localized_month('%Ob')
149+
except ValueError:
150+
standalone_month_name = month_name
151+
standalone_month_abbr = month_abbr
152+
142153

143154
def isleap(year):
144155
"""Return True for leap years, False for non-leap years."""
@@ -377,7 +388,7 @@ def formatmonthname(self, theyear, themonth, width, withyear=True):
377388
"""
378389
_validate_month(themonth)
379390

380-
s = month_name[themonth]
391+
s = standalone_month_name[themonth]
381392
if withyear:
382393
s = "%s %r" % (s, theyear)
383394
return s.center(width)
@@ -510,9 +521,9 @@ def formatmonthname(self, theyear, themonth, withyear=True):
510521
"""
511522
_validate_month(themonth)
512523
if withyear:
513-
s = '%s %s' % (month_name[themonth], theyear)
524+
s = '%s %s' % (standalone_month_name[themonth], theyear)
514525
else:
515-
s = '%s' % month_name[themonth]
526+
s = standalone_month_name[themonth]
516527
return '<tr><th colspan="7" class="%s">%s</th></tr>' % (
517528
self.cssclass_month_head, s)
518529

Lib/test/test_calendar.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io
99
import locale
1010
import os
11+
import platform
1112
import sys
1213
import time
1314

@@ -546,7 +547,8 @@ def test_days(self):
546547
self.assertEqual(value[::-1], list(reversed(value)))
547548

548549
def test_months(self):
549-
for attr in "month_name", "month_abbr":
550+
for attr in ("month_name", "month_abbr", "standalone_month_name",
551+
"standalone_month_abbr"):
550552
value = getattr(calendar, attr)
551553
self.assertEqual(len(value), 13)
552554
self.assertEqual(len(value[:]), 13)
@@ -556,6 +558,38 @@ def test_months(self):
556558
# verify it "acts like a sequence" in two forms of iteration
557559
self.assertEqual(value[::-1], list(reversed(value)))
558560

561+
@support.run_with_locale('LC_ALL', 'pl_PL')
562+
@unittest.skipUnless(sys.platform == 'darwin' or platform.libc_ver()[0] == 'glibc',
563+
"Guaranteed to work with glibc and macOS")
564+
def test_standalone_month_name_and_abbr_pl_locale(self):
565+
expected_standalone_month_names = [
566+
"", "styczeń", "luty", "marzec", "kwiecień", "maj", "czerwiec",
567+
"lipiec", "sierpień", "wrzesień", "październik", "listopad",
568+
"grudzień"
569+
]
570+
expected_standalone_month_abbr = [
571+
"", "sty", "lut", "mar", "kwi", "maj", "cze",
572+
"lip", "sie", "wrz", "paź", "lis", "gru"
573+
]
574+
self.assertEqual(
575+
list(calendar.standalone_month_name),
576+
expected_standalone_month_names
577+
)
578+
self.assertEqual(
579+
list(calendar.standalone_month_abbr),
580+
expected_standalone_month_abbr
581+
)
582+
583+
def test_standalone_month_name_and_abbr_C_locale(self):
584+
# Ensure that the standalone month names and abbreviations are
585+
# equal to the regular month names and abbreviations for
586+
# the "C" locale.
587+
with calendar.different_locale("C"):
588+
self.assertListEqual(list(calendar.month_name),
589+
list(calendar.standalone_month_name))
590+
self.assertListEqual(list(calendar.month_abbr),
591+
list(calendar.standalone_month_abbr))
592+
559593
def test_locale_text_calendar(self):
560594
try:
561595
cal = calendar.LocaleTextCalendar(locale='')
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix :class:`calendar.TextCalendar`, :class:`calendar.HTMLCalendar`,
2+
and the :mod:`calendar` CLI to display month names in the nominative
3+
case by adding :data:`calendar.standalone_month_name` and
4+
:data:`calendar.standalone_month_abbr`, which provide month names and
5+
abbreviations in the grammatical form used when a month name stands by
6+
itself, if the locale supports it.

0 commit comments

Comments
 (0)