From 2f6c0ce05e2f87f7c1867ea557160434bebe59f2 Mon Sep 17 00:00:00 2001 From: marat Date: Wed, 17 Sep 2025 02:04:35 +0300 Subject: [PATCH 01/16] gh-137165: Add non-zero-padded Windows support for datetime.strftime --- Lib/_pydatetime.py | 15 +++++++ Lib/test/datetimetester.py | 15 +++++++ ...-09-16-15-36-18.gh-issue-137165.AclPcn.rst | 2 + Modules/_datetimemodule.c | 44 +++++++++++++++++++ 4 files changed, 76 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b6d68f2372850a..50066df792fc7d 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -213,6 +213,11 @@ def _need_normalize_century(): _normalize_century = True return _normalize_century +def _make_dash_replacement(ch, timetuple): + fmt = '%' + ch + val = _time.strftime(fmt, timetuple) + return val.lstrip('0') or '0' + # Correctly substitute for %z and %Z escapes in strftime formats. def _wrap_strftime(object, format, timetuple): # Don't call utcoffset() or tzname() unless actually needed. @@ -284,6 +289,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 sys.platform.startswith('win') or sys.platform.startswith('android'): + push(_make_dash_replacement(next_ch, timetuple)) + else: + push('%-' + next_ch) + else: + push('%-') else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 2299d1fab2e73d..f509ad1cc9fafd 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -14,6 +14,7 @@ import textwrap import unittest import warnings +import platform from array import array @@ -1588,6 +1589,15 @@ def test_strftime(self): self.assertEqual(t.strftime(""), "") # SF bug #761337 self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 + # SF bug #137165 + if platform.system() == 'Darwin': + self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:05") + elif platform.system() == 'Windows': + self.assertEqual(t.strftime("m:%#m d:%#d y:%#y"), "m:3 d:2 y:5") + self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:5") + else: + self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:5") + 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 @@ -3890,6 +3900,11 @@ def test_strftime(self): # A naive object replaces %z, %:z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''") + # SF bug #137165 + self.assertEqual(t.strftime('%-H %-M %-S %f'), "1 2 3 000004") + if platform.system() == 'Windows': + self.assertEqual(t.strftime('%#H %#M %#S %f'), "1 2 3 000004") + # bpo-34482: Check that surrogates don't cause a crash. try: t.strftime('%H\ud800%M') diff --git a/Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst b/Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst new file mode 100644 index 00000000000000..bd4ab63eb19efe --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst @@ -0,0 +1,2 @@ +Add support for Windows non-zero-padded formatting directives in +:func:`datetime.datetime.strftime` (e.g., ``"m:%-m d:%-d y:%-y"``). diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 12d316985fceb9..c8cc958ebf9cee 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1856,6 +1856,39 @@ make_freplacement(PyObject *object) return PyUnicode_FromString(freplacement); } +#if defined(MS_WINDOWS) || defined(__ANDROID__) +static PyObject * +make_dash_replacement(PyObject *object, Py_UCS4 ch, PyObject *timetuple) +{ + PyObject *strftime = PyImport_ImportModuleAttrString("time", "strftime"); + if (!strftime) { + return NULL; + } + + char fmt[3] = {'%', (char)ch, 0}; + PyObject *fmt_obj = PyUnicode_FromString(fmt); + if (!fmt_obj) { + Py_DECREF(strftime); + return NULL; + } + + PyObject *res = PyObject_CallFunctionObjArgs(strftime, fmt_obj, timetuple, NULL); + Py_DECREF(fmt_obj); + Py_DECREF(strftime); + if (!res) { + return NULL; + } + + PyObject *stripped = PyObject_CallMethod(res, "lstrip", "s", "0"); + Py_DECREF(res); + if (!stripped) { + return NULL; + } + + return stripped; +} +#endif + /* I sure don't want to reproduce the strftime code from the time module, * so this imports the module and calls it. All the hair is due to * giving special meanings to the %z, %:z, %Z and %f format codes via a @@ -2002,6 +2035,17 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } continue; } + #if defined(MS_WINDOWS) || defined(__ANDROID__) + /* non-0-pad Windows support */ + else if (ch == '-' && i < flen) { + Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); + i++; + replacement = make_dash_replacement(object, next_ch, timetuple); + if (replacement == NULL) { + goto Error; + } + } + #endif else { /* percent followed by something else */ continue; From 8d169011f13a6d6b84c3131c361c1065d95fdfaf Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 18 Sep 2025 03:13:51 +0300 Subject: [PATCH 02/16] Refactor make_dash_replacement func and add negative test --- Lib/test/datetimetester.py | 5 +- ...-09-16-15-36-18.gh-issue-137165.AclPcn.rst | 2 - ...-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst | 3 ++ Modules/_datetimemodule.c | 46 +++++++++++-------- 4 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst create mode 100644 Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index f509ad1cc9fafd..5031f2629e7e95 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1592,10 +1592,9 @@ def test_strftime(self): # SF bug #137165 if platform.system() == 'Darwin': self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:05") - elif platform.system() == 'Windows': - self.assertEqual(t.strftime("m:%#m d:%#d y:%#y"), "m:3 d:2 y:5") - self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:5") else: + if platform.system() == 'Windows': + self.assertEqual(t.strftime("m:%#m d:%#d y:%#y"), "m:3 d:2 y:5") self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:5") self.assertRaises(TypeError, t.strftime) # needs an arg diff --git a/Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst b/Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst deleted file mode 100644 index bd4ab63eb19efe..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-09-16-15-36-18.gh-issue-137165.AclPcn.rst +++ /dev/null @@ -1,2 +0,0 @@ -Add support for Windows non-zero-padded formatting directives in -:func:`datetime.datetime.strftime` (e.g., ``"m:%-m d:%-d y:%-y"``). diff --git a/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst b/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst new file mode 100644 index 00000000000000..eb8c562f28dfc8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst @@ -0,0 +1,3 @@ +Standardized non-zero-padded date formatting in +:func:`datetime.datetime.strftime` and :func:`datetime.date.strftime` across +Windows and Unix. (e.g. ``"m:%-m d:%-d y:%-y"``). diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index c8cc958ebf9cee..38321a7cb5114b 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1860,32 +1860,42 @@ make_freplacement(PyObject *object) static PyObject * make_dash_replacement(PyObject *object, Py_UCS4 ch, PyObject *timetuple) { - PyObject *strftime = PyImport_ImportModuleAttrString("time", "strftime"); - if (!strftime) { - return NULL; + PyObject *strftime = NULL; + PyObject *fmt_obj = NULL; + PyObject *res = NULL; + PyObject *stripped = NULL; + + strftime = PyImport_ImportModuleAttrString("time", "strftime"); + if (strftime == NULL) { + goto error; } - char fmt[3] = {'%', (char)ch, 0}; - PyObject *fmt_obj = PyUnicode_FromString(fmt); - if (!fmt_obj) { - Py_DECREF(strftime); - return NULL; + fmt_obj = PyUnicode_FromFormat("%%%c", (char)ch); + if (fmt_obj == NULL) { + goto error; } - PyObject *res = PyObject_CallFunctionObjArgs(strftime, fmt_obj, timetuple, NULL); - Py_DECREF(fmt_obj); - Py_DECREF(strftime); - if (!res) { - return NULL; + res = PyObject_CallFunctionObjArgs(strftime, fmt_obj, timetuple, NULL); + if (res == NULL) { + goto error; } - PyObject *stripped = PyObject_CallMethod(res, "lstrip", "s", "0"); - Py_DECREF(res); - if (!stripped) { - return NULL; + stripped = PyObject_CallMethod(res, "lstrip", "s", "0"); + if (stripped == NULL) { + goto error; } + Py_DECREF(fmt_obj); + Py_DECREF(strftime); + Py_DECREF(res); return stripped; + +error: + Py_XDECREF(fmt_obj); + Py_XDECREF(strftime); + Py_XDECREF(res); + Py_XDECREF(stripped); + return NULL; } #endif @@ -2036,7 +2046,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, continue; } #if defined(MS_WINDOWS) || defined(__ANDROID__) - /* non-0-pad Windows support */ + /* non-0-pad Windows and Android support */ else if (ch == '-' && i < flen) { Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); i++; From 11e021843d37e47e3822bf091bbb88f513e022a7 Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 18 Sep 2025 16:42:13 +0300 Subject: [PATCH 03/16] Fix tests for IOS and add docs --- Doc/library/datetime.rst | 8 +++++++- Lib/test/datetimetester.py | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 7010f99c54da0a..433afa62af4168 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2762,11 +2762,17 @@ Notes: interchangeable. (9) + When used with the :meth:`~.datetime.strftime` method, the leading zero is optional + for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, + ``%W``, ``%V`` and ``%y`` (except that on Apple platforms, ``%y`` still requires a leading zero). + Use the ``%-`` flag to produce non-zero-padded output (e.g. ``%-d``). + +(10) When used with the :meth:`~.datetime.strptime` method, the leading zero is optional for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, and ``%V``. Format ``%y`` does require a leading zero. -(10) +(11) When parsing a month and day using :meth:`~.datetime.strptime`, always include a year in the format. If the value you need to parse lacks a year, append an explicit dummy leap year. Otherwise your code will raise an diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 5031f2629e7e95..dad6bdcda26d9b 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1590,13 +1590,15 @@ def test_strftime(self): self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 # SF bug #137165 - if platform.system() == 'Darwin': + if platform.system() in ('Darwin', 'iOS'): self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:05") else: if platform.system() == 'Windows': self.assertEqual(t.strftime("m:%#m d:%#d y:%#y"), "m:3 d:2 y:5") self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:5") + self.assertEqual(t.strftime("%-j. %-U. %-W. %-V."), "61. 9. 9. 9.") + 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 From eb5c88393ae921033ba845071267d3d48712edfd Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 18 Sep 2025 17:10:39 +0300 Subject: [PATCH 04/16] Update docs and news entry --- Doc/library/datetime.rst | 2 +- .../Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst | 3 --- .../Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst create mode 100644 Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 433afa62af4168..4778e4622d92c6 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2764,7 +2764,7 @@ Notes: (9) When used with the :meth:`~.datetime.strftime` method, the leading zero is optional for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, - ``%W``, ``%V`` and ``%y`` (except that on Apple platforms, ``%y`` still requires a leading zero). + ``%W``, ``%V`` and ``%y`` (except that on Apple platforms, ``%y`` always produces a leading zero). Use the ``%-`` flag to produce non-zero-padded output (e.g. ``%-d``). (10) diff --git a/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst b/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst deleted file mode 100644 index eb8c562f28dfc8..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-09-18-03-02-39.gh-issue-137165.7tJ6DS.rst +++ /dev/null @@ -1,3 +0,0 @@ -Standardized non-zero-padded date formatting in -:func:`datetime.datetime.strftime` and :func:`datetime.date.strftime` across -Windows and Unix. (e.g. ``"m:%-m d:%-d y:%-y"``). diff --git a/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst b/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst new file mode 100644 index 00000000000000..3299a7f9f00412 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst @@ -0,0 +1,3 @@ +Standardized non-zero-padded numeric formatting for dates and times in +:func:`datetime.datetime.strftime` and :func:`datetime.date.strftime` across +all platforms (e.g. ``"m:%-m d:%-d y:%-y"``). From 224654c50605d842697e3368a9bdac9c0f047575 Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 18 Sep 2025 18:45:25 +0300 Subject: [PATCH 05/16] Fix 0 seconds/minutes strip and update docs --- Doc/library/datetime.rst | 5 +++-- Lib/test/datetimetester.py | 7 +++++-- Modules/_datetimemodule.c | 4 ++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 4778e4622d92c6..6ec23be6e12bab 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2764,8 +2764,9 @@ Notes: (9) When used with the :meth:`~.datetime.strftime` method, the leading zero is optional for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, - ``%W``, ``%V`` and ``%y`` (except that on Apple platforms, ``%y`` always produces a leading zero). - Use the ``%-`` flag to produce non-zero-padded output (e.g. ``%-d``). + ``%W``, ``%V`` and ``%y`` (except that on Apple platforms, ``%y`` always produces + a leading zero). Use the ``%-`` flag to produce non-zero-padded output + (for example, ``%-d``). (10) When used with the :meth:`~.datetime.strptime` method, the leading zero is optional diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index dad6bdcda26d9b..40120c36fa61e9 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1589,7 +1589,7 @@ def test_strftime(self): self.assertEqual(t.strftime(""), "") # SF bug #761337 self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 - # SF bug #137165 + # See gh-137165 if platform.system() in ('Darwin', 'iOS'): self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:05") else: @@ -3901,11 +3901,14 @@ def test_strftime(self): # A naive object replaces %z, %:z and %Z with empty strings. self.assertEqual(t.strftime("'%z' '%:z' '%Z'"), "'' '' ''") - # SF bug #137165 + # See gh-137165 self.assertEqual(t.strftime('%-H %-M %-S %f'), "1 2 3 000004") if platform.system() == 'Windows': self.assertEqual(t.strftime('%#H %#M %#S %f'), "1 2 3 000004") + t_zero = self.theclass(0, 0, 0, 4) + self.assertEqual(t_zero.strftime('%-H %-M %-S %f'), "0 0 0 000004") + # bpo-34482: Check that surrogates don't cause a crash. try: t.strftime('%H\ud800%M') diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 38321a7cb5114b..25add396a22eef 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1885,6 +1885,10 @@ make_dash_replacement(PyObject *object, Py_UCS4 ch, PyObject *timetuple) goto error; } + if (PyUnicode_GET_LENGTH(stripped) == 0) { + stripped = PyUnicode_FromString("0"); + } + Py_DECREF(fmt_obj); Py_DECREF(strftime); Py_DECREF(res); From 551f16e36774f597939a5d24cbd418f6c21d572e Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 18 Sep 2025 19:15:48 +0300 Subject: [PATCH 06/16] Update news entry --- .../next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst b/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst index 3299a7f9f00412..13ed317e60dfb8 100644 --- a/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst +++ b/Misc/NEWS.d/next/Library/2025-09-18-17-08-08.gh-issue-137165.sJase0.rst @@ -1,3 +1,3 @@ Standardized non-zero-padded numeric formatting for dates and times in :func:`datetime.datetime.strftime` and :func:`datetime.date.strftime` across -all platforms (e.g. ``"m:%-m d:%-d y:%-y"``). +all platforms. From ddc75ed650ca2282acceb1a5cb433f92aef555ed Mon Sep 17 00:00:00 2001 From: marat Date: Fri, 19 Sep 2025 07:46:54 +0300 Subject: [PATCH 07/16] Unify strftime/ptime notes and refactor code --- Doc/library/datetime.rst | 13 ++++++------- Lib/_pydatetime.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 6ec23be6e12bab..afb4bbc03cff17 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2762,17 +2762,16 @@ Notes: interchangeable. (9) - When used with the :meth:`~.datetime.strftime` method, the leading zero is optional - for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, - ``%W``, ``%V`` and ``%y`` (except that on Apple platforms, ``%y`` always produces - a leading zero). Use the ``%-`` flag to produce non-zero-padded output - (for example, ``%-d``). - -(10) When used with the :meth:`~.datetime.strptime` method, the leading zero is optional for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, and ``%V``. Format ``%y`` does require a leading zero. + When used with the :meth:`~.datetime.strftime` method, the leading zero is optional + for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, + ``%W``, ``%V`` and ``%y``. The ``%-`` flag (for example, ``%-d``) will produce + non-zero-padded output, except for ``%-y`` on Apple platforms, which is still + zero-padded. + (11) When parsing a month and day using :meth:`~.datetime.strptime`, always include a year in the format. If the value you need to parse lacks a year, diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 50066df792fc7d..3475c2740931d6 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -293,7 +293,7 @@ def _wrap_strftime(object, format, timetuple): if i < n: next_ch = format[i] i += 1 - if sys.platform.startswith('win') or sys.platform.startswith('android'): + if sys.platform in ['win32', 'android']: push(_make_dash_replacement(next_ch, timetuple)) else: push('%-' + next_ch) From a51dad631ac7caafd45e14e2d3b495d9c4233ca8 Mon Sep 17 00:00:00 2001 From: marat Date: Fri, 19 Sep 2025 07:47:59 +0300 Subject: [PATCH 08/16] Update notes numbers --- Doc/library/datetime.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index afb4bbc03cff17..f047eb99f27417 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2772,7 +2772,7 @@ Notes: non-zero-padded output, except for ``%-y`` on Apple platforms, which is still zero-padded. -(11) +(10) When parsing a month and day using :meth:`~.datetime.strptime`, always include a year in the format. If the value you need to parse lacks a year, append an explicit dummy leap year. Otherwise your code will raise an From 0e5ef8b6389aac0dae25db5da29cf41c70b49b5f Mon Sep 17 00:00:00 2001 From: marat Date: Fri, 19 Sep 2025 13:46:26 +0300 Subject: [PATCH 09/16] Update datetime.rst --- Doc/library/datetime.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index f047eb99f27417..53736540e6431a 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2766,8 +2766,8 @@ Notes: for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, and ``%V``. Format ``%y`` does require a leading zero. - When used with the :meth:`~.datetime.strftime` method, the leading zero is optional - for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, + When used with the :meth:`~.datetime.strftime` method, leading zeroes are included + by default for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, ``%V`` and ``%y``. The ``%-`` flag (for example, ``%-d``) will produce non-zero-padded output, except for ``%-y`` on Apple platforms, which is still zero-padded. From 375cf41de30a7dbc0befe73dc073989411535bdf Mon Sep 17 00:00:00 2001 From: marat Date: Tue, 23 Sep 2025 18:49:25 +0300 Subject: [PATCH 10/16] Fix refleaks and tests for FreeBSD --- Doc/library/datetime.rst | 4 ++-- Lib/test/datetimetester.py | 2 +- Modules/_datetimemodule.c | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 53736540e6431a..7c169bac5b2e3b 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2769,8 +2769,8 @@ Notes: When used with the :meth:`~.datetime.strftime` method, leading zeroes are included by default for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, ``%V`` and ``%y``. The ``%-`` flag (for example, ``%-d``) will produce - non-zero-padded output, except for ``%-y`` on Apple platforms, which is still - zero-padded. + non-zero-padded output, except for ``%-y`` on Apple platforms and FreeBSD, + which is still zero-padded. (10) When parsing a month and day using :meth:`~.datetime.strptime`, always diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 40120c36fa61e9..618bbce6b931d3 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1590,7 +1590,7 @@ def test_strftime(self): self.assertEqual(t.strftime('x'*1000), 'x'*1000) # SF bug #1556784 # See gh-137165 - if platform.system() in ('Darwin', 'iOS'): + if platform.system() in ('Darwin', 'iOS', 'FreeBSD'): self.assertEqual(t.strftime("m:%-m d:%-d y:%-y"), "m:3 d:2 y:05") else: if platform.system() == 'Windows': diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 25add396a22eef..df5c7083c32542 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1886,6 +1886,7 @@ make_dash_replacement(PyObject *object, Py_UCS4 ch, PyObject *timetuple) } if (PyUnicode_GET_LENGTH(stripped) == 0) { + Py_DECREF(stripped); stripped = PyUnicode_FromString("0"); } From 40d0e1c149dbff7ff31a2e78be4b7c4bf5317ff0 Mon Sep 17 00:00:00 2001 From: marat Date: Wed, 24 Sep 2025 10:48:57 +0300 Subject: [PATCH 11/16] Fix refleaks --- Modules/_datetimemodule.c | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index df5c7083c32542..950591b3415603 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2055,10 +2055,25 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, else if (ch == '-' && i < flen) { Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); i++; - replacement = make_dash_replacement(object, next_ch, timetuple); - if (replacement == NULL) { + + PyObject *tmp = make_dash_replacement(object, next_ch, timetuple); + if (tmp == NULL) { + Py_DECREF(tmp); goto Error; } + + if (PyUnicodeWriter_WriteSubstring(writer, format, start, end) < 0) { + Py_DECREF(tmp); + goto Error; + } + start = i; + if (PyUnicodeWriter_WriteStr(writer, tmp) < 0) { + Py_DECREF(tmp); + goto Error; + } + + Py_DECREF(tmp); + continue; } #endif else { From 2054356e5e96ff1882d25ec45316c0101f07cd97 Mon Sep 17 00:00:00 2001 From: marat Date: Wed, 24 Sep 2025 11:03:29 +0300 Subject: [PATCH 12/16] Fix linting --- Modules/_datetimemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 950591b3415603..22c3186c746073 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -2055,7 +2055,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, else if (ch == '-' && i < flen) { Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); i++; - + PyObject *tmp = make_dash_replacement(object, next_ch, timetuple); if (tmp == NULL) { Py_DECREF(tmp); From 6ff5f4f385b895bacf6b01d7eb2c8b5aa4d6276f Mon Sep 17 00:00:00 2001 From: marat Date: Thu, 25 Sep 2025 21:27:57 +0300 Subject: [PATCH 13/16] Refactor wrap_strftime func --- Modules/_datetimemodule.c | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 22c3186c746073..7f55bc50b78fcb 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1959,6 +1959,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, i++; /* A % has been seen and ch is the character after it. */ PyObject *replacement = NULL; + int need_decref_replacement = 0; + if (ch == 'z') { /* %z -> +HHMM */ if (zreplacement == NULL) { @@ -2058,22 +2060,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *tmp = make_dash_replacement(object, next_ch, timetuple); if (tmp == NULL) { - Py_DECREF(tmp); - goto Error; - } - - if (PyUnicodeWriter_WriteSubstring(writer, format, start, end) < 0) { - Py_DECREF(tmp); - goto Error; - } - start = i; - if (PyUnicodeWriter_WriteStr(writer, tmp) < 0) { - Py_DECREF(tmp); goto Error; } - Py_DECREF(tmp); - continue; + replacement = tmp; + need_decref_replacement = 1; } #endif else { @@ -2083,12 +2074,21 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, assert(replacement != NULL); assert(PyUnicode_Check(replacement)); if (PyUnicodeWriter_WriteSubstring(writer, format, start, end) < 0) { + if (need_decref_replacement) { + Py_DECREF(replacement); + } goto Error; } start = i; if (PyUnicodeWriter_WriteStr(writer, replacement) < 0) { + if (need_decref_replacement) { + Py_DECREF(replacement); + } goto Error; } + if (need_decref_replacement) { + Py_DECREF(replacement); + } } /* end while() */ PyObject *newformat; From d5f2014681d7d9d8eb2a692388ba9993d043e2a3 Mon Sep 17 00:00:00 2001 From: marat Date: Fri, 26 Sep 2025 16:05:20 +0300 Subject: [PATCH 14/16] Make replacement less complex --- Modules/_datetimemodule.c | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 7f55bc50b78fcb..6d76bc10ed3de9 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1921,6 +1921,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */ PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ + PyObject *dash_replacement = NULL; /* py string, replacement for %- */ assert(object && format && timetuple); assert(PyUnicode_Check(format)); @@ -2058,13 +2059,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_UCS4 next_ch = PyUnicode_READ_CHAR(format, i); i++; - PyObject *tmp = make_dash_replacement(object, next_ch, timetuple); - if (tmp == NULL) { + Py_XDECREF(dash_replacement); + dash_replacement = make_dash_replacement(object, next_ch, timetuple); + if (dash_replacement == NULL) { goto Error; } - - replacement = tmp; - need_decref_replacement = 1; + replacement = dash_replacement; } #endif else { @@ -2074,21 +2074,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, assert(replacement != NULL); assert(PyUnicode_Check(replacement)); if (PyUnicodeWriter_WriteSubstring(writer, format, start, end) < 0) { - if (need_decref_replacement) { - Py_DECREF(replacement); - } goto Error; } start = i; if (PyUnicodeWriter_WriteStr(writer, replacement) < 0) { - if (need_decref_replacement) { - Py_DECREF(replacement); - } goto Error; } - if (need_decref_replacement) { - Py_DECREF(replacement); - } } /* end while() */ PyObject *newformat; @@ -2115,6 +2106,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_XDECREF(colonzreplacement); Py_XDECREF(Zreplacement); Py_XDECREF(strftime); + Py_XDECREF(dash_replacement); return result; Error: From 0d53cedac90a0ad7810540d8c8cd30a1353dc44b Mon Sep 17 00:00:00 2001 From: marat Date: Fri, 26 Sep 2025 16:29:13 +0300 Subject: [PATCH 15/16] Remove useless variable --- Modules/_datetimemodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 6d76bc10ed3de9..98fa5eb62f156a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1960,7 +1960,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, i++; /* A % has been seen and ch is the character after it. */ PyObject *replacement = NULL; - int need_decref_replacement = 0; if (ch == 'z') { /* %z -> +HHMM */ From 9212a5affcd796423a0b9fafd99a77e578c7840d Mon Sep 17 00:00:00 2001 From: marat Date: Wed, 1 Oct 2025 23:32:15 +0300 Subject: [PATCH 16/16] Docs formatting and code style --- Doc/library/datetime.rst | 9 +++++---- Modules/_datetimemodule.c | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 7c169bac5b2e3b..6c92a2505f07d3 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -2766,10 +2766,11 @@ Notes: for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, and ``%V``. Format ``%y`` does require a leading zero. - When used with the :meth:`~.datetime.strftime` method, leading zeroes are included - by default for formats ``%d``, ``%m``, ``%H``, ``%I``, ``%M``, ``%S``, ``%j``, ``%U``, - ``%W``, ``%V`` and ``%y``. The ``%-`` flag (for example, ``%-d``) will produce - non-zero-padded output, except for ``%-y`` on Apple platforms and FreeBSD, + When used with the :meth:`~.datetime.strftime` method, leading zeroes + are included by default for formats ``%d``, ``%m``, ``%H``, ``%I``, + ``%M``, ``%S``, ``%j``, ``%U``, ``%W``, ``%V`` and ``%y``. + The ``%-`` flag (for example, ``%-d``) will produce non-zero-padded + output, except for ``%-y`` on Apple platforms and FreeBSD, which is still zero-padded. (10) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 98fa5eb62f156a..7bab779a37d15a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1960,7 +1960,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, i++; /* A % has been seen and ch is the character after it. */ PyObject *replacement = NULL; - if (ch == 'z') { /* %z -> +HHMM */ if (zreplacement == NULL) { @@ -2104,8 +2103,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_XDECREF(zreplacement); Py_XDECREF(colonzreplacement); Py_XDECREF(Zreplacement); - Py_XDECREF(strftime); Py_XDECREF(dash_replacement); + Py_XDECREF(strftime); return result; Error: