Skip to content

Commit 719e5c3

Browse files
gh-123681: Check NORMALIZE_CENTURY behavior at runtime; require C99 (GH-136022)
A runtime check is needed to support cross-compiling. Remove the _Py_NORMALIZE_CENTURY macro. Remove _pydatetime.py's _can_support_c99. Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent b07a267 commit 719e5c3

File tree

8 files changed

+48
-191
lines changed

8 files changed

+48
-191
lines changed

Lib/_pydatetime.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -213,17 +213,6 @@ def _need_normalize_century():
213213
_normalize_century = True
214214
return _normalize_century
215215

216-
_supports_c99 = None
217-
def _can_support_c99():
218-
global _supports_c99
219-
if _supports_c99 is None:
220-
try:
221-
_supports_c99 = (
222-
_time.strftime("%F", (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == "1900-01-01")
223-
except ValueError:
224-
_supports_c99 = False
225-
return _supports_c99
226-
227216
# Correctly substitute for %z and %Z escapes in strftime formats.
228217
def _wrap_strftime(object, format, timetuple):
229218
# Don't call utcoffset() or tzname() unless actually needed.
@@ -283,7 +272,7 @@ def _wrap_strftime(object, format, timetuple):
283272
newformat.append(Zreplace)
284273
# Note that datetime(1000, 1, 1).strftime('%G') == '1000' so
285274
# year 1000 for %G can go on the fast path.
286-
elif ((ch in 'YG' or ch in 'FC' and _can_support_c99()) and
275+
elif ((ch in 'YG' or ch in 'FC') and
287276
object.year < 1000 and _need_normalize_century()):
288277
if ch == 'G':
289278
year = int(_time.strftime("%G", timetuple))

Lib/test/datetimetester.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1807,7 +1807,7 @@ def test_bool(self):
18071807
self.assertTrue(self.theclass.min)
18081808
self.assertTrue(self.theclass.max)
18091809

1810-
def test_strftime_y2k(self):
1810+
def check_strftime_y2k(self, specifier):
18111811
# Test that years less than 1000 are 0-padded; note that the beginning
18121812
# of an ISO 8601 year may fall in an ISO week of the year before, and
18131813
# therefore needs an offset of -1 when formatting with '%G'.
@@ -1821,22 +1821,28 @@ def test_strftime_y2k(self):
18211821
(1000, 0),
18221822
(1970, 0),
18231823
)
1824-
specifiers = 'YG'
1825-
if _time.strftime('%F', (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == '1900-01-01':
1826-
specifiers += 'FC'
18271824
for year, g_offset in dataset:
1828-
for specifier in specifiers:
1829-
with self.subTest(year=year, specifier=specifier):
1830-
d = self.theclass(year, 1, 1)
1831-
if specifier == 'G':
1832-
year += g_offset
1833-
if specifier == 'C':
1834-
expected = f"{year // 100:02d}"
1835-
else:
1836-
expected = f"{year:04d}"
1837-
if specifier == 'F':
1838-
expected += f"-01-01"
1839-
self.assertEqual(d.strftime(f"%{specifier}"), expected)
1825+
with self.subTest(year=year, specifier=specifier):
1826+
d = self.theclass(year, 1, 1)
1827+
if specifier == 'G':
1828+
year += g_offset
1829+
if specifier == 'C':
1830+
expected = f"{year // 100:02d}"
1831+
else:
1832+
expected = f"{year:04d}"
1833+
if specifier == 'F':
1834+
expected += f"-01-01"
1835+
self.assertEqual(d.strftime(f"%{specifier}"), expected)
1836+
1837+
def test_strftime_y2k(self):
1838+
self.check_strftime_y2k('Y')
1839+
self.check_strftime_y2k('G')
1840+
1841+
def test_strftime_y2k_c99(self):
1842+
# CPython requires C11; specifiers new in C99 must work.
1843+
# (Other implementations may want to disable this test.)
1844+
self.check_strftime_y2k('F')
1845+
self.check_strftime_y2k('C')
18401846

18411847
def test_replace(self):
18421848
cls = self.theclass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Check the ``strftime()`` behavior at runtime instead of at the compile time
2+
to support cross-compiling.
3+
Remove the internal macro ``_Py_NORMALIZE_CENTURY``.

Modules/_datetimemodule.c

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,24 @@ format_utcoffset(char *buf, size_t buflen, const char *sep,
17601760
return 0;
17611761
}
17621762

1763+
/* Check whether year with century should be normalized for strftime. */
1764+
inline static int
1765+
normalize_century(void)
1766+
{
1767+
static int cache = -1;
1768+
if (cache < 0) {
1769+
char year[5];
1770+
struct tm date = {
1771+
.tm_year = -1801,
1772+
.tm_mon = 0,
1773+
.tm_mday = 1
1774+
};
1775+
cache = (strftime(year, sizeof(year), "%Y", &date) &&
1776+
strcmp(year, "0099") != 0);
1777+
}
1778+
return cache;
1779+
}
1780+
17631781
static PyObject *
17641782
make_somezreplacement(PyObject *object, char *sep, PyObject *tzinfoarg)
17651783
{
@@ -1931,10 +1949,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
19311949
}
19321950
replacement = freplacement;
19331951
}
1934-
#ifdef _Py_NORMALIZE_CENTURY
1935-
else if (ch == 'Y' || ch == 'G'
1936-
|| ch == 'F' || ch == 'C'
1937-
) {
1952+
else if (normalize_century()
1953+
&& (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C'))
1954+
{
19381955
/* 0-pad year with century as necessary */
19391956
PyObject *item = PySequence_GetItem(timetuple, 0);
19401957
if (item == NULL) {
@@ -1985,7 +2002,6 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple,
19852002
}
19862003
continue;
19872004
}
1988-
#endif
19892005
else {
19902006
/* percent followed by something else */
19912007
continue;

Tools/c-analyzer/cpython/ignored.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ Modules/_datetimemodule.c datetime_isoformat specs -
228228
Modules/_datetimemodule.c parse_hh_mm_ss_ff correction -
229229
Modules/_datetimemodule.c time_isoformat specs -
230230
Modules/_datetimemodule.c - capi_types -
231+
Modules/_datetimemodule.c normalize_century cache -
231232
Modules/_decimal/_decimal.c - cond_map_template -
232233
Modules/_decimal/_decimal.c - dec_signal_string -
233234
Modules/_decimal/_decimal.c - dflt_ctx -

configure

Lines changed: 0 additions & 104 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

configure.ac

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -6800,57 +6800,6 @@ then
68006800
[Define if you have struct stat.st_mtimensec])
68016801
fi
68026802

6803-
AC_CACHE_CHECK([whether year with century should be normalized for strftime], [ac_cv_normalize_century], [
6804-
AC_RUN_IFELSE([AC_LANG_SOURCE([[
6805-
#include <time.h>
6806-
#include <string.h>
6807-
6808-
int main(void)
6809-
{
6810-
char year[5];
6811-
struct tm date = {
6812-
.tm_year = -1801,
6813-
.tm_mon = 0,
6814-
.tm_mday = 1
6815-
};
6816-
if (strftime(year, sizeof(year), "%Y", &date) && !strcmp(year, "0099")) {
6817-
return 1;
6818-
}
6819-
return 0;
6820-
}
6821-
]])],
6822-
[ac_cv_normalize_century=yes],
6823-
[ac_cv_normalize_century=no],
6824-
[ac_cv_normalize_century=yes])])
6825-
if test "$ac_cv_normalize_century" = yes
6826-
then
6827-
AC_DEFINE([_Py_NORMALIZE_CENTURY], [1],
6828-
[Define if year with century should be normalized for strftime.])
6829-
fi
6830-
6831-
AC_CACHE_CHECK([whether C99-compatible strftime specifiers are supported], [ac_cv_strftime_c99_support], [
6832-
AC_RUN_IFELSE([AC_LANG_SOURCE([[
6833-
#include <time.h>
6834-
#include <string.h>
6835-
6836-
int main(void)
6837-
{
6838-
char full_date[11];
6839-
struct tm date = {
6840-
.tm_year = 0,
6841-
.tm_mon = 0,
6842-
.tm_mday = 1
6843-
};
6844-
if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "1900-01-01")) {
6845-
return 0;
6846-
}
6847-
return 1;
6848-
}
6849-
]])],
6850-
[ac_cv_strftime_c99_support=yes],
6851-
[AC_MSG_ERROR([Python requires C99-compatible strftime specifiers])],
6852-
[ac_cv_strftime_c99_support=])])
6853-
68546803
dnl check for ncursesw/ncurses and panelw/panel
68556804
dnl NOTE: old curses is not detected.
68566805
dnl have_curses=[no, yes]

pyconfig.h.in

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,9 +2023,6 @@
20232023
/* HACL* library can compile SIMD256 implementations */
20242024
#undef _Py_HACL_CAN_COMPILE_VEC256
20252025

2026-
/* Define if year with century should be normalized for strftime. */
2027-
#undef _Py_NORMALIZE_CENTURY
2028-
20292026
/* Define to force use of thread-safe errno, h_errno, and other functions */
20302027
#undef _REENTRANT
20312028

0 commit comments

Comments
 (0)