https://github.com/python/cpython/commit/7d9b2671c6fcdb09cf49a235e3ef3bd5230b4156
commit: 7d9b2671c6fcdb09cf49a235e3ef3bd5230b4156
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: encukou <[email protected]>
date: 2025-10-07T19:59:06+02:00
summary:

[3.14] gh-123681: Check NORMALIZE_CENTURY behavior at runtime; require C99 
(GH-136022) (GH-137947)

A runtime check is needed to support cross-compiling.

Remove the _Py_NORMALIZE_CENTURY macro.
Remove _pydatetime.py's _can_support_c99.
(cherry picked from commit 719e5c3f7111bcda5eee72fe648786c427c4d4c2)

Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Build/2025-01-03-13-02-06.gh-issue-123681.gQ67nK.rst
M Lib/_pydatetime.py
M Lib/test/datetimetester.py
M Modules/_datetimemodule.c
M Tools/c-analyzer/cpython/ignored.tsv
M configure
M configure.ac
M pyconfig.h.in

diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py
index cfcd35827a275a..70251dbb6535d2 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))
diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py
index 1c1cbd03d02ccc..aca5fbc04b18cb 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/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..a60b4607456096
--- /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 the internal macro ``_Py_NORMALIZE_CENTURY``.
diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c
index c4bb44f1234990..ddb9e9fd2e57c0 100644
--- a/Modules/_datetimemodule.c
+++ b/Modules/_datetimemodule.c
@@ -1753,6 +1753,24 @@ 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 cache = -1;
+    if (cache < 0) {
+        char year[5];
+        struct tm date = {
+            .tm_year = -1801,
+            .tm_mon = 0,
+            .tm_mday = 1
+        };
+        cache = (strftime(year, sizeof(year), "%Y", &date) &&
+                 strcmp(year, "0099") != 0);
+    }
+    return cache;
+}
+
 static PyObject *
 make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg)
 {
@@ -1924,10 +1942,9 @@ wrap_strftime(PyObject *object, PyObject *format, 
PyObject *timetuple,
             }
             replacement = freplacement;
         }
-#ifdef _Py_NORMALIZE_CENTURY
-        else if (ch == 'Y' || ch == 'G'
-                 || 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);
             if (item == NULL) {
@@ -1978,7 +1995,6 @@ wrap_strftime(PyObject *object, PyObject *format, 
PyObject *timetuple,
             }
             continue;
         }
-#endif
         else {
             /* percent followed by something else */
             continue;
diff --git a/Tools/c-analyzer/cpython/ignored.tsv 
b/Tools/c-analyzer/cpython/ignored.tsv
index d5d806be42f39d..687871068e8bae 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -226,6 +226,7 @@ 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/_decimal/_decimal.c    -       cond_map_template       -
 Modules/_decimal/_decimal.c    -       dec_signal_string       -
 Modules/_decimal/_decimal.c    -       dflt_ctx        -
diff --git a/configure b/configure
index d31c24dffa2e9d..dc66ea068ebc1f 100755
--- a/configure
+++ b/configure
@@ -28186,110 +28186,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 case e in #(
-  e)
-if test "$cross_compiling" = yes
-then :
-  ac_cv_normalize_century=yes
-else case e in #(
-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-#include <time.h>
-#include <string.h>
-
-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 case e in #(
-  e) ac_cv_normalize_century=no ;;
-esac
-fi
-rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;
-esac
-fi
- ;;
-esac
-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-compatible 
strftime specifiers are supported" >&5
-printf %s "checking whether C99-compatible strftime specifiers are 
supported... " >&6; }
-if test ${ac_cv_strftime_c99_support+y}
-then :
-  printf %s "(cached) " >&6
-else case e in #(
-  e)
-if test "$cross_compiling" = yes
-then :
-  ac_cv_strftime_c99_support=
-else case e in #(
-  e) cat confdefs.h - <<_ACEOF >conftest.$ac_ext
-/* end confdefs.h.  */
-
-#include <time.h>
-#include <string.h>
-
-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 case e in #(
-  e) as_fn_error $? "Python requires C99-compatible strftime specifiers" 
"$LINENO" 5 ;;
-esac
-fi
-rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
-  conftest.$ac_objext conftest.beam conftest.$ac_ext ;;
-esac
-fi
- ;;
-esac
-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; }
-
 have_curses=no
 have_panel=no
 
diff --git a/configure.ac b/configure.ac
index af7a9623d7bd79..8340319c7e63f2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -6792,57 +6792,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 <time.h>
-#include <string.h>
-
-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-compatible strftime specifiers are supported], 
[ac_cv_strftime_c99_support], [
-AC_RUN_IFELSE([AC_LANG_SOURCE([[
-#include <time.h>
-#include <string.h>
-
-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_MSG_ERROR([Python requires C99-compatible strftime specifiers])],
-[ac_cv_strftime_c99_support=])])
-
 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 1c533b2bfb7fb4..0d6ad4465c0e93 100644
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -2023,9 +2023,6 @@
 /* HACL* library can compile SIMD256 implementations */
 #undef _Py_HACL_CAN_COMPILE_VEC256
 
-/* Define if year with century should be normalized for strftime. */
-#undef _Py_NORMALIZE_CENTURY
-
 /* Define to force use of thread-safe errno, h_errno, and other functions */
 #undef _REENTRANT
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to