Skip to content

Commit 5eaaf29

Browse files
committed
gh-155245: Fix calendar failing to import when strftime rejects %OB
_localized_month stores the format string and calls strftime lazily from __getitem__, so constructing _localized_month('%OB') never raises. The first strftime call happens further down, while comparing the standalone names against the regular ones to detect systems that keep '%OB' as-is -- and that comparison sits in the else branch, outside the try block meant to catch the failure. On a platform whose strftime rejects '%OB' the ValueError therefore escaped and importing calendar failed outright. Move the comparison into the try block so the intended fallback to month_name/month_abbr applies.
1 parent 4130af1 commit 5eaaf29

3 files changed

Lines changed: 34 additions & 4 deletions

File tree

Lib/calendar.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,20 @@ def __len__(self):
146146
try:
147147
standalone_month_name = _localized_month('%OB')
148148
standalone_month_abbr = _localized_month('%Ob')
149-
except ValueError:
150-
standalone_month_name = month_name
151-
standalone_month_abbr = month_abbr
152-
else:
149+
# _localized_month only stores the format; strftime is called lazily when
150+
# the names are first read, so systems that reject '%OB' raise ValueError
151+
# here rather than above.
152+
#
153153
# Some systems that do not support '%OB' will keep it as-is (i.e.,
154154
# we get [..., '%OB', '%OB', '%OB']), so for non-distinct names,
155155
# we fall back to month_name/month_abbr.
156156
if len(set(standalone_month_name)) != len(set(month_name)):
157157
standalone_month_name = month_name
158158
if len(set(standalone_month_abbr)) != len(set(month_abbr)):
159159
standalone_month_abbr = month_abbr
160+
except ValueError:
161+
standalone_month_name = month_name
162+
standalone_month_abbr = month_abbr
160163

161164

162165
def isleap(year):

Lib/test/test_calendar.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import unittest
33

44
from test import support
5+
from test.support import import_helper
56
from test.support.script_helper import assert_python_ok, assert_python_failure
67
import contextlib
78
import datetime
@@ -11,6 +12,7 @@
1112
import platform
1213
import sys
1314
import time
15+
from unittest import mock
1416

1517
# From https://en.wikipedia.org/wiki/Leap_year_starting_on_Saturday
1618
result_0_02_text = """\
@@ -644,6 +646,26 @@ def test_standalone_month_name_and_abbr_C_locale(self):
644646
self.assertListEqual(list(calendar.month_abbr),
645647
list(calendar.standalone_month_abbr))
646648

649+
def test_standalone_month_fallback_when_specifier_rejected(self):
650+
# gh-155245: _localized_month stores the format string and only calls
651+
# strftime when a name is first read, so a platform that rejects
652+
# '%OB' raises ValueError while the fallback below is being computed,
653+
# not while the object is being built. Importing calendar has to fall
654+
# back to the regular names instead of failing outright.
655+
class _RejectsStandalone(datetime.date):
656+
def strftime(self, format):
657+
if 'O' in format:
658+
raise ValueError(f'Invalid format string: {format}')
659+
return super().strftime(format)
660+
661+
with mock.patch.object(datetime, 'date', _RejectsStandalone):
662+
fresh_calendar = import_helper.import_fresh_module('calendar')
663+
664+
self.assertListEqual(list(fresh_calendar.standalone_month_name),
665+
list(fresh_calendar.month_name))
666+
self.assertListEqual(list(fresh_calendar.standalone_month_abbr),
667+
list(fresh_calendar.month_abbr))
668+
647669
def test_locale_text_calendar(self):
648670
try:
649671
cal = calendar.LocaleTextCalendar(locale='')
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix :mod:`calendar` failing to import on platforms whose ``strftime``
2+
rejects the ``%OB`` and ``%Ob`` format specifiers. The names are computed
3+
lazily, so the resulting :exc:`ValueError` was raised outside the ``try``
4+
block that was meant to catch it, instead of falling back to the regular
5+
month names.

0 commit comments

Comments
 (0)