From b77759e01e81d961467e7f50ef5cb0ea72371721 Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 19 Feb 2026 22:44:52 +0300 Subject: [PATCH 1/4] Unify behavior of unsupported %-format specifiers --- Lib/_pydatetime.py | 10 ++++++++++ Lib/test/datetimetester.py | 6 ++++++ Modules/_datetimemodule.c | 14 ++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a7..97f359d7866aa2f 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -284,6 +284,16 @@ def _wrap_strftime(object, format, timetuple): push('{:04}'.format(year)) if ch == 'F': push('-{:02}-{:02}'.format(*timetuple[1:3])) + elif ch == '-': + if i < n: + next_ch = format[i] + i += 1 + if next_ch not in 'dmHIMSjUWVy': + push('%%-' + next_ch) + else: + push('%-' + next_ch) + else: + push('%-') else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 97eec618932aa56..2e9cbbc01ee7b1c 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -15,6 +15,7 @@ import textwrap import unittest import warnings +import platform from array import array @@ -1589,6 +1590,11 @@ def test_strftime(self): self.assertEqual(t.strftime(""), "") # SF bug #761337 self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + # unsupported %-format specifiers are passed through unchanged. + self.assertEqual(t.strftime("%-1"), "%-1") + self.assertEqual(t.strftime("%--"), "%--") + self.assertEqual(t.strftime("%-#"), "%-#") + self.assertRaises(TypeError, t.strftime) # needs an arg self.assertRaises(TypeError, t.strftime, "one", "two") # too many args self.assertRaises(TypeError, t.strftime, 42) # arg wrong type diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 8f64e572bd6086e..f5ad660202d68af 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2003,6 +2003,20 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } continue; } + else if (ch == '-' && i < flen) { + Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); + i++; + + if (strchr("dmHIMSjUWVy", (int)next_ch) == NULL) { + replacement = PyUnicode_FromFormat("%%%%-%c", (char)next_ch); + if (replacement == NULL) { + goto Error; + } + } + else { + continue; + } + } else { /* percent followed by something else */ continue; From 4b09c8e3f8c19f25d028900657110eb6d4b5048c Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 19 Feb 2026 23:01:18 +0300 Subject: [PATCH 2/4] Remove platform from tests --- Lib/test/datetimetester.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 2e9cbbc01ee7b1c..1edec59d71764c6 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -15,7 +15,6 @@ import textwrap import unittest import warnings -import platform from array import array From 70394a30da19d22d2c3b804cccb3126289107600 Mon Sep 17 00:00:00 2001 From: Lukasz Langa Date: Wed, 21 Oct 2026 03:03:23 +0200 Subject: [PATCH 3/4] Add NEWS entry for gh-137165 Co-authored-by: Muse Code (Meta Muse Spark) --- .../Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst b/Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst new file mode 100644 index 000000000000000..713c66b59e72a5e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-21-02-40-00.gh-issue-137165.QXa3Pg.rst @@ -0,0 +1,4 @@ +Unify :meth:`datetime.date.strftime`, :meth:`datetime.time.strftime` and +:meth:`datetime.datetime.strftime` behavior for unsupported non-zero-padded +(``%-``) format specifiers across platforms: unknown specifiers are now echoed +back literally instead of raising :exc:`ValueError` or returning partial output. From 1760b849378947e4e2293bac04c68fcbe82f5298 Mon Sep 17 00:00:00 2001 From: Lukasz Langa Date: Wed, 21 Oct 2026 11:47:47 +0200 Subject: [PATCH 4/4] gh-137165: Echo non-ASCII after `%-` literally in C strftime strchr() and (char) narrowing misclassified non-ASCII conversions: characters like U+0164 (low byte 'd') were passed through to the platform strftime, U+0080-U+00FF raised OverflowError, and the rest were echoed as mojibake. Only ASCII can name a conversion, so echo anything else back literally with the full code point, matching the pure-Python implementation. Add regression tests. Co-authored-by: Muse Code (Meta Muse Spark) --- Lib/test/datetimetester.py | 4 ++++ Modules/_datetimemodule.c | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index a3a4f61896f677c..6d19a420a11fcb5 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1602,6 +1602,10 @@ def test_strftime(self): self.assertEqual(t.strftime("%-1"), "%-1") self.assertEqual(t.strftime("%--"), "%--") self.assertEqual(t.strftime("%-#"), "%-#") + # non-ASCII specifiers are unsupported too: echo without narrowing. + self.assertEqual(t.strftime("%-é"), "%-é") + self.assertEqual(t.strftime("%-Ť"), "%-Ť") # low byte aliases 'd' + self.assertEqual(t.strftime("%-中"), "%-中") self.assertRaises(TypeError, t.strftime) # needs an arg self.assertRaises(TypeError, t.strftime, "one", "two") # too many args diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 0f4e2c30b620381..8ed0ccfb86d1d3f 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2051,8 +2051,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); i++; - if (strchr("dmHIMSjUWVy", (int)next_ch) == NULL) { - replacement = PyUnicode_FromFormat("%%%%-%c", (char)next_ch); + /* Only ASCII can name a conversion: strchr() would narrow + anything else to a byte, so echo it back literally. */ + if (next_ch > 127 || + strchr("dmHIMSjUWVy", (int)next_ch) == NULL) { + replacement = PyUnicode_FromFormat("%%%%-%c", (int)next_ch); if (replacement == NULL) { goto Error; }