From b8dd4abbd11e3075f41146171d15c50e8b1b6283 Mon Sep 17 00:00:00 2001 From: sudeep Date: Fri, 7 Aug 2026 11:33:56 +0530 Subject: [PATCH 1/4] gh-155245: Fix calendar failing to import when strftime rejects %OB The strftime() call for standalone month names ('%OB'/'%Ob') is evaluated lazily inside set(), outside the try/except that guards the initial _localized_month construction. On platforms where '%O' is accepted at construction but rejected when actually formatting (e.g. under Wine), this raised an uncaught ValueError. Wrap the set() comparisons in their own try/except so we fall back to month_name/month_abbr in that case too. --- Lib/calendar.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Lib/calendar.py b/Lib/calendar.py index 92fe6b7723fe268..de467a7e8367b5e 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): From bcf985c78e93ab9400372fad3a1ddea997bd3307 Mon Sep 17 00:00:00 2001 From: sudeep Date: Fri, 7 Aug 2026 11:58:42 +0530 Subject: [PATCH 2/4] gh-155245: Add regression test for lazy %OB strftime failure Adds test_standalone_month_name_survives_lazy_OB_failure to OutputTestCase. It simulates a platform (like Wine) where strftime() accepts '%OB' when a _localized_month is constructed but raises ValueError once the lazy strftime call actually happens inside set(), by reloading the calendar module with a patched datetime.date. Without the fix in calendar.py this test fails with an uncaught ValueError. --- Lib/test/test_calendar.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index 8646cfcad58cea8..55d59cbb219f9f3 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='') From db898482c8a88118597e1f241afa519f403f4dcf Mon Sep 17 00:00:00 2001 From: sudeep Date: Fri, 7 Aug 2026 12:03:55 +0530 Subject: [PATCH 3/4] gh-155245: Add NEWS entry for calendar %OB fix --- .../next/Library/2026-08-07-12-00-00.gh-issue-155245.aB3xY9.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-07-12-00-00.gh-issue-155245.aB3xY9.rst 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 000000000000000..56c3735aad075ce --- /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). From 3c1517574aa8cfc603e3747e20b6a46c876ba803 Mon Sep 17 00:00:00 2001 From: sudeep Date: Fri, 7 Aug 2026 13:06:30 +0530 Subject: [PATCH 4/4] gh-155245: Remove trailing whitespace in test_calendar.py --- Lib/test/test_calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_calendar.py b/Lib/test/test_calendar.py index 55d59cbb219f9f3..04f878124326cb0 100644 --- a/Lib/test/test_calendar.py +++ b/Lib/test/test_calendar.py @@ -670,7 +670,7 @@ def strftime(self, fmt): 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='')