From 0d17190cc821ee5ce1c1bf6ddafaeeebb2c3f463 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 3 Jan 2025 13:02:20 +0200 Subject: [PATCH 1/4] gh-123681: Check the strftime() behavior at runtime instead of at the compile time It is needed to support cross-compiling. Remove macros Py_NORMALIZE_CENTURY and Py_STRFTIME_C99_SUPPORT. --- ...-01-03-13-02-06.gh-issue-123681.gQ67nK.rst | 3 + Modules/_datetimemodule.c | 50 +++++++-- configure | 104 ------------------ configure.ac | 56 ---------- pyconfig.h.in | 6 - 5 files changed, 42 insertions(+), 177 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2025-01-03-13-02-06.gh-issue-123681.gQ67nK.rst diff --git a/Misc/NEWS.d/next/Build/2025-01-03-13-02-06.gh-issue-123681.gQ67nK.rst b/Misc/NEWS.d/next/Build/2025-01-03-13-02-06.gh-issue-123681.gQ67nK.rst new file mode 100644 index 00000000000000..8f4f1fb4fabbe5 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2025-01-03-13-02-06.gh-issue-123681.gQ67nK.rst @@ -0,0 +1,3 @@ +Check the ``strftime()`` behavior at runtime instead of at the compile time +to support cross-compiling. Remove macros Py_NORMALIZE_CENTURY and +Py_STRFTIME_C99_SUPPORT. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index b1102984cb5e9e..04b256edd88498 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1740,6 +1740,42 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, return 0; } +/* Check whether year with century should be normalized for strftime. */ +inline static int +normalize_century(void) +{ + static int _normalize_century = -1; + if (_normalize_century < 0) { + char year[5]; + struct tm date = { + .tm_year = -1801, + .tm_mon = 0, + .tm_mday = 1 + }; + _normalize_century = (strftime(year, sizeof(year), "%Y", &date) && + strcmp(year, "0099") != 0); + } + return _normalize_century; +} + +/* Check whether C99-specific strftime specifiers are supported. */ +inline static int +strftime_c99_support(void) +{ + static int _strftime_c99_support = -1; + if (_strftime_c99_support < 0) { + char full_date[11]; + struct tm date = { + .tm_year = 0, + .tm_mon = 0, + .tm_mday = 1 + }; + _strftime_c99_support = (strftime(full_date, sizeof(full_date), "%F", &date) && + strcmp(full_date, "1900-01-01") == 0); + } + return _strftime_c99_support; +} + static PyObject * make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg) { @@ -1910,12 +1946,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } replacement = freplacement; } -#ifdef Py_NORMALIZE_CENTURY - else if (ch == 'Y' || ch == 'G' -#ifdef Py_STRFTIME_C99_SUPPORT - || ch == 'F' || ch == 'C' -#endif - ) { + else if (normalize_century() && (ch == 'Y' || ch == 'G' || + (strftime_c99_support() && (ch == 'F' || ch == 'C')))) + { /* 0-pad year with century as necessary */ PyObject *item = PySequence_GetItem(timetuple, 0); if (item == NULL) { @@ -1952,15 +1985,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, * +6 to accommodate dashes, 2-digit month and day for %F. */ char buf[SIZEOF_LONG * 5 / 2 + 2 + 6]; Py_ssize_t n = PyOS_snprintf(buf, sizeof(buf), -#ifdef Py_STRFTIME_C99_SUPPORT ch == 'F' ? "%04ld-%%m-%%d" : -#endif "%04ld", year_long); -#ifdef Py_STRFTIME_C99_SUPPORT if (ch == 'C') { n -= 2; } -#endif if (_PyUnicodeWriter_WriteSubstring(&writer, format, start, end) < 0) { goto Error; } @@ -1970,7 +1999,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } continue; } -#endif else { /* percent followed by something else */ continue; diff --git a/configure b/configure index 3d2c60213db591..9f67c9ead085dd 100755 --- a/configure +++ b/configure @@ -26384,110 +26384,6 @@ printf "%s\n" "#define HAVE_STAT_TV_NSEC2 1" >>confdefs.h fi -{ 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=yes -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#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, "0099")) { - return 1; - } - return 0; -} - -_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 Py_NORMALIZE_CENTURY 1" >>confdefs.h - -fi - -{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether C99-specific strftime specifiers are supported" >&5 -printf %s "checking whether C99-specific strftime specifiers are supported... " >&6; } -if test ${ac_cv_strftime_c99_support+y} -then : - printf %s "(cached) " >&6 -else $as_nop - -if test "$cross_compiling" = yes -then : - ac_cv_strftime_c99_support=no -else $as_nop - cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -#include -#include - -int main(void) -{ - char full_date[11]; - struct tm date = { - .tm_year = 0, - .tm_mon = 0, - .tm_mday = 1 - }; - if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "1900-01-01")) { - return 0; - } - return 1; -} - -_ACEOF -if ac_fn_c_try_run "$LINENO" -then : - ac_cv_strftime_c99_support=yes -else $as_nop - ac_cv_strftime_c99_support=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_strftime_c99_support" >&5 -printf "%s\n" "$ac_cv_strftime_c99_support" >&6; } -if test "$ac_cv_strftime_c99_support" = yes -then - -printf "%s\n" "#define Py_STRFTIME_C99_SUPPORT 1" >>confdefs.h - -fi - have_curses=no have_panel=no diff --git a/configure.ac b/configure.ac index ee034e5a9621df..7edc7caa0e3d4c 100644 --- a/configure.ac +++ b/configure.ac @@ -6644,62 +6644,6 @@ then [Define if you have struct stat.st_mtimensec]) fi -AC_CACHE_CHECK([whether year with century should be normalized for strftime], [ac_cv_normalize_century], [ -AC_RUN_IFELSE([AC_LANG_SOURCE([[ -#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, "0099")) { - return 1; - } - return 0; -} -]])], -[ac_cv_normalize_century=yes], -[ac_cv_normalize_century=no], -[ac_cv_normalize_century=yes])]) -if test "$ac_cv_normalize_century" = yes -then - AC_DEFINE([Py_NORMALIZE_CENTURY], [1], - [Define if year with century should be normalized for strftime.]) -fi - -AC_CACHE_CHECK([whether C99-specific strftime specifiers are supported], [ac_cv_strftime_c99_support], [ -AC_RUN_IFELSE([AC_LANG_SOURCE([[ -#include -#include - -int main(void) -{ - char full_date[11]; - struct tm date = { - .tm_year = 0, - .tm_mon = 0, - .tm_mday = 1 - }; - if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "1900-01-01")) { - return 0; - } - return 1; -} -]])], -[ac_cv_strftime_c99_support=yes], -[ac_cv_strftime_c99_support=no], -[ac_cv_strftime_c99_support=no])]) -if test "$ac_cv_strftime_c99_support" = yes -then - AC_DEFINE([Py_STRFTIME_C99_SUPPORT], [1], - [Define if C99-specific strftime specifiers are supported.]) -fi - dnl check for ncursesw/ncurses and panelw/panel dnl NOTE: old curses is not detected. dnl have_curses=[no, yes] diff --git a/pyconfig.h.in b/pyconfig.h.in index 1ca83fd2f2ca1b..6edd4a5678484f 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1706,18 +1706,12 @@ /* Defined if _Complex C type is available. */ #undef Py_HAVE_C_COMPLEX -/* 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 /* Define if you want to enable internal statistics gathering. */ #undef Py_STATS -/* Define if C99-specific strftime specifiers are supported. */ -#undef Py_STRFTIME_C99_SUPPORT - /* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ #undef Py_SUNOS_VERSION From 26441f90997b6442d7a9325cc2987161a83b753c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 8 Apr 2025 15:14:42 +0300 Subject: [PATCH 2/4] Add static variables to ignored.tsv. --- Modules/_datetimemodule.c | 20 ++++++++++---------- Tools/c-analyzer/cpython/ignored.tsv | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index bef98d484c72c1..c808d36b791d59 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1761,36 +1761,36 @@ format_utcoffset(char *buf, size_t buflen, const char *sep, inline static int normalize_century(void) { - static int _normalize_century = -1; - if (_normalize_century < 0) { + static int cache = -1; + if (cache < 0) { char year[5]; struct tm date = { .tm_year = -1801, .tm_mon = 0, .tm_mday = 1 }; - _normalize_century = (strftime(year, sizeof(year), "%Y", &date) && - strcmp(year, "0099") != 0); + cache = (strftime(year, sizeof(year), "%Y", &date) && + strcmp(year, "0099") != 0); } - return _normalize_century; + return cache; } /* Check whether C99-specific strftime specifiers are supported. */ inline static int strftime_c99_support(void) { - static int _strftime_c99_support = -1; - if (_strftime_c99_support < 0) { + static int cache = -1; + if (cache < 0) { char full_date[11]; struct tm date = { .tm_year = 0, .tm_mon = 0, .tm_mday = 1 }; - _strftime_c99_support = (strftime(full_date, sizeof(full_date), "%F", &date) && - strcmp(full_date, "1900-01-01") == 0); + cache = (strftime(full_date, sizeof(full_date), "%F", &date) && + strcmp(full_date, "1900-01-01") == 0); } - return _strftime_c99_support; + return cache; } static PyObject * diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 14dc5007b65861..28a83c8c41fe45 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -224,6 +224,8 @@ Modules/_datetimemodule.c datetime_isoformat specs - Modules/_datetimemodule.c parse_hh_mm_ss_ff correction - Modules/_datetimemodule.c time_isoformat specs - Modules/_datetimemodule.c - capi_types - +Modules/_datetimemodule.c normalize_century cache - +Modules/_datetimemodule.c strftime_c99_support cache - Modules/_decimal/_decimal.c - cond_map_template - Modules/_decimal/_decimal.c - dec_signal_string - Modules/_decimal/_decimal.c - dflt_ctx - From 8231ace06c0b160fa6ec62ac092350bced352572 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 27 Jun 2025 10:56:16 +0200 Subject: [PATCH 3/4] CPython needs C99 --- Lib/test/datetimetester.py | 38 ++++++++++++++++------------ Modules/_datetimemodule.c | 22 ++-------------- Tools/c-analyzer/cpython/ignored.tsv | 1 - 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 93b3382b9c654e..9dcbcd8b4d291e 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1807,7 +1807,7 @@ def test_bool(self): self.assertTrue(self.theclass.min) self.assertTrue(self.theclass.max) - def test_strftime_y2k(self): + def check_strftime_y2k(self, specifier): # 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'. @@ -1821,22 +1821,28 @@ def test_strftime_y2k(self): (1000, 0), (1970, 0), ) - specifiers = 'YG' - if _time.strftime('%F', (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == '1900-01-01': - specifiers += 'FC' for year, g_offset in dataset: - for specifier in specifiers: - with self.subTest(year=year, specifier=specifier): - d = self.theclass(year, 1, 1) - if specifier == 'G': - year += g_offset - if specifier == 'C': - expected = f"{year // 100:02d}" - else: - expected = f"{year:04d}" - if specifier == 'F': - expected += f"-01-01" - self.assertEqual(d.strftime(f"%{specifier}"), expected) + with self.subTest(year=year, specifier=specifier): + d = self.theclass(year, 1, 1) + if specifier == 'G': + year += g_offset + if specifier == 'C': + expected = f"{year // 100:02d}" + else: + expected = f"{year:04d}" + if specifier == 'F': + expected += f"-01-01" + self.assertEqual(d.strftime(f"%{specifier}"), expected) + + def test_strftime_y2k(self): + self.check_strftime_y2k('Y') + self.check_strftime_y2k('G') + + def test_strftime_y2k_c99(self): + # CPython requires C11; specifiers new in C99 must work. + # (Other implementations may want to disable this test.) + self.check_strftime_y2k('F') + self.check_strftime_y2k('C') def test_replace(self): cls = self.theclass diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 6f8469cc322980..77918e188723ff 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1781,24 +1781,6 @@ normalize_century(void) return cache; } -/* Check whether C99-specific strftime specifiers are supported. */ -inline static int -strftime_c99_support(void) -{ - static int cache = -1; - if (cache < 0) { - char full_date[11]; - struct tm date = { - .tm_year = 0, - .tm_mon = 0, - .tm_mday = 1 - }; - cache = (strftime(full_date, sizeof(full_date), "%F", &date) && - strcmp(full_date, "1900-01-01") == 0); - } - return cache; -} - static PyObject * make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg) { @@ -1970,8 +1952,8 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } replacement = freplacement; } - else if (normalize_century() && (ch == 'Y' || ch == 'G' || - (strftime_c99_support() && (ch == 'F' || ch == 'C')))) + else if (normalize_century() + && (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C')) { /* 0-pad year with century as necessary */ PyObject *item = PySequence_GetItem(timetuple, 0); diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 1f377a00dad32c..7b083cd9b1e8b5 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -227,7 +227,6 @@ Modules/_datetimemodule.c parse_hh_mm_ss_ff correction - Modules/_datetimemodule.c time_isoformat specs - Modules/_datetimemodule.c - capi_types - Modules/_datetimemodule.c normalize_century cache - -Modules/_datetimemodule.c strftime_c99_support cache - Modules/_decimal/_decimal.c - cond_map_template - Modules/_decimal/_decimal.c - dec_signal_string - Modules/_decimal/_decimal.c - dflt_ctx - From edf4428a59eecb2b547c92a0e19d9bbf3c2743d0 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 1 Jul 2025 11:41:20 +0200 Subject: [PATCH 4/4] Remove _can_support_c99 from _pydatetime.py --- Lib/_pydatetime.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index bc35823f70144e..b4b1b228c41199 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -213,17 +213,6 @@ def _need_normalize_century(): _normalize_century = True return _normalize_century -_supports_c99 = None -def _can_support_c99(): - global _supports_c99 - if _supports_c99 is None: - try: - _supports_c99 = ( - _time.strftime("%F", (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == "1900-01-01") - except ValueError: - _supports_c99 = False - return _supports_c99 - # 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. @@ -283,7 +272,7 @@ def _wrap_strftime(object, format, timetuple): newformat.append(Zreplace) # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so # year 1000 for %G can go on the fast path. - elif ((ch in 'YG' or ch in 'FC' and _can_support_c99()) and + elif ((ch in 'YG' or ch in 'FC') and object.year < 1000 and _need_normalize_century()): if ch == 'G': year = int(_time.strftime("%G", timetuple))