diff --git a/Lib/calendar.py b/Lib/calendar.py index 92fe6b7723fe26..de467a7e8367b5 100644 --- a/Lib/calendar.py +++ b/Lib/calendar.py @@ -150,13 +150,20 @@ def __len__(self): standalone_month_name = month_name standalone_month_abbr = month_abbr else: - # Some systems that do not support '%OB' will keep it as-is (i.e., - # we get [..., '%OB', '%OB', '%OB']), so for non-distinct names, - # we fall back to month_name/month_abbr. - if len(set(standalone_month_name)) != len(set(month_name)): - standalone_month_name = month_name - if len(set(standalone_month_abbr)) != len(set(month_abbr)): - standalone_month_abbr = month_abbr + # Some systems that do not support '%OB' will keep it as-is (i.e., + # we get [..., '%OB', '%OB', '%OB']), so for non-distinct names, + # we fall back to month_name/month_abbr. The strftime() calls + # inside set() are evaluated lazily here, so on platforms that + # accept '%OB' at construction time but reject it later (e.g. + # under Wine), this can still raise ValueError. + try: + if len(set(standalone_month_name)) != len(set(month_name)): + standalone_month_name = month_name + if len(set(standalone_month_abbr)) != len(set(month_abbr)): + standalone_month_abbr = month_abbr + except ValueError: + standalone_month_name = month_name + standalone_month_abbr = month_abbr def isleap(year): diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index 8646cfcad58cea..04f878124326cb 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -644,6 +644,33 @@ def test_standalone_month_name_and_abbr_C_locale(self): self.assertListEqual(list(calendar.month_abbr), list(calendar.standalone_month_abbr)) + def test_standalone_month_name_survives_lazy_OB_failure(self): + # gh-155245: some platforms accept '%OB'/'%Ob' when a + # _localized_month object is constructed, but the underlying + # strftime() call -- which only happens lazily, inside set() -- + # can still raise ValueError once it actually runs (e.g. under + # Wine). Reloading the calendar module used to let that + # ValueError propagate instead of falling back to + # month_name/month_abbr. + import importlib + + real_date = datetime.date + + class FakeDate(real_date): + def strftime(self, fmt): + if '%O' in fmt: + raise ValueError('Invalid format string') + return super().strftime(fmt) + + datetime.date = FakeDate + try: + importlib.reload(calendar) + finally: + datetime.date = real_date + importlib.reload(calendar) + + self.assertEqual(calendar.standalone_month_name[1], calendar.month_name[1]) + def test_locale_text_calendar(self): try: cal = calendar.LocaleTextCalendar(locale='') diff --git a/Misc/NEWS.d/next/Library/2026-08-07-12-00-00.gh-issue-155245.aB3xY9.rst b/Misc/NEWS.d/next/Library/2026-08-07-12-00-00.gh-issue-155245.aB3xY9.rst new file mode 100644 index 00000000000000..56c3735aad075c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-07-12-00-00.gh-issue-155245.aB3xY9.rst @@ -0,0 +1 @@ +Fixed a bug where :mod:`calendar` could raise an uncaught :exc:`ValueError` on import when falling back from :data:`calendar.standalone_month_name`/:data:`calendar.standalone_month_abbr` to :data:`calendar.month_name`/:data:`calendar.month_abbr`, on platforms where ``strftime()`` accepts ``%OB``/``%Ob`` when a name object is constructed but rejects them once the value is actually formatted (e.g. under Wine).