From 9022177bf33a0a1a212c5336e2884b22838167a4 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 21 Jun 2024 06:27:32 +0000 Subject: [PATCH 01/56] gh-120713: normalize year with century for datetime.strftime --- Lib/_pydatetime.py | 16 +++++++++++ Lib/test/datetimetester.py | 11 +------- Modules/_datetimemodule.c | 7 +++++ configure | 54 ++++++++++++++++++++++++++++++++++++++ configure.ac | 30 +++++++++++++++++++++ pyconfig.h.in | 3 +++ 6 files changed, 111 insertions(+), 10 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 34ccb2da13d0f3..e147af90b441f0 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -204,6 +204,18 @@ def _format_offset(off, sep=':'): s += '.%06d' % ss.microseconds return s +_normalize_century = None +def _need_normalize_century(): + global _normalize_century + if _normalize_century is None: + try: + _normalize_century = ( + _time.strftime("%Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) == "99" and + _time.strftime("%4Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) == "0099") + except ValueError: + _normalize_century = False + return _normalize_century + # 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. @@ -261,6 +273,10 @@ def _wrap_strftime(object, format, timetuple): # strftime is going to have at this: escape % Zreplace = s.replace('%', '%%') newformat.append(Zreplace) + elif ch == 'Y' and _need_normalize_century(): + push('%') + push('4') + push(ch) else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e55b738eb4a975..c4edace88d3522 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1699,16 +1699,7 @@ def test_bool(self): def test_strftime_y2k(self): for y in (1, 49, 70, 99, 100, 999, 1000, 1970): d = self.theclass(y, 1, 1) - # Issue 13305: For years < 1000, the value is not always - # padded to 4 digits across platforms. The C standard - # assumes year >= 1900, so it does not specify the number - # of digits. - if d.strftime("%Y") != '%04d' % y: - # Year 42 returns '42', not padded - self.assertEqual(d.strftime("%Y"), '%d' % y) - # '0042' is obtained anyway - if support.has_strftime_extensions: - self.assertEqual(d.strftime("%4Y"), '%04d' % y) + self.assertEqual(d.strftime("%Y"), '%04d' % y) def test_replace(self): cls = self.theclass diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 31bf641152d803..27ecd2a7969ac0 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1939,6 +1939,13 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, ptoappend = PyBytes_AS_STRING(freplacement); ntoappend = PyBytes_GET_SIZE(freplacement); } +#ifdef NORMALIZE_CENTURY + else if (ch == 'Y') { + /* default to 4 digits for year with century */ + ptoappend = "%4Y"; + ntoappend = 3; + } +#endif else { /* percent followed by something else */ ptoappend = pin - 2; diff --git a/configure b/configure index 0f7ea7dfb5259d..c6f0c304ae8610 100755 --- a/configure +++ b/configure @@ -25871,6 +25871,60 @@ printf "%s\n" "#define HAVE_STAT_TV_NSEC2 1" >>confdefs.h fi +# Check if year with century should be normalized for strftime +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether year with century should be normalized for strftime" >&5 +printf %s "checking whether year with century should be normalized for strftime... " >&6; } +if test ${ac_cv_normalize_century+y} +then : + printf %s "(cached) " >&6 +else $as_nop + +if test "$cross_compiling" = yes +then : + ac_cv_normalize_century=no +else $as_nop + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#include +#include +#include + +int main(void) +{ + char year[5]; + struct tm date = { + .tm_year = -1801, + .tm_mon = 0, + .tm_mday = 1 + }; + if (strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "99") && + strftime(year, sizeof year, "%4Y", &date) && !strcmp(year, "0099")) + exit(0); + exit(1); +} + +_ACEOF +if ac_fn_c_try_run "$LINENO" +then : + ac_cv_normalize_century=yes +else $as_nop + ac_cv_normalize_century=no +fi +rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ + conftest.$ac_objext conftest.beam conftest.$ac_ext +fi + +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_normalize_century" >&5 +printf "%s\n" "$ac_cv_normalize_century" >&6; } +if test "$ac_cv_normalize_century" = yes +then + +printf "%s\n" "#define NORMALIZE_CENTURY 1" >>confdefs.h + +fi + have_curses=no have_panel=no diff --git a/configure.ac b/configure.ac index a4698451465155..de607aabaac93c 100644 --- a/configure.ac +++ b/configure.ac @@ -6572,6 +6572,36 @@ then [Define if you have struct stat.st_mtimensec]) fi +# Check if year with century should be normalized for strftime +AC_CACHE_CHECK([whether year with century should be normalized for strftime], [ac_cv_normalize_century], [ +AC_RUN_IFELSE([AC_LANG_SOURCE([[ +#include +#include +#include + +int main(void) +{ + char year[5]; + struct tm date = { + .tm_year = -1801, + .tm_mon = 0, + .tm_mday = 1 + }; + if (strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "99") && + strftime(year, sizeof year, "%4Y", &date) && !strcmp(year, "0099")) + exit(0); + exit(1); +} +]])], +[ac_cv_normalize_century=yes], +[ac_cv_normalize_century=no], +[ac_cv_normalize_century=no])]) +if test "$ac_cv_normalize_century" = yes +then + AC_DEFINE([NORMALIZE_CENTURY], [1], + [Define if year with century should be normalized for strftime.]) +fi + dnl check for ncurses/ncursesw and panel/panelw dnl NOTE: old curses is not detected. dnl have_curses=[no, ncursesw, ncurses] diff --git a/pyconfig.h.in b/pyconfig.h.in index c279b147db3bdd..86322b02a6a337 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1222,6 +1222,9 @@ /* Define if you have struct stat.st_mtimensec */ #undef HAVE_STAT_TV_NSEC2 +/* Define if year with century should be normalized for strftime. */ +#undef NORMALIZE_CENTURY + /* Define to 1 if you have the header file. */ #undef HAVE_STDINT_H From a1592e3f98ba5f3ebc3a2f4922599f77eb3454f4 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 21 Jun 2024 06:37:52 +0000 Subject: [PATCH 02/56] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst diff --git a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst new file mode 100644 index 00000000000000..dcc2044bbde182 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst @@ -0,0 +1 @@ +:func:`datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifier ``%Y`` on Linux. From 48affbd2f2ee07d61ddf67d57b3210166f283289 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 21 Jun 2024 06:50:33 +0000 Subject: [PATCH 03/56] gh-120713: fixed pyconfig.h.in --- pyconfig.h.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyconfig.h.in b/pyconfig.h.in index 86322b02a6a337..f91137cf8be044 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1222,9 +1222,6 @@ /* Define if you have struct stat.st_mtimensec */ #undef HAVE_STAT_TV_NSEC2 -/* Define if year with century should be normalized for strftime. */ -#undef NORMALIZE_CENTURY - /* Define to 1 if you have the header file. */ #undef HAVE_STDINT_H @@ -1594,6 +1591,9 @@ /* Define if mvwdelch in curses.h is an expression. */ #undef MVWDELCH_IS_EXPRESSION +/* Define if year with century should be normalized for strftime. */ +#undef NORMALIZE_CENTURY + /* Define to the address where bug reports for this package should be sent. */ #undef PACKAGE_BUGREPORT From 2ff6c0bc95f9d33a7fff6923af864861266aba3e Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 21 Jun 2024 15:03:45 +0800 Subject: [PATCH 04/56] Update 2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst --- .../next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst index dcc2044bbde182..c991ec4999711f 100644 --- a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst +++ b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst @@ -1 +1,2 @@ :func:`datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifier ``%Y`` on Linux. +Patch by Ben Hsing From ab39dd940a015e6147d0575dc79b50eca8ad4c6e Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 21 Jun 2024 19:29:22 +0800 Subject: [PATCH 05/56] Update datetimetester.py reverted unit test --- Lib/test/datetimetester.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index c4edace88d3522..541ac82ac89e47 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1699,7 +1699,16 @@ def test_bool(self): def test_strftime_y2k(self): for y in (1, 49, 70, 99, 100, 999, 1000, 1970): d = self.theclass(y, 1, 1) - self.assertEqual(d.strftime("%Y"), '%04d' % y) + # Issue 13305: For years < 1000, the value is not always + # padded to 4 digits across platforms. The C standard + # assumes year >= 1900, so it does not specify the number + # of digits. + if d.strftime("%Y") != '%04d' % y: + # Year 42 returns '42', not padded + self.assertEqual(d.strftime("%Y"), '%d' % y) + # '0042' is obtained anyway + if support.has_strftime_extensions: + self.assertEqual(d.strftime("%4Y"), '%04d' % y) def test_replace(self): cls = self.theclass From b905a4e8891879784bff6d1ee3ca37cf182b797a Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 21 Jun 2024 19:29:58 +0800 Subject: [PATCH 06/56] Update datetimetester.py --- Lib/test/datetimetester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 541ac82ac89e47..e55b738eb4a975 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1699,7 +1699,7 @@ def test_bool(self): def test_strftime_y2k(self): for y in (1, 49, 70, 99, 100, 999, 1000, 1970): d = self.theclass(y, 1, 1) - # Issue 13305: For years < 1000, the value is not always + # Issue 13305: For years < 1000, the value is not always # padded to 4 digits across platforms. The C standard # assumes year >= 1900, so it does not specify the number # of digits. From b43858e1401194ad1a5e8b739b206b69048221de Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 21 Jun 2024 22:09:21 +0800 Subject: [PATCH 07/56] Update _pydatetime.py --- Lib/_pydatetime.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index e147af90b441f0..933cca3db89c1c 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -274,9 +274,7 @@ def _wrap_strftime(object, format, timetuple): Zreplace = s.replace('%', '%%') newformat.append(Zreplace) elif ch == 'Y' and _need_normalize_century(): - push('%') - push('4') - push(ch) + push('%4Y') else: push('%') push(ch) From 6ca887e17c41214f06408b962046a5be44bbad91 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 02:39:53 +0000 Subject: [PATCH 08/56] gh-120713: format "%Y" with "%4d" using sprintf instead of replacing the specifier with "%4Y" --- Lib/_pydatetime.py | 5 ++--- Modules/_datetimemodule.c | 11 ++++++++--- configure | 5 +---- configure.ac | 5 +---- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 933cca3db89c1c..8856923df20aca 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -210,8 +210,7 @@ def _need_normalize_century(): if _normalize_century is None: try: _normalize_century = ( - _time.strftime("%Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) == "99" and - _time.strftime("%4Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) == "0099") + _time.strftime("%Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) == "99") except ValueError: _normalize_century = False return _normalize_century @@ -274,7 +273,7 @@ def _wrap_strftime(object, format, timetuple): Zreplace = s.replace('%', '%%') newformat.append(Zreplace) elif ch == 'Y' and _need_normalize_century(): - push('%4Y') + push('{:04}'.format(object.year)) else: push('%') push(ch) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 27ecd2a7969ac0..a57b45c404bd5f 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1941,9 +1941,14 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } #ifdef NORMALIZE_CENTURY else if (ch == 'Y') { - /* default to 4 digits for year with century */ - ptoappend = "%4Y"; - ntoappend = 3; + /* 0-pad year with century on platforms that do not do so */ + PyObject *year = PyObject_GetAttrString(object, "year"); + if (year == NULL) + goto Done; + char formatted[5]; + ntoappend = sprintf(formatted, "%04d", (int)PyLong_AsLong(year)); + ptoappend = formatted; + Py_DECREF(year); } #endif else { diff --git a/configure b/configure index c6f0c304ae8610..0a9ed20d60f790 100755 --- a/configure +++ b/configure @@ -25898,10 +25898,7 @@ int main(void) .tm_mon = 0, .tm_mday = 1 }; - if (strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "99") && - strftime(year, sizeof year, "%4Y", &date) && !strcmp(year, "0099")) - exit(0); - exit(1); + exit(strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "0099")); } _ACEOF diff --git a/configure.ac b/configure.ac index de607aabaac93c..43bd3fb874e795 100644 --- a/configure.ac +++ b/configure.ac @@ -6587,10 +6587,7 @@ int main(void) .tm_mon = 0, .tm_mday = 1 }; - if (strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "99") && - strftime(year, sizeof year, "%4Y", &date) && !strcmp(year, "0099")) - exit(0); - exit(1); + exit(strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "0099")); } ]])], [ac_cv_normalize_century=yes], From a9623c44c8e646aee84cc24fd70289a6daee9f64 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 02:44:01 +0000 Subject: [PATCH 09/56] gh-120713: test that a 0-padded year with century is guaranteed --- Lib/test/datetimetester.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e55b738eb4a975..ce5a0b6ad00098 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1699,16 +1699,7 @@ def test_bool(self): def test_strftime_y2k(self): for y in (1, 49, 70, 99, 100, 999, 1000, 1970): d = self.theclass(y, 1, 1) - # Issue 13305: For years < 1000, the value is not always - # padded to 4 digits across platforms. The C standard - # assumes year >= 1900, so it does not specify the number - # of digits. - if d.strftime("%Y") != '%04d' % y: - # Year 42 returns '42', not padded - self.assertEqual(d.strftime("%Y"), '%d' % y) - # '0042' is obtained anyway - if support.has_strftime_extensions: - self.assertEqual(d.strftime("%4Y"), '%04d' % y) + self.assertEqual(d.strftime("%4Y"), '%04d' % y) def test_replace(self): cls = self.theclass From 2fc1c6a97850863a49b89feae96addd74de10ebc Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 03:00:40 +0000 Subject: [PATCH 10/56] gh-120713: revised detection logics in Python --- Lib/_pydatetime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 8856923df20aca..d85a15c11f343b 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -210,9 +210,9 @@ def _need_normalize_century(): if _normalize_century is None: try: _normalize_century = ( - _time.strftime("%Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) == "99") + _time.strftime("%Y", (99, 1, 1, 0, 0, 0, 0, 1, 0)) != "0099") except ValueError: - _normalize_century = False + _normalize_century = True return _normalize_century # Correctly substitute for %z and %Z escapes in strftime formats. From 2c3252f34a2e06110a6c8791b6031d13547b4e56 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 03:17:44 +0000 Subject: [PATCH 11/56] gh-120713: fixed test --- Lib/test/datetimetester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index ce5a0b6ad00098..c4edace88d3522 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1699,7 +1699,7 @@ def test_bool(self): def test_strftime_y2k(self): for y in (1, 49, 70, 99, 100, 999, 1000, 1970): d = self.theclass(y, 1, 1) - self.assertEqual(d.strftime("%4Y"), '%04d' % y) + self.assertEqual(d.strftime("%Y"), '%04d' % y) def test_replace(self): cls = self.theclass From 9fd3d2db00c9bc731db71cdb6d3f091c6c93b326 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 03:47:44 +0000 Subject: [PATCH 12/56] gh-120713: use "%04ld" instead of "%04d" to avoid unnecessary int conversion --- Modules/_datetimemodule.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index a57b45c404bd5f..ca6be7a3838a16 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1942,13 +1942,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #ifdef NORMALIZE_CENTURY else if (ch == 'Y') { /* 0-pad year with century on platforms that do not do so */ - PyObject *year = PyObject_GetAttrString(object, "year"); - if (year == NULL) - goto Done; char formatted[5]; - ntoappend = sprintf(formatted, "%04d", (int)PyLong_AsLong(year)); + ntoappend = sprintf(formatted, "%04ld", + PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0))); ptoappend = formatted; - Py_DECREF(year); } #endif else { From e64780bf78ecaee231c1c609b2349346333b848e Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 04:05:06 +0000 Subject: [PATCH 13/56] gh-120713: fixed scope for formatted year buffer --- Modules/_datetimemodule.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index ca6be7a3838a16..91608321031977 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1837,6 +1837,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 */ + char year_formatted[5]; /* formatted year with century for %Y */ const char *pin; /* pointer to next char in input format */ Py_ssize_t flen; /* length of input format */ @@ -1942,10 +1943,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #ifdef NORMALIZE_CENTURY else if (ch == 'Y') { /* 0-pad year with century on platforms that do not do so */ - char formatted[5]; - ntoappend = sprintf(formatted, "%04ld", + ntoappend = sprintf(year_formatted, "%04ld", PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0))); - ptoappend = formatted; + ptoappend = year_formatted; } #endif else { From 32026be75fba3ced3d4003f72b6c5514df9be266 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 04:12:15 +0000 Subject: [PATCH 14/56] gh-120713: added #ifdef guard over formatted year buffer declaration --- Modules/_datetimemodule.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 91608321031977..66f0ec70d0f735 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1841,7 +1841,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, const char *pin; /* pointer to next char in input format */ Py_ssize_t flen; /* length of input format */ +#ifdef NORMALIZE_CENTURY char ch; /* next char in input format */ +#endif PyObject *newfmt = NULL; /* py string, the output format */ char *pnew; /* pointer to available byte in output format */ From fed76574b6c0238b73c30510207609cbcdef3edd Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Mon, 24 Jun 2024 04:13:08 +0000 Subject: [PATCH 15/56] gh-120713: fixed typo --- Modules/_datetimemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 66f0ec70d0f735..886f4da4376b9d 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1837,13 +1837,13 @@ 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 */ +#ifdef NORMALIZE_CENTURY char year_formatted[5]; /* formatted year with century for %Y */ +#endif const char *pin; /* pointer to next char in input format */ Py_ssize_t flen; /* length of input format */ -#ifdef NORMALIZE_CENTURY char ch; /* next char in input format */ -#endif PyObject *newfmt = NULL; /* py string, the output format */ char *pnew; /* pointer to available byte in output format */ From 7560a60f005c729f0ebac80d20132cb2515d95ac Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 25 Jun 2024 04:12:06 +0000 Subject: [PATCH 16/56] gh-120713: account for "%G"; increase buffer for formatted year to maximum of long --- Lib/_pydatetime.py | 2 +- Lib/test/datetimetester.py | 5 +++-- Modules/_datetimemodule.c | 4 ++-- configure.ac | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index d85a15c11f343b..130bf609c6d278 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -272,7 +272,7 @@ def _wrap_strftime(object, format, timetuple): # strftime is going to have at this: escape % Zreplace = s.replace('%', '%%') newformat.append(Zreplace) - elif ch == 'Y' and _need_normalize_century(): + elif ch in 'YG' and _need_normalize_century(): push('{:04}'.format(object.year)) else: push('%') diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index c4edace88d3522..01c22d515ceb77 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1698,8 +1698,9 @@ def test_bool(self): def test_strftime_y2k(self): for y in (1, 49, 70, 99, 100, 999, 1000, 1970): - d = self.theclass(y, 1, 1) - self.assertEqual(d.strftime("%Y"), '%04d' % y) + for s in 'YG': + d = self.theclass(y, 1, 1) + self.assertEqual(d.strftime("%" + s), '%04d' % y) def test_replace(self): cls = self.theclass diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 886f4da4376b9d..840c6fc188be0f 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1838,7 +1838,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ #ifdef NORMALIZE_CENTURY - char year_formatted[5]; /* formatted year with century for %Y */ + char year_formatted[12]; /* formatted year with century for %Y */ #endif const char *pin; /* pointer to next char in input format */ @@ -1943,7 +1943,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, ntoappend = PyBytes_GET_SIZE(freplacement); } #ifdef NORMALIZE_CENTURY - else if (ch == 'Y') { + else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century on platforms that do not do so */ ntoappend = sprintf(year_formatted, "%04ld", PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0))); diff --git a/configure.ac b/configure.ac index 43bd3fb874e795..52434a8f611d20 100644 --- a/configure.ac +++ b/configure.ac @@ -6592,7 +6592,7 @@ int main(void) ]])], [ac_cv_normalize_century=yes], [ac_cv_normalize_century=no], -[ac_cv_normalize_century=no])]) +[ac_cv_normalize_century=yes])]) if test "$ac_cv_normalize_century" = yes then AC_DEFINE([NORMALIZE_CENTURY], [1], From f5508eeb85b8764bbb73e2f836c36a71d5855144 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 25 Jun 2024 04:18:40 +0000 Subject: [PATCH 17/56] gh-120713: be pessimistic when cross-compiling --- configure | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure b/configure index 0a9ed20d60f790..636cc03b80c452 100755 --- a/configure +++ b/configure @@ -25881,7 +25881,7 @@ else $as_nop if test "$cross_compiling" = yes then : - ac_cv_normalize_century=no + ac_cv_normalize_century=yes else $as_nop cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ From abc3d21f261bbca84f7ded9593cb3af95faf4861 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 25 Jun 2024 06:05:30 +0000 Subject: [PATCH 18/56] gh-120713: call time.strftime instead to obtain the proper ISO-8601 year --- Lib/_pydatetime.py | 6 +++++- Lib/test/datetimetester.py | 10 +++++++--- Modules/_datetimemodule.c | 24 ++++++++++++++++++------ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 130bf609c6d278..afd192936b8c6e 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -273,7 +273,11 @@ def _wrap_strftime(object, format, timetuple): Zreplace = s.replace('%', '%%') newformat.append(Zreplace) elif ch in 'YG' and _need_normalize_century(): - push('{:04}'.format(object.year)) + if ch == 'G': + year = int(_time.strftime("%G", timetuple)) + else: + year = object.year + push('{:04}'.format(year)) else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 01c22d515ceb77..751de379783b26 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1697,10 +1697,14 @@ def test_bool(self): self.assertTrue(self.theclass.max) def test_strftime_y2k(self): - for y in (1, 49, 70, 99, 100, 999, 1000, 1970): + for y, o in ((1, 0), (49, -1), (70, 0), (99, 0), (100, -1), + (999, 0), (1000, 0), (1970, 0)): for s in 'YG': - d = self.theclass(y, 1, 1) - self.assertEqual(d.strftime("%" + s), '%04d' % y) + with self.subTest(year=y, specifier=s): + d = self.theclass(y, 1, 1) + if s == 'G': + y += o + self.assertEqual(d.strftime("%" + s), '%04d' % y) def test_replace(self): cls = self.theclass diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 840c6fc188be0f..4592fbec6ef147 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -19,6 +19,7 @@ #include "datetime.h" +#include #include #ifdef MS_WINDOWS @@ -1838,6 +1839,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ #ifdef NORMALIZE_CENTURY + long year; /* year of timetuple as long */ char year_formatted[12]; /* formatted year with century for %Y */ #endif @@ -1876,6 +1878,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, pnew = PyBytes_AsString(newfmt); usednew = 0; + PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime"); + + if (strftime == NULL) + goto Done; + while ((ch = *pin++) != '\0') { if (ch != '%') { ptoappend = pin - 1; @@ -1944,9 +1951,17 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } #ifdef NORMALIZE_CENTURY else if (ch == 'Y' || ch == 'G') { - /* 0-pad year with century on platforms that do not do so */ - ntoappend = sprintf(year_formatted, "%04ld", - PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0))); + /* 0-pad year with century as necessary */ + if (ch == 'G') { + format = PyUnicode_FromString("%G"); + result = PyObject_CallFunctionObjArgs(strftime, format, + timetuple, NULL); + year = atoi(PyUnicode_AsUTF8(result)); + Py_DECREF(format); + Py_DECREF(result); + } else + year = PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0)); + ntoappend = sprintf(year_formatted, "%04ld", year); ptoappend = year_formatted; } #endif @@ -1983,10 +1998,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; { PyObject *format; - PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime"); - if (strftime == NULL) - goto Done; format = PyUnicode_FromString(PyBytes_AS_STRING(newfmt)); if (format != NULL) { result = PyObject_CallFunctionObjArgs(strftime, From ff7de92c70d93b08fb3e0dc0180761b21780a595 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 25 Jun 2024 06:18:27 +0000 Subject: [PATCH 19/56] gh-120713: align comment --- Modules/_datetimemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 4592fbec6ef147..2d73ab79f940da 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1839,7 +1839,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ #ifdef NORMALIZE_CENTURY - long year; /* year of timetuple as long */ + long year; /* year of timetuple as long */ char year_formatted[12]; /* formatted year with century for %Y */ #endif From 9c9e645a52537895ccc76dedefa0ec832832ec7e Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 25 Jun 2024 06:25:12 +0000 Subject: [PATCH 20/56] gh-120713: additional comment and formatting --- Lib/test/datetimetester.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 751de379783b26..97032a4e6429fb 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1697,8 +1697,11 @@ def test_bool(self): self.assertTrue(self.theclass.max) def test_strftime_y2k(self): - for y, o in ((1, 0), (49, -1), (70, 0), (99, 0), (100, -1), - (999, 0), (1000, 0), (1970, 0)): + # Test that years less than 1000 are 0-padded; note that the beginning + # an ISO 8601 year may fall in an ISO week of the year before, and + # therefore needs an offset of -1 when formatting with '%G'. + for y, o in ((1, 0), (49, -1), (70, 0), (99, 0), (100, -1), (999, 0), + (1000, 0), (1970, 0)): for s in 'YG': with self.subTest(year=y, specifier=s): d = self.theclass(y, 1, 1) From 0f8886d73a9d6fc9e6af428307be6a3c7c1beb5f Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 25 Jun 2024 06:26:12 +0000 Subject: [PATCH 21/56] gh-120713: fixed comment --- Lib/test/datetimetester.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 97032a4e6429fb..2781a2302d7fc7 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1698,7 +1698,7 @@ def test_bool(self): def test_strftime_y2k(self): # Test that years less than 1000 are 0-padded; note that the beginning - # an ISO 8601 year may fall in an ISO week of the year before, and + # of an ISO 8601 year may fall in an ISO week of the year before, and # therefore needs an offset of -1 when formatting with '%G'. for y, o in ((1, 0), (49, -1), (70, 0), (99, 0), (100, -1), (999, 0), (1000, 0), (1970, 0)): From 2ff8cc530abb4f2fab675458beefe24f763cf904 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Wed, 26 Jun 2024 08:53:15 +0000 Subject: [PATCH 22/56] gh-120713: streamlined C code with better error handling --- Modules/_datetimemodule.c | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 2d73ab79f940da..7b883a44e7dfe2 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -19,7 +19,6 @@ #include "datetime.h" -#include #include #ifdef MS_WINDOWS @@ -1833,14 +1832,16 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *tzinfoarg) { PyObject *result = NULL; /* guilty until proved innocent */ + PyObject *strftime = NULL; /* time.strftime */ PyObject *zreplacement = NULL; /* py string, replacement for %z */ PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */ PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ #ifdef NORMALIZE_CENTURY - long year; /* year of timetuple as long */ - char year_formatted[12]; /* formatted year with century for %Y */ + PyObject *year; /* year of timetuple */ + long year_long; /* year of timetuple as long int */ + char year_formatted[12]; /* formatted year with century for %Y/G */ #endif const char *pin; /* pointer to next char in input format */ @@ -1878,10 +1879,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, pnew = PyBytes_AsString(newfmt); usednew = 0; - PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime"); - - if (strftime == NULL) + strftime = _PyImport_GetModuleAttrString("time", "strftime"); + if (strftime == NULL) { goto Done; + } while ((ch = *pin++) != '\0') { if (ch != '%') { @@ -1953,15 +1954,23 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century as necessary */ if (ch == 'G') { - format = PyUnicode_FromString("%G"); - result = PyObject_CallFunctionObjArgs(strftime, format, - timetuple, NULL); - year = atoi(PyUnicode_AsUTF8(result)); - Py_DECREF(format); + result = PyObject_CallFunction(strftime, "sO", "%G", timetuple); + if (result == NULL) { + goto Done; + } + year = PyNumber_Long(result); + if (year == NULL) { + goto Done; + } Py_DECREF(result); - } else - year = PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0)); - ntoappend = sprintf(year_formatted, "%04ld", year); + } else { + year = PyTuple_GET_ITEM(timetuple, 0); + } + year_long = PyLong_AsLong(year); + if (year_long == -1 && PyErr_Occurred() != NULL) { + goto Done; + } + ntoappend = sprintf(year_formatted, "%04ld", year_long); ptoappend = year_formatted; } #endif @@ -2005,7 +2014,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, format, timetuple, NULL); Py_DECREF(format); } - Py_DECREF(strftime); } Done: Py_XDECREF(freplacement); @@ -2013,6 +2021,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_XDECREF(colonzreplacement); Py_XDECREF(Zreplacement); Py_XDECREF(newfmt); + Py_XDECREF(strftime); return result; } From 637c341fc0be3ab89f0d93af3ebdad2901b49f3c Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Wed, 26 Jun 2024 09:26:21 +0000 Subject: [PATCH 23/56] gh-120713: release reference sooner --- Modules/_datetimemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 7b883a44e7dfe2..6fa30fe497aefe 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1959,10 +1959,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; } year = PyNumber_Long(result); + Py_DECREF(result); if (year == NULL) { goto Done; } - Py_DECREF(result); } else { year = PyTuple_GET_ITEM(timetuple, 0); } From 07327c5c2552e402d72611c5d69f4aaa80523bbd Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Wed, 26 Jun 2024 10:15:47 +0000 Subject: [PATCH 24/56] gh-120713: release reference to year if created for %G --- Modules/_datetimemodule.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 6fa30fe497aefe..398116e28db76a 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1967,6 +1967,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, year = PyTuple_GET_ITEM(timetuple, 0); } year_long = PyLong_AsLong(year); + if (ch == 'G') { + Py_DECREF(year); + } if (year_long == -1 && PyErr_Occurred() != NULL) { goto Done; } From 818d1f44afec5af6acc28d2fc2b85e736af678f8 Mon Sep 17 00:00:00 2001 From: blhsing Date: Wed, 26 Jun 2024 18:22:54 +0800 Subject: [PATCH 25/56] Update 2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst --- .../next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst index c991ec4999711f..093f2c00678c22 100644 --- a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst +++ b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst @@ -1,2 +1,2 @@ -:func:`datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifier ``%Y`` on Linux. +:meth:`!datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifier ``%Y`` on Linux. Patch by Ben Hsing From 7038c1df91883a9c0d718d29c8501a323340bc23 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Thu, 27 Jun 2024 05:18:00 +0000 Subject: [PATCH 26/56] gh-120713: made year as python string a variable separate from result; fixed coding style --- ...4-06-21-06-37-46.gh-issue-120713.WBbQx4.rst | 2 +- Modules/_datetimemodule.c | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst index 093f2c00678c22..7ceb60beb2b74f 100644 --- a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst +++ b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst @@ -1,2 +1,2 @@ -:meth:`!datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifier ``%Y`` on Linux. +:meth:`!datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifiers ``%Y`` and ``%G`` on Linux. Patch by Ben Hsing diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 398116e28db76a..ec4baced9b1814 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1839,8 +1839,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ #ifdef NORMALIZE_CENTURY - PyObject *year; /* year of timetuple */ - long year_long; /* year of timetuple as long int */ + PyObject *year_str; /* py string, year */ + PyObject *year; /* py int, year */ + long year_long; /* year as long int */ char year_formatted[12]; /* formatted year with century for %Y/G */ #endif @@ -1954,23 +1955,24 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century as necessary */ if (ch == 'G') { - result = PyObject_CallFunction(strftime, "sO", "%G", timetuple); - if (result == NULL) { + year_str = PyObject_CallFunction(strftime, "sO", "%G", timetuple); + if (year_str == NULL) { goto Done; } - year = PyNumber_Long(result); - Py_DECREF(result); + year = PyNumber_Long(year_str); + Py_DECREF(year_str); if (year == NULL) { goto Done; } - } else { + } + else { year = PyTuple_GET_ITEM(timetuple, 0); } year_long = PyLong_AsLong(year); if (ch == 'G') { Py_DECREF(year); } - if (year_long == -1 && PyErr_Occurred() != NULL) { + if (year_long == -1 && PyErr_Occurred()) { goto Done; } ntoappend = sprintf(year_formatted, "%04ld", year_long); From 2fb026253a67b99a4d72140ac5447dda00211998 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Thu, 27 Jun 2024 14:28:54 +0800 Subject: [PATCH 27/56] gh-120713: skip 0-padding logics if year > 1000 --- Modules/_datetimemodule.c | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index ec4baced9b1814..f93ff26ddb890f 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1954,6 +1954,13 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #ifdef NORMALIZE_CENTURY else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century as necessary */ + year_long = PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0)); + if (year_long == -1 && PyErr_Occurred()) { + goto Done; + } + if (year_long > 1000) { + goto PassThrough; + } if (ch == 'G') { year_str = PyObject_CallFunction(strftime, "sO", "%G", timetuple); if (year_str == NULL) { @@ -1964,16 +1971,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, if (year == NULL) { goto Done; } - } - else { - year = PyTuple_GET_ITEM(timetuple, 0); - } - year_long = PyLong_AsLong(year); - if (ch == 'G') { + year_long = PyLong_AsLong(year); Py_DECREF(year); - } - if (year_long == -1 && PyErr_Occurred()) { - goto Done; + if (year_long == -1 && PyErr_Occurred()) { + goto Done; + } } ntoappend = sprintf(year_formatted, "%04ld", year_long); ptoappend = year_formatted; @@ -1981,6 +1983,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #endif else { /* percent followed by something else */ +#ifdef NORMALIZE_CENTURY + PassThrough: +#endif ptoappend = pin - 2; ntoappend = 2; } From 96484f9e945a00664d8e656c741c14795184868a Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Thu, 27 Jun 2024 15:53:32 +0800 Subject: [PATCH 28/56] gh-120713: made python implementation skip 0-padding logics if year > 1000; account for year of 64-bit long --- Lib/_pydatetime.py | 2 +- Modules/_datetimemodule.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index afd192936b8c6e..b27b52afdfb49d 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -272,7 +272,7 @@ def _wrap_strftime(object, format, timetuple): # strftime is going to have at this: escape % Zreplace = s.replace('%', '%%') newformat.append(Zreplace) - elif ch in 'YG' and _need_normalize_century(): + elif ch in 'YG' and object.year <= 1000 and _need_normalize_century(): if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index f93ff26ddb890f..00465880be38ba 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1839,10 +1839,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *Zreplacement = NULL; /* py string, replacement for %Z */ PyObject *freplacement = NULL; /* py string, replacement for %f */ #ifdef NORMALIZE_CENTURY - PyObject *year_str; /* py string, year */ - PyObject *year; /* py int, year */ - long year_long; /* year as long int */ - char year_formatted[12]; /* formatted year with century for %Y/G */ + PyObject *year_str; /* py string, year */ + PyObject *year; /* py int, year */ + long year_long; /* year as long int */ + char year_formatted[SIZEOF_LONG*5/2+2]; /* formatted year for %Y/%G */ #endif const char *pin; /* pointer to next char in input format */ From 6c2c1692245c5968b66e4d3009d9a849fe430c76 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 01:02:10 +0000 Subject: [PATCH 29/56] gh-120713: made C check return exit code instead of calling exit --- configure.ac | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 52434a8f611d20..56c4084f5b08c1 100644 --- a/configure.ac +++ b/configure.ac @@ -6587,7 +6587,10 @@ int main(void) .tm_mon = 0, .tm_mday = 1 }; - exit(strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "0099")); + if (strftime(year, sizeof(year), "%Y", &date) && !strcmp(year, "0099")) { + return 1; + } + return 0; } ]])], [ac_cv_normalize_century=yes], From 42244f42277e786c7321bfcb774c923b878efc16 Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 09:02:53 +0800 Subject: [PATCH 30/56] removed inclusion of stdlib.h now that we don't call exit Co-authored-by: Erlend E. Aasland --- configure.ac | 1 - 1 file changed, 1 deletion(-) diff --git a/configure.ac b/configure.ac index 56c4084f5b08c1..3dd2a7a615e999 100644 --- a/configure.ac +++ b/configure.ac @@ -6575,7 +6575,6 @@ fi # Check if year with century should be normalized for strftime AC_CACHE_CHECK([whether year with century should be normalized for strftime], [ac_cv_normalize_century], [ AC_RUN_IFELSE([AC_LANG_SOURCE([[ -#include #include #include From af147f74eb1ee2799891420e5875267076a5b757 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 01:08:47 +0000 Subject: [PATCH 31/56] gh-120713: updated configure --- configure | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configure b/configure index 636cc03b80c452..947cce9a6abd59 100755 --- a/configure +++ b/configure @@ -25898,7 +25898,10 @@ int main(void) .tm_mon = 0, .tm_mday = 1 }; - exit(strftime(year, sizeof year, "%Y", &date) && !strcmp(year, "0099")); + if (strftime(year, sizeof(year), "%Y", &date) && !strcmp(year, "0099")) { + return 1; + } + return 0; } _ACEOF From 40ae42bb6bc4e9f61d7e1e75a76f7922bad3de81 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 01:34:06 +0000 Subject: [PATCH 32/56] gh-120713: updated configure --- configure | 1 - 1 file changed, 1 deletion(-) diff --git a/configure b/configure index 947cce9a6abd59..7523428312174b 100755 --- a/configure +++ b/configure @@ -25886,7 +25886,6 @@ else $as_nop cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ -#include #include #include From fc765d03d8ba2c8a4745a7b23fb7ae39b115f50e Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 13:53:26 +0800 Subject: [PATCH 33/56] gh-120713: removed unnecessary comment --- configure | 1 - configure.ac | 1 - 2 files changed, 2 deletions(-) diff --git a/configure b/configure index 7523428312174b..f04ec5c08f2def 100755 --- a/configure +++ b/configure @@ -25871,7 +25871,6 @@ printf "%s\n" "#define HAVE_STAT_TV_NSEC2 1" >>confdefs.h fi -# Check if year with century should be normalized for strftime { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether year with century should be normalized for strftime" >&5 printf %s "checking whether year with century should be normalized for strftime... " >&6; } if test ${ac_cv_normalize_century+y} diff --git a/configure.ac b/configure.ac index 3dd2a7a615e999..48720a4d1a2aed 100644 --- a/configure.ac +++ b/configure.ac @@ -6572,7 +6572,6 @@ then [Define if you have struct stat.st_mtimensec]) fi -# Check if year with century should be normalized for strftime AC_CACHE_CHECK([whether year with century should be normalized for strftime], [ac_cv_normalize_century], [ AC_RUN_IFELSE([AC_LANG_SOURCE([[ #include From 3cd21f0cf5a6679cf4a27928c1220d03192921b0 Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 17:03:58 +0800 Subject: [PATCH 34/56] made the method a link in the blurb Co-authored-by: Erlend E. Aasland --- .../next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst index 7ceb60beb2b74f..9a2f554a0cc867 100644 --- a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst +++ b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst @@ -1,2 +1,2 @@ -:meth:`!datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifiers ``%Y`` and ``%G`` on Linux. +:meth:`datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifiers ``%Y`` and ``%G`` on Linux. Patch by Ben Hsing From 32cc992686cedcf1be0df657dbacae99393349ca Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 17:17:36 +0800 Subject: [PATCH 35/56] Update configure.ac Co-authored-by: Erlend E. Aasland --- configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 48720a4d1a2aed..0f4993dfe9357b 100644 --- a/configure.ac +++ b/configure.ac @@ -6596,7 +6596,7 @@ int main(void) [ac_cv_normalize_century=yes])]) if test "$ac_cv_normalize_century" = yes then - AC_DEFINE([NORMALIZE_CENTURY], [1], + AC_DEFINE([Py_NORMALIZE_CENTURY], [1], [Define if year with century should be normalized for strftime.]) fi From c506568a802c401dfc5ba2dab65a7cc8e533eb37 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 17:17:07 +0800 Subject: [PATCH 36/56] gh-120713: renamed macro NORMALIZE_CENTURY; moved declarations of new variables in the same scope as usage --- Modules/_datetimemodule.c | 15 +++++++-------- configure | 2 +- pyconfig.h.in | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 00465880be38ba..afd4fed9fe0912 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1838,12 +1838,6 @@ 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 */ -#ifdef NORMALIZE_CENTURY - PyObject *year_str; /* py string, year */ - PyObject *year; /* py int, year */ - long year_long; /* year as long int */ - char year_formatted[SIZEOF_LONG*5/2+2]; /* formatted year for %Y/%G */ -#endif const char *pin; /* pointer to next char in input format */ Py_ssize_t flen; /* length of input format */ @@ -1951,9 +1945,14 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, ptoappend = PyBytes_AS_STRING(freplacement); ntoappend = PyBytes_GET_SIZE(freplacement); } -#ifdef NORMALIZE_CENTURY +#ifdef Py_NORMALIZE_CENTURY else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century as necessary */ + PyObject *year_str; /* py string, year */ + PyObject *year; /* py int, year */ + long year_long; /* year as long int */ + char year_formatted[SIZEOF_LONG*5/2+2]; /* formatted year for %Y/%G */ + year_long = PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0)); if (year_long == -1 && PyErr_Occurred()) { goto Done; @@ -1983,7 +1982,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #endif else { /* percent followed by something else */ -#ifdef NORMALIZE_CENTURY +#ifdef Py_NORMALIZE_CENTURY PassThrough: #endif ptoappend = pin - 2; diff --git a/configure b/configure index f04ec5c08f2def..4a68cc633378f9 100755 --- a/configure +++ b/configure @@ -25919,7 +25919,7 @@ printf "%s\n" "$ac_cv_normalize_century" >&6; } if test "$ac_cv_normalize_century" = yes then -printf "%s\n" "#define NORMALIZE_CENTURY 1" >>confdefs.h +printf "%s\n" "#define Py_NORMALIZE_CENTURY 1" >>confdefs.h fi diff --git a/pyconfig.h.in b/pyconfig.h.in index f91137cf8be044..a90f9363f0ccab 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1591,9 +1591,6 @@ /* Define if mvwdelch in curses.h is an expression. */ #undef MVWDELCH_IS_EXPRESSION -/* Define if year with century should be normalized for strftime. */ -#undef NORMALIZE_CENTURY - /* Define to the address where bug reports for this package should be sent. */ #undef PACKAGE_BUGREPORT @@ -1662,6 +1659,9 @@ SipHash13: 3, externally defined: 0 */ #undef Py_HASH_ALGORITHM +/* Define if year with century should be normalized for strftime. */ +#undef Py_NORMALIZE_CENTURY + /* Define if rl_startup_hook takes arguments */ #undef Py_RL_STARTUP_HOOK_TAKES_ARGS From 5f8bf96751fe4bf93eed1fe73c3339bcc24dcc05 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 17:20:23 +0800 Subject: [PATCH 37/56] gh-120713: initialize strftime on declaration --- Modules/_datetimemodule.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index afd4fed9fe0912..e09bcc43715953 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1832,7 +1832,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, PyObject *tzinfoarg) { PyObject *result = NULL; /* guilty until proved innocent */ - PyObject *strftime = NULL; /* time.strftime */ PyObject *zreplacement = NULL; /* py string, replacement for %z */ PyObject *colonzreplacement = NULL; /* py string, replacement for %:z */ @@ -1874,7 +1873,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, pnew = PyBytes_AsString(newfmt); usednew = 0; - strftime = _PyImport_GetModuleAttrString("time", "strftime"); + PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime"); if (strftime == NULL) { goto Done; } From 07df016ee2db4761895eac0b012ff1e746c0735c Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 17:32:04 +0800 Subject: [PATCH 38/56] gh-120713: added versionchanged info in the documentation --- Doc/library/datetime.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index b6d8e6e6df07fa..07924786fb7a2c 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -755,7 +755,6 @@ Instance methods: Format codes referring to hours, minutes or seconds will see 0 values. See also :ref:`strftime-strptime-behavior` and :meth:`date.isoformat`. - .. method:: date.__format__(format) Same as :meth:`.date.strftime`. This makes it possible to specify a format @@ -2540,6 +2539,10 @@ differences between platforms in handling of unsupported format specifiers. .. versionadded:: 3.12 ``%:z`` was added. +.. versionchanged:: 3.14 + ``%Y`` and ``%G`` are now guaranteed to 0-pad year with century to 4 digits. Previously + they would not 0-pad years less than 1000 on certain platforms such as Linux. + Technical Detail ^^^^^^^^^^^^^^^^ From 0588e3810b1f8904d2682b2f638e40cd1831455e Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 17:48:47 +0800 Subject: [PATCH 39/56] gh-120713: switched to PyOS_snprintf; moved declarations closer to usage; separate calls on separate lines --- Modules/_datetimemodule.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index e09bcc43715953..82fb48c5a95838 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1947,12 +1947,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #ifdef Py_NORMALIZE_CENTURY else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century as necessary */ - PyObject *year_str; /* py string, year */ - PyObject *year; /* py int, year */ - long year_long; /* year as long int */ - char year_formatted[SIZEOF_LONG*5/2+2]; /* formatted year for %Y/%G */ + size_t buf_size = SIZEOF_LONG*5/2+2; /* maximum size of formatted year permitted by long */ + char buf[buf_size]; /* formatted year for %Y/%G */ + + PyObject *item = PyTuple_GET_ITEM(timetuple, 0); + long year_long = PyLong_AsLong(item); - year_long = PyLong_AsLong(PyTuple_GET_ITEM(timetuple, 0)); if (year_long == -1 && PyErr_Occurred()) { goto Done; } @@ -1960,6 +1960,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto PassThrough; } if (ch == 'G') { + PyObject *year_str; /* py string, year */ + PyObject *year; /* py int, year */ + year_str = PyObject_CallFunction(strftime, "sO", "%G", timetuple); if (year_str == NULL) { goto Done; @@ -1975,8 +1978,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; } } - ntoappend = sprintf(year_formatted, "%04ld", year_long); - ptoappend = year_formatted; + ntoappend = PyOS_snprintf(buf, buf_size, "%04ld", year_long); + ptoappend = buf; } #endif else { From e8bbd88a50f70ae8ccac91dedf7db6d99c811429 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 17:51:10 +0800 Subject: [PATCH 40/56] gh-120713: reverted addition of the versionchanged directive --- Doc/library/datetime.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Doc/library/datetime.rst b/Doc/library/datetime.rst index 07924786fb7a2c..b6d8e6e6df07fa 100644 --- a/Doc/library/datetime.rst +++ b/Doc/library/datetime.rst @@ -755,6 +755,7 @@ Instance methods: Format codes referring to hours, minutes or seconds will see 0 values. See also :ref:`strftime-strptime-behavior` and :meth:`date.isoformat`. + .. method:: date.__format__(format) Same as :meth:`.date.strftime`. This makes it possible to specify a format @@ -2539,10 +2540,6 @@ differences between platforms in handling of unsupported format specifiers. .. versionadded:: 3.12 ``%:z`` was added. -.. versionchanged:: 3.14 - ``%Y`` and ``%G`` are now guaranteed to 0-pad year with century to 4 digits. Previously - they would not 0-pad years less than 1000 on certain platforms such as Linux. - Technical Detail ^^^^^^^^^^^^^^^^ From 2ca5af6669bfaa190ac4f6e32224b98ee8c12af4 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 17:57:11 +0800 Subject: [PATCH 41/56] gh-120713: moved declarations closer to usage --- Modules/_datetimemodule.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 82fb48c5a95838..f89a4095f17a6b 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1947,9 +1947,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #ifdef Py_NORMALIZE_CENTURY else if (ch == 'Y' || ch == 'G') { /* 0-pad year with century as necessary */ - size_t buf_size = SIZEOF_LONG*5/2+2; /* maximum size of formatted year permitted by long */ - char buf[buf_size]; /* formatted year for %Y/%G */ - PyObject *item = PyTuple_GET_ITEM(timetuple, 0); long year_long = PyLong_AsLong(item); @@ -1978,6 +1975,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; } } + + size_t buf_size = SIZEOF_LONG*5/2+2; /* maximum size of formatted year permitted by long */ + char buf[buf_size]; /* formatted year for %Y/%G */ + ntoappend = PyOS_snprintf(buf, buf_size, "%04ld", year_long); ptoappend = buf; } From 451ebf19374d9932773a51043e6847b139ee3ca8 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 18:00:58 +0800 Subject: [PATCH 42/56] gh-120713: initialize variables on declarations --- Modules/_datetimemodule.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index f89a4095f17a6b..118b0e05ffaf84 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1957,14 +1957,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto PassThrough; } if (ch == 'G') { - PyObject *year_str; /* py string, year */ - PyObject *year; /* py int, year */ - - year_str = PyObject_CallFunction(strftime, "sO", "%G", timetuple); + PyObject *year_str = PyObject_CallFunction(strftime, "sO", "%G", timetuple); if (year_str == NULL) { goto Done; } - year = PyNumber_Long(year_str); + PyObject *year = PyNumber_Long(year_str); Py_DECREF(year_str); if (year == NULL) { goto Done; From 25e4fa528bde29a0883ce91c80a07dbcec8bf443 Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 19:27:47 +0800 Subject: [PATCH 43/56] Update Lib/test/datetimetester.py Co-authored-by: Erlend E. Aasland --- Lib/test/datetimetester.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 2781a2302d7fc7..e3fdf56e416210 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1700,8 +1700,17 @@ def test_strftime_y2k(self): # Test that years less than 1000 are 0-padded; note that the beginning # of an ISO 8601 year may fall in an ISO week of the year before, and # therefore needs an offset of -1 when formatting with '%G'. - for y, o in ((1, 0), (49, -1), (70, 0), (99, 0), (100, -1), (999, 0), - (1000, 0), (1970, 0)): + dataset = ( + (1, 0), + (49, -1), + (70, 0), + (99, 0), + (100, -1), + (999, 0), + (1000, 0), + (1970, 0), + ) + for year, offset in dataset: for s in 'YG': with self.subTest(year=y, specifier=s): d = self.theclass(y, 1, 1) From 0656e218c4a7fa248c6983e9b37fbbfada1acae4 Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 19:28:08 +0800 Subject: [PATCH 44/56] Update Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst Co-authored-by: Erlend E. Aasland --- .../next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst index 9a2f554a0cc867..18386a43eddc6f 100644 --- a/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst +++ b/Misc/NEWS.d/next/Library/2024-06-21-06-37-46.gh-issue-120713.WBbQx4.rst @@ -1,2 +1,2 @@ -:meth:`datetime.datetime.strftime` now 0-pads a year <= 999 for the format specifiers ``%Y`` and ``%G`` on Linux. +:meth:`datetime.datetime.strftime` now 0-pads years with less than four digits for the format specifiers ``%Y`` and ``%G`` on Linux. Patch by Ben Hsing From 049a05b1061dca83de530c92b23f06cc3ebb9abe Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 19:33:37 +0800 Subject: [PATCH 45/56] Update Modules/_datetimemodule.c Co-authored-by: Erlend E. Aasland --- Modules/_datetimemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 118b0e05ffaf84..cc1b5d65c35fc6 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1953,7 +1953,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, if (year_long == -1 && PyErr_Occurred()) { goto Done; } - if (year_long > 1000) { + if (year_long >= 1000) { goto PassThrough; } if (ch == 'G') { From a82bca4f7ae498fb0989b0e9e651bf063be142d6 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 12:26:13 +0000 Subject: [PATCH 46/56] gh-120713: moved initialization of strftime above the first goto to avoid uninitialized usage --- Modules/_datetimemodule.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index cc1b5d65c35fc6..6207587e1525ba 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1858,6 +1858,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, if (!pin) return NULL; + PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime"); + if (strftime == NULL) { + goto Done; + } + /* Scan the input format, looking for %z/%Z/%f escapes, building * a new format. Since computing the replacements for those codes * is expensive, don't unless they're actually used. @@ -1873,11 +1878,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, pnew = PyBytes_AsString(newfmt); usednew = 0; - PyObject *strftime = _PyImport_GetModuleAttrString("time", "strftime"); - if (strftime == NULL) { - goto Done; - } - while ((ch = *pin++) != '\0') { if (ch != '%') { ptoappend = pin - 1; From 492ecf999717292f8e2e6036f17ef412b157e20f Mon Sep 17 00:00:00 2001 From: blhsing Date: Fri, 28 Jun 2024 20:54:29 +0800 Subject: [PATCH 47/56] Update Lib/test/datetimetester.py Co-authored-by: Erlend E. Aasland --- Lib/test/datetimetester.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e3fdf56e416210..1f3e42ab34f090 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1711,12 +1711,12 @@ def test_strftime_y2k(self): (1970, 0), ) for year, offset in dataset: - for s in 'YG': - with self.subTest(year=y, specifier=s): - d = self.theclass(y, 1, 1) - if s == 'G': - y += o - self.assertEqual(d.strftime("%" + s), '%04d' % y) + for specifier in 'YG': + with self.subTest(year=year, specifier=specifier): + d = self.theclass(year, 1, 1) + if specifier == 'G': + year += offset + self.assertEqual(d.strftime(f"%{specifier}"), f"{year:04d}") def test_replace(self): cls = self.theclass From 7d8a9f117876e39a300603ed525045e1b7c05efd Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 28 Jun 2024 14:17:34 +0000 Subject: [PATCH 48/56] gh-120713: comment on why year 1000 can go on the fast path for %G --- Lib/_pydatetime.py | 4 +++- Modules/_datetimemodule.c | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b27b52afdfb49d..a1ad0c66d0802c 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -272,7 +272,9 @@ def _wrap_strftime(object, format, timetuple): # strftime is going to have at this: escape % Zreplace = s.replace('%', '%%') newformat.append(Zreplace) - elif ch in 'YG' and object.year <= 1000 and _need_normalize_century(): + elif ch in 'YG' and object.year < 1000 and _need_normalize_century(): + # note that datetime(1000, 1, 1).strftime('%G') == '1000' so + # year 1000 for %G can go on the fast path if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 6207587e1525ba..b358ef4fe7e6b4 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1953,6 +1953,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, if (year_long == -1 && PyErr_Occurred()) { goto Done; } + /* note that datetime(1000, 1, 1).strftime('%G') == '1000' so year + 1000 for %G can go on the fast path */ if (year_long >= 1000) { goto PassThrough; } From 455fa961133bfa2404d72e54a00813574daee1e4 Mon Sep 17 00:00:00 2001 From: blhsing Date: Sat, 29 Jun 2024 08:39:06 +0800 Subject: [PATCH 49/56] Update Modules/_datetimemodule.c Co-authored-by: Erlend E. Aasland --- Modules/_datetimemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index b358ef4fe7e6b4..fe391972c3d849 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1953,8 +1953,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, if (year_long == -1 && PyErr_Occurred()) { goto Done; } - /* note that datetime(1000, 1, 1).strftime('%G') == '1000' so year - 1000 for %G can go on the fast path */ + /* Note that datetime(1000, 1, 1).strftime('%G') == '1000' so year + 1000 for %G can go on the fast path. */ if (year_long >= 1000) { goto PassThrough; } From f299c20f66dc6e5c02c0ebb3f691faf15209bc61 Mon Sep 17 00:00:00 2001 From: blhsing Date: Sat, 29 Jun 2024 08:41:57 +0800 Subject: [PATCH 50/56] Update Modules/_datetimemodule.c Co-authored-by: Erlend E. Aasland --- Modules/_datetimemodule.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index fe391972c3d849..6081a8b1b080b6 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1959,7 +1959,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto PassThrough; } if (ch == 'G') { - PyObject *year_str = PyObject_CallFunction(strftime, "sO", "%G", timetuple); + PyObject *year_str = PyObject_CallFunction(strftime, "sO", + "%G", timetuple); if (year_str == NULL) { goto Done; } From fcc7c7f8109cf1bebfbb9f60d05d80fa111635da Mon Sep 17 00:00:00 2001 From: blhsing Date: Sat, 29 Jun 2024 08:42:09 +0800 Subject: [PATCH 51/56] Update Lib/_pydatetime.py Co-authored-by: Erlend E. Aasland --- Lib/_pydatetime.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index a1ad0c66d0802c..27cacb8e01ff3b 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -273,8 +273,8 @@ def _wrap_strftime(object, format, timetuple): Zreplace = s.replace('%', '%%') newformat.append(Zreplace) elif ch in 'YG' and object.year < 1000 and _need_normalize_century(): - # note that datetime(1000, 1, 1).strftime('%G') == '1000' so - # year 1000 for %G can go on the fast path + # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so + # year 1000 for %G can go on the fast path. if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: From 3069d622738670841a60f30971de11f6a891b0c9 Mon Sep 17 00:00:00 2001 From: blhsing Date: Sat, 29 Jun 2024 08:42:52 +0800 Subject: [PATCH 52/56] Update Modules/_datetimemodule.c Co-authored-by: Erlend E. Aasland --- Modules/_datetimemodule.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 6081a8b1b080b6..61df0b541bd2f6 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1976,7 +1976,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } } - size_t buf_size = SIZEOF_LONG*5/2+2; /* maximum size of formatted year permitted by long */ + /* Maximum size of formatted year permitted by long. */ + size_t buf_size = SIZEOF_LONG*5/2+2; char buf[buf_size]; /* formatted year for %Y/%G */ ntoappend = PyOS_snprintf(buf, buf_size, "%04ld", year_long); From e6031327178c3888b601a2e0f0dbcaa555ea150d Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Sat, 29 Jun 2024 00:49:51 +0000 Subject: [PATCH 53/56] gh-120713: use sizeof instead of a variable to determine the size of buffer --- Modules/_datetimemodule.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 61df0b541bd2f6..82b9db8dfb4058 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1976,11 +1976,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } } - /* Maximum size of formatted year permitted by long. */ - size_t buf_size = SIZEOF_LONG*5/2+2; - char buf[buf_size]; /* formatted year for %Y/%G */ + /* Buffer of maximum size of formatted year permitted by long. */ + char buf[SIZEOF_LONG*5/2+2]; - ntoappend = PyOS_snprintf(buf, buf_size, "%04ld", year_long); + ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); ptoappend = buf; } #endif From c54739b62d419410595b27d5547e747a6d466b4c Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Sat, 29 Jun 2024 01:23:54 +0000 Subject: [PATCH 54/56] gh-120713: use macro instead of sizeof --- Modules/_datetimemodule.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 82b9db8dfb4058..2c820f806d1911 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1976,10 +1976,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } } - /* Buffer of maximum size of formatted year permitted by long. */ - char buf[SIZEOF_LONG*5/2+2]; +/* Maximum size of formatted year permitted by long. */ +#define MAX_YEAR_BUFFER_LEN SIZEOF_LONG*5/2+2 + char buf[MAX_YEAR_BUFFER_LEN]; - ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); + ntoappend = PyOS_snprintf(buf, MAX_YEAR_BUFFER_LEN, "%04ld", year_long); ptoappend = buf; } #endif From 881dc1d4fd4b0270b0c8b50a44cc41632eca3143 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Sat, 29 Jun 2024 01:51:03 +0000 Subject: [PATCH 55/56] gh-120713: moved buffer declaration to the same scope as the ptoappend pointer --- Modules/_datetimemodule.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 2c820f806d1911..d33d05d3f4623b 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1851,6 +1851,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, const char *ptoappend; /* ptr to string to append to output buffer */ Py_ssize_t ntoappend; /* # of bytes to append to output buffer */ +#ifdef NORMALIZE_CENTURY + /* Buffer of maximum size of formatted year permitted by long. */ + char buf[SIZEOF_LONG*5/2+2]; +#endif + assert(object && format && timetuple); assert(PyUnicode_Check(format)); /* Convert the input format to a C string and size */ @@ -1976,11 +1981,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } } -/* Maximum size of formatted year permitted by long. */ -#define MAX_YEAR_BUFFER_LEN SIZEOF_LONG*5/2+2 - char buf[MAX_YEAR_BUFFER_LEN]; - - ntoappend = PyOS_snprintf(buf, MAX_YEAR_BUFFER_LEN, "%04ld", year_long); + ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); ptoappend = buf; } #endif From 0cd5e27db75e08a33b4c078c44fce84d0d8973dd Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Sat, 29 Jun 2024 01:54:48 +0000 Subject: [PATCH 56/56] gh-120713: fixed typo --- Modules/_datetimemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index d33d05d3f4623b..c84af50791177d 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1851,7 +1851,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, const char *ptoappend; /* ptr to string to append to output buffer */ Py_ssize_t ntoappend; /* # of bytes to append to output buffer */ -#ifdef NORMALIZE_CENTURY +#ifdef Py_NORMALIZE_CENTURY /* Buffer of maximum size of formatted year permitted by long. */ char buf[SIZEOF_LONG*5/2+2]; #endif