From 1c23f5ac909480ee9afd76d2dac0037935d45def Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Fri, 26 Jun 2026 22:20:30 +0800 Subject: [PATCH 1/3] gh-152305: Fix pure-Python time.strftime AttributeError on %Y/%G/%C/%F --- Lib/_pydatetime.py | 4 ++-- Lib/test/datetimetester.py | 7 +++++++ .../Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst | 3 +++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index db4ea8d30c7064f..47ed2b75a486f46 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -273,11 +273,11 @@ def _wrap_strftime(object, format, timetuple): # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so # year 1000 for %G can go on the fast path. elif ((ch in 'YG' or ch in 'FC') and - object.year < 1000 and _need_normalize_century()): + timetuple[0] < 1000 and _need_normalize_century()): if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: - year = object.year + year = timetuple[0] if ch == 'C': push('{:02}'.format(year // 100)) else: diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 28c3ab2605c45db..bdc8b4d1af6e269 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -4084,6 +4084,13 @@ def test_strftime(self): # A naive object replaces %z, %:z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''") + # gh-152305: the year directives must not raise on a time (1900-01-01). + t1230 = self.theclass(12, 30) + self.assertEqual(t1230.strftime('%Y'), '1900') + self.assertEqual(t1230.strftime('%G'), '1900') + self.assertEqual(t1230.strftime('%C'), '19') + self.assertEqual(t1230.strftime('%F'), '1900-01-01') + # bpo-34482: Check that surrogates don't cause a crash. try: t.strftime('%H\ud800%M') diff --git a/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst b/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst new file mode 100644 index 000000000000000..9cd4da60be11892 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst @@ -0,0 +1,3 @@ +Fix :meth:`datetime.time.strftime` raising :exc:`AttributeError` for the +``%Y``, ``%G``, ``%C`` and ``%F`` directives in the pure-Python +implementation. Patch by tonghuaroot. From 677b8a5e3b800f852289ac0c8f139107ef109d8b Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Sat, 27 Jun 2026 11:57:46 +0100 Subject: [PATCH 2/3] Use `subTest`, move to `test_stftime_special` --- Lib/test/datetimetester.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index bdc8b4d1af6e269..192b22ff7540034 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -4084,13 +4084,6 @@ def test_strftime(self): # A naive object replaces %z, %:z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''") - # gh-152305: the year directives must not raise on a time (1900-01-01). - t1230 = self.theclass(12, 30) - self.assertEqual(t1230.strftime('%Y'), '1900') - self.assertEqual(t1230.strftime('%G'), '1900') - self.assertEqual(t1230.strftime('%C'), '19') - self.assertEqual(t1230.strftime('%F'), '1900-01-01') - # bpo-34482: Check that surrogates don't cause a crash. try: t.strftime('%H\ud800%M') @@ -4126,6 +4119,11 @@ def test_strftime_special(self): self.assertEqual(t.strftime('\0'*1000), '\0'*1000) self.assertEqual(t.strftime('\0%I%p%Z\0%X'), f'\0{s1}\0{s2}') self.assertEqual(t.strftime('%I%p%Z\0%X\0'), f'{s1}\0{s2}\0') + # gh-152305: the year directives must not raise on a time. + for directive, expected in (('%Y', '1900'), ('%G', '1900'), + ('%C', '19'), ('%F', '1900-01-01')): + with self.subTest(directive=directive): + self.assertEqual(t.strftime(directive), expected) def test_format(self): t = self.theclass(1, 2, 3, 4) From 955c6f79f826f674b263cab8fedae9aaf633b9b1 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sat, 27 Jun 2026 19:27:20 +0800 Subject: [PATCH 3/3] Address review: simplify the year-directive check and reword the NEWS entry --- Lib/_pydatetime.py | 4 ++-- .../Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 47ed2b75a486f46..c47f4e671b39def 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -272,8 +272,8 @@ def _wrap_strftime(object, format, timetuple): newformat.append(Zreplace) # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so # year 1000 for %G can go on the fast path. - elif ((ch in 'YG' or ch in 'FC') and - timetuple[0] < 1000 and _need_normalize_century()): + elif (ch in 'YGFC' and timetuple[0] < 1000 and + _need_normalize_century()): if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: diff --git a/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst b/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst index 9cd4da60be11892..4f27e2ed016d694 100644 --- a/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst +++ b/Misc/NEWS.d/next/Library/2026-06-26-15-41-34.gh-issue-152305.WnbbBc.rst @@ -1,3 +1,2 @@ -Fix :meth:`datetime.time.strftime` raising :exc:`AttributeError` for the -``%Y``, ``%G``, ``%C`` and ``%F`` directives in the pure-Python -implementation. Patch by tonghuaroot. +Fix the pure-Python :meth:`datetime.time.strftime` implementation raising :exc:`AttributeError` for the +year directives. Patch by tonghuaroot.