From 77d117c145ee7a2ced7f716e8b1815fb5d7cf587 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 12 Jul 2025 09:47:12 -0400 Subject: [PATCH 01/15] Load _datetime during interpreter initialization --- Include/internal/pycore_pylifecycle.h | 1 + Modules/Setup.bootstrap.in | 2 ++ Modules/Setup.stdlib.in | 3 --- Modules/_datetimemodule.c | 17 +++++------------ Python/pylifecycle.c | 5 +++++ 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index 6e89ca33e4208c..2a5986ad9941ee 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -41,6 +41,7 @@ extern PyStatus _Py_HashRandomization_Init(const PyConfig *); extern PyStatus _PyGC_Init(PyInterpreterState *interp); extern PyStatus _PyAtExit_Init(PyInterpreterState *interp); +extern PyStatus _PyDateTime_Init(PyInterpreterState *interp); /* Various internal finalizers */ diff --git a/Modules/Setup.bootstrap.in b/Modules/Setup.bootstrap.in index 2b2e8cb3e3cacd..65a1fefe72e92e 100644 --- a/Modules/Setup.bootstrap.in +++ b/Modules/Setup.bootstrap.in @@ -12,6 +12,8 @@ posix posixmodule.c _signal signalmodule.c _tracemalloc _tracemalloc.c _suggestions _suggestions.c +# needs libm and on some platforms librt +_datetime _datetimemodule.c # modules used by importlib, deepfreeze, freeze, runpy, and sysconfig _codecs _codecsmodule.c diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 3a38a60a152e8c..905ea4aa2e57a9 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -56,9 +56,6 @@ @MODULE_CMATH_TRUE@cmath cmathmodule.c @MODULE__STATISTICS_TRUE@_statistics _statisticsmodule.c -# needs libm and on some platforms librt -@MODULE__DATETIME_TRUE@_datetime _datetimemodule.c - # _decimal uses libmpdec # either static libmpdec.a from Modules/_decimal/libmpdec or libmpdec.so # with ./configure --with-system-libmpdec diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 7a6426593d021f..a328e54e8ae3fe 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -14,6 +14,7 @@ #include "pycore_object.h" // _PyObject_Init() #include "pycore_time.h" // _PyTime_ObjectToTime_t() #include "pycore_unicodeobject.h" // _PyUnicode_Copy() +#include "pycore_initconfig.h" // _PyStatus_OK() #include "datetime.h" @@ -7329,13 +7330,9 @@ clear_state(datetime_state *st) } -static int -init_static_types(PyInterpreterState *interp, int reloading) +PyStatus +_PyDateTime_Init(PyInterpreterState *interp) { - if (reloading) { - return 0; - } - // `&...` is not a constant expression according to a strict reading // of C standards. Fill tp_base at run-time rather than statically. // See https://bugs.python.org/issue40777 @@ -7347,11 +7344,11 @@ init_static_types(PyInterpreterState *interp, int reloading) for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) { PyTypeObject *type = capi_types[i]; if (_PyStaticType_InitForExtension(interp, type) < 0) { - return -1; + return _PyStatus_ERR("could not initialize static types"); } } - return 0; + return _PyStatus_OK(); } @@ -7379,10 +7376,6 @@ _datetime_exec(PyObject *module) } /* We actually set the "current" module right before a successful return. */ - if (init_static_types(interp, reloading) < 0) { - goto error; - } - for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) { PyTypeObject *type = capi_types[i]; const char *name = _PyType_Name(type); diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 00e8d030765560..7ceba7ab6d0ff4 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -915,6 +915,11 @@ pycore_interp_init(PyThreadState *tstate) goto done; } + status = _PyDateTime_Init(tstate->interp); + if (_PyStatus_EXCEPTION(status)) { + goto done; + } + const PyConfig *config = _PyInterpreterState_GetConfig(interp); status = _PyImport_InitCore(tstate, sysmod, config->_install_importlib); From 963a9ee58be7635b42c2dcc11c1fbef6500c09c5 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 12 Jul 2025 09:58:40 -0400 Subject: [PATCH 02/15] Add a test case. --- Lib/test/datetimetester.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 93b3382b9c654e..57255f45c4afc2 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3651,6 +3651,35 @@ def test_repr_subclass(self): td = SubclassDatetime(2010, 10, 2, second=3) self.assertEqual(repr(td), "SubclassDatetime(2010, 10, 2, 0, 0, 3)") + @support.cpython_only + def test_concurrent_initialization(self): + try: + from concurrent.futures import InterpreterPoolExecutor as _ + except ImportError: + self.skipTest("requires subinterpreters") + + try: + import _datetime as _ + except ImportError: + self.skipTest("requires C implementation of datetime") + + # Run in a subprocess to ensure we get a clean version of _datetime + script = """if True: + from concurrent.futures import InterpreterPoolExecutor + + def func(): + import _datetime + print('a', end='') + + with InterpreterPoolExecutor() as executor: + for _ in range(8): + executor.submit(func) + """ + rc, out, err = script_helper.assert_python_ok("-c", script) + self.assertEqual(rc, 0) + self.assertEqual(out, b"a" * 8) + self.assertEqual(err, b"") + class TestSubclassDateTime(TestDateTime): theclass = SubclassDatetime From e16fb5453e9fdefc79c630c3a9ab403b1ecf1fc5 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 12 Jul 2025 09:59:19 -0400 Subject: [PATCH 03/15] Add blurb. --- .../2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst new file mode 100644 index 00000000000000..dcc73267a78546 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-07-12-09-59-14.gh-issue-136421.ZD1rNj.rst @@ -0,0 +1 @@ +Fix crash when initializing :mod:`datetime` concurrently. From ed656820a919706499ec6e7f98831edc6422adc8 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 12 Jul 2025 10:06:06 -0400 Subject: [PATCH 04/15] Move to pycore_init_types() --- Include/internal/pycore_pylifecycle.h | 2 +- Modules/_datetimemodule.c | 2 +- Python/pylifecycle.c | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Include/internal/pycore_pylifecycle.h b/Include/internal/pycore_pylifecycle.h index 2a5986ad9941ee..8faf7a4d403f84 100644 --- a/Include/internal/pycore_pylifecycle.h +++ b/Include/internal/pycore_pylifecycle.h @@ -41,7 +41,7 @@ extern PyStatus _Py_HashRandomization_Init(const PyConfig *); extern PyStatus _PyGC_Init(PyInterpreterState *interp); extern PyStatus _PyAtExit_Init(PyInterpreterState *interp); -extern PyStatus _PyDateTime_Init(PyInterpreterState *interp); +extern PyStatus _PyDateTime_InitTypes(PyInterpreterState *interp); /* Various internal finalizers */ diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index a328e54e8ae3fe..43dcbf503fee82 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -7331,7 +7331,7 @@ clear_state(datetime_state *st) PyStatus -_PyDateTime_Init(PyInterpreterState *interp) +_PyDateTime_InitTypes(PyInterpreterState *interp) { // `&...` is not a constant expression according to a strict reading // of C standards. Fill tp_base at run-time rather than statically. diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 7ceba7ab6d0ff4..e22a9cc1c75050 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -760,6 +760,11 @@ pycore_init_types(PyInterpreterState *interp) return status; } + status = _PyDateTime_InitTypes(interp); + if (_PyStatus_EXCEPTION(status)) { + return status; + } + return _PyStatus_OK(); } @@ -915,11 +920,6 @@ pycore_interp_init(PyThreadState *tstate) goto done; } - status = _PyDateTime_Init(tstate->interp); - if (_PyStatus_EXCEPTION(status)) { - goto done; - } - const PyConfig *config = _PyInterpreterState_GetConfig(interp); status = _PyImport_InitCore(tstate, sysmod, config->_install_importlib); From 43b4843c82752a91b6cc39a82a90a75e447add12 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 12 Jul 2025 10:12:02 -0400 Subject: [PATCH 05/15] Fix lint. --- Lib/test/datetimetester.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 57255f45c4afc2..01bc6d2aa3c376 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3653,16 +3653,6 @@ def test_repr_subclass(self): @support.cpython_only def test_concurrent_initialization(self): - try: - from concurrent.futures import InterpreterPoolExecutor as _ - except ImportError: - self.skipTest("requires subinterpreters") - - try: - import _datetime as _ - except ImportError: - self.skipTest("requires C implementation of datetime") - # Run in a subprocess to ensure we get a clean version of _datetime script = """if True: from concurrent.futures import InterpreterPoolExecutor From 0ad304fa2121a2676efd3c7ee5c96725b1f56bff Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 12 Jul 2025 10:22:27 -0400 Subject: [PATCH 06/15] Add _datetime to the frozen modules. I have no idea if this will fix the Windows build, but let's hope. --- PCbuild/_freeze_module.vcxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj index efff6a58d895cb..5ceddf759b8f3b 100644 --- a/PCbuild/_freeze_module.vcxproj +++ b/PCbuild/_freeze_module.vcxproj @@ -106,6 +106,7 @@ + From d762ed5df1b6ac3bb02fc4d194a4ac640a0b4473 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 00:52:45 -0400 Subject: [PATCH 07/15] Remove obsolete 'reloading' state. --- Modules/_datetimemodule.c | 137 ++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 71 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 43dcbf503fee82..009c842c977178 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -125,10 +125,9 @@ get_module_state(PyObject *module) #define INTERP_KEY ((PyObject *)&_Py_ID(cached_datetime_module)) static PyObject * -get_current_module(PyInterpreterState *interp, int *p_reloading) +get_current_module(PyInterpreterState *interp) { PyObject *mod = NULL; - int reloading = 0; PyObject *dict = PyInterpreterState_GetDict(interp); if (dict == NULL) { @@ -139,7 +138,6 @@ get_current_module(PyInterpreterState *interp, int *p_reloading) goto error; } if (ref != NULL) { - reloading = 1; if (ref != Py_None) { (void)PyWeakref_GetRef(ref, &mod); if (mod == Py_None) { @@ -148,9 +146,6 @@ get_current_module(PyInterpreterState *interp, int *p_reloading) Py_DECREF(ref); } } - if (p_reloading != NULL) { - *p_reloading = reloading; - } return mod; error: @@ -164,7 +159,7 @@ static datetime_state * _get_current_state(PyObject **p_mod) { PyInterpreterState *interp = PyInterpreterState_Get(); - PyObject *mod = get_current_module(interp, NULL); + PyObject *mod = get_current_module(interp); if (mod == NULL) { assert(!PyErr_Occurred()); if (PyErr_Occurred()) { @@ -7348,7 +7343,70 @@ _PyDateTime_InitTypes(PyInterpreterState *interp) } } +#define DATETIME_ADD_MACRO(dict, c, value_expr) \ + do { \ + assert(!PyErr_Occurred()); \ + PyObject *value = (value_expr); \ + if (value == NULL) { \ + goto error; \ + } \ + if (PyDict_SetItemString(dict, c, value) < 0) { \ + Py_DECREF(value); \ + goto error; \ + } \ + Py_DECREF(value); \ + } while(0) + + /* timedelta values */ + PyObject *d = _PyType_GetDict(&PyDateTime_DeltaType); + DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0)); + DATETIME_ADD_MACRO(d, "min", new_delta(-MAX_DELTA_DAYS, 0, 0, 0)); + DATETIME_ADD_MACRO(d, "max", + new_delta(MAX_DELTA_DAYS, 24*3600-1, 1000000-1, 0)); + + /* date values */ + d = _PyType_GetDict(&PyDateTime_DateType); + DATETIME_ADD_MACRO(d, "min", new_date(1, 1, 1)); + DATETIME_ADD_MACRO(d, "max", new_date(MAXYEAR, 12, 31)); + DATETIME_ADD_MACRO(d, "resolution", new_delta(1, 0, 0, 0)); + + /* time values */ + d = _PyType_GetDict(&PyDateTime_TimeType); + DATETIME_ADD_MACRO(d, "min", new_time(0, 0, 0, 0, Py_None, 0)); + DATETIME_ADD_MACRO(d, "max", new_time(23, 59, 59, 999999, Py_None, 0)); + DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0)); + + /* datetime values */ + d = _PyType_GetDict(&PyDateTime_DateTimeType); + DATETIME_ADD_MACRO(d, "min", + new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0)); + DATETIME_ADD_MACRO(d, "max", new_datetime(MAXYEAR, 12, 31, 23, 59, 59, + 999999, Py_None, 0)); + DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0)); + + /* timezone values */ + d = _PyType_GetDict(&PyDateTime_TimeZoneType); + if (PyDict_SetItemString(d, "utc", (PyObject *)&utc_timezone) < 0) { + goto error; + } + + /* bpo-37642: These attributes are rounded to the nearest minute for backwards + * compatibility, even though the constructor will accept a wider range of + * values. This may change in the future.*/ + + /* -23:59 */ + DATETIME_ADD_MACRO(d, "min", create_timezone_from_delta(-1, 60, 0, 1)); + + /* +23:59 */ + DATETIME_ADD_MACRO( + d, "max", create_timezone_from_delta(0, (23 * 60 + 59) * 60, 0, 0)); + +#undef DATETIME_ADD_MACRO + return _PyStatus_OK(); + +error: + return _PyStatus_NO_MEMORY(); } @@ -7366,10 +7424,9 @@ _datetime_exec(PyObject *module) { int rc = -1; datetime_state *st = get_module_state(module); - int reloading = 0; PyInterpreterState *interp = PyInterpreterState_Get(); - PyObject *old_module = get_current_module(interp, &reloading); + PyObject *old_module = get_current_module(interp); if (PyErr_Occurred()) { assert(old_module == NULL); goto error; @@ -7389,68 +7446,6 @@ _datetime_exec(PyObject *module) goto error; } -#define DATETIME_ADD_MACRO(dict, c, value_expr) \ - do { \ - assert(!PyErr_Occurred()); \ - PyObject *value = (value_expr); \ - if (value == NULL) { \ - goto error; \ - } \ - if (PyDict_SetItemString(dict, c, value) < 0) { \ - Py_DECREF(value); \ - goto error; \ - } \ - Py_DECREF(value); \ - } while(0) - - if (!reloading) { - /* timedelta values */ - PyObject *d = _PyType_GetDict(&PyDateTime_DeltaType); - DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0)); - DATETIME_ADD_MACRO(d, "min", new_delta(-MAX_DELTA_DAYS, 0, 0, 0)); - DATETIME_ADD_MACRO(d, "max", - new_delta(MAX_DELTA_DAYS, 24*3600-1, 1000000-1, 0)); - - /* date values */ - d = _PyType_GetDict(&PyDateTime_DateType); - DATETIME_ADD_MACRO(d, "min", new_date(1, 1, 1)); - DATETIME_ADD_MACRO(d, "max", new_date(MAXYEAR, 12, 31)); - DATETIME_ADD_MACRO(d, "resolution", new_delta(1, 0, 0, 0)); - - /* time values */ - d = _PyType_GetDict(&PyDateTime_TimeType); - DATETIME_ADD_MACRO(d, "min", new_time(0, 0, 0, 0, Py_None, 0)); - DATETIME_ADD_MACRO(d, "max", new_time(23, 59, 59, 999999, Py_None, 0)); - DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0)); - - /* datetime values */ - d = _PyType_GetDict(&PyDateTime_DateTimeType); - DATETIME_ADD_MACRO(d, "min", - new_datetime(1, 1, 1, 0, 0, 0, 0, Py_None, 0)); - DATETIME_ADD_MACRO(d, "max", new_datetime(MAXYEAR, 12, 31, 23, 59, 59, - 999999, Py_None, 0)); - DATETIME_ADD_MACRO(d, "resolution", new_delta(0, 0, 1, 0)); - - /* timezone values */ - d = _PyType_GetDict(&PyDateTime_TimeZoneType); - if (PyDict_SetItemString(d, "utc", (PyObject *)&utc_timezone) < 0) { - goto error; - } - - /* bpo-37642: These attributes are rounded to the nearest minute for backwards - * compatibility, even though the constructor will accept a wider range of - * values. This may change in the future.*/ - - /* -23:59 */ - DATETIME_ADD_MACRO(d, "min", create_timezone_from_delta(-1, 60, 0, 1)); - - /* +23:59 */ - DATETIME_ADD_MACRO( - d, "max", create_timezone_from_delta(0, (23 * 60 + 59) * 60, 0, 0)); - } - -#undef DATETIME_ADD_MACRO - /* Add module level attributes */ if (PyModule_AddIntMacro(module, MINYEAR) < 0) { goto error; From db327e71b490accf6cabb4d45100447bb9af0bd2 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 01:14:25 -0400 Subject: [PATCH 08/15] Some test refactoring and improvements. --- Lib/test/datetimetester.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 01bc6d2aa3c376..0765989c8b6f4c 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3652,7 +3652,7 @@ def test_repr_subclass(self): self.assertEqual(repr(td), "SubclassDatetime(2010, 10, 2, 0, 0, 3)") @support.cpython_only - def test_concurrent_initialization(self): + def test_concurrent_initialization_subinterpreter(self): # Run in a subprocess to ensure we get a clean version of _datetime script = """if True: from concurrent.futures import InterpreterPoolExecutor @@ -3670,6 +3670,13 @@ def func(): self.assertEqual(out, b"a" * 8) self.assertEqual(err, b"") + # Now test against concurrent reinitialization + script += "\nimport _datetime" + rc, out, err = script_helper.assert_python_ok("-c", script) + self.assertEqual(rc, 0) + self.assertEqual(out, b"a" * 8) + self.assertEqual(err, b"") + class TestSubclassDateTime(TestDateTime): theclass = SubclassDatetime From 94561478da49ebba0ea13a47185a30f3299e65a3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 01:16:49 -0400 Subject: [PATCH 09/15] Fix 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 0765989c8b6f4c..4bf1cc6a5404e9 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3671,7 +3671,7 @@ def func(): self.assertEqual(err, b"") # Now test against concurrent reinitialization - script += "\nimport _datetime" + script = "import _datetime\n" + script rc, out, err = script_helper.assert_python_ok("-c", script) self.assertEqual(rc, 0) self.assertEqual(out, b"a" * 8) From 6adafa1d25b2e4cb4285f6f6a0a7b2399a36f0ab Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 09:56:45 -0400 Subject: [PATCH 10/15] Make tp_base stores atomic. --- Modules/_datetimemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 009c842c977178..5868482dcd9047 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -7331,8 +7331,8 @@ _PyDateTime_InitTypes(PyInterpreterState *interp) // `&...` is not a constant expression according to a strict reading // of C standards. Fill tp_base at run-time rather than statically. // See https://bugs.python.org/issue40777 - PyDateTime_TimeZoneType.tp_base = &PyDateTime_TZInfoType; - PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType; + _Py_atomic_store_ptr_relaxed(&PyDateTime_TimeZoneType.tp_base, &PyDateTime_TZInfoType); + _Py_atomic_store_ptr_relaxed(&PyDateTime_DateTimeType.tp_base , &PyDateTime_DateType); /* Bases classes must be initialized before subclasses, * so capi_types must have the types in the appropriate order. */ From 6e2f891491bd17ebea922c52646e05bbe07cc64e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 09:59:42 -0400 Subject: [PATCH 11/15] Revert "Make tp_base stores atomic." This reverts commit 6adafa1d25b2e4cb4285f6f6a0a7b2399a36f0ab. --- Modules/_datetimemodule.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 5868482dcd9047..009c842c977178 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -7331,8 +7331,8 @@ _PyDateTime_InitTypes(PyInterpreterState *interp) // `&...` is not a constant expression according to a strict reading // of C standards. Fill tp_base at run-time rather than statically. // See https://bugs.python.org/issue40777 - _Py_atomic_store_ptr_relaxed(&PyDateTime_TimeZoneType.tp_base, &PyDateTime_TZInfoType); - _Py_atomic_store_ptr_relaxed(&PyDateTime_DateTimeType.tp_base , &PyDateTime_DateType); + PyDateTime_TimeZoneType.tp_base = &PyDateTime_TZInfoType; + PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType; /* Bases classes must be initialized before subclasses, * so capi_types must have the types in the appropriate order. */ From a15843f925997ece4d6a8eb32e400ed64c763fdb Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 10:58:52 -0400 Subject: [PATCH 12/15] Set tp_base in the PyTypeObject definition. --- Modules/_datetimemodule.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 009c842c977178..a1f6b3bb58f1d4 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -4478,7 +4478,7 @@ static PyTypeObject PyDateTime_TimeZoneType = { timezone_methods, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ - 0, /* tp_base; filled in PyInit__datetime */ + &PyDateTime_TZInfoType, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ @@ -7143,8 +7143,7 @@ static PyTypeObject PyDateTime_DateTimeType = { datetime_methods, /* tp_methods */ 0, /* tp_members */ datetime_getset, /* tp_getset */ - 0, /* tp_base; filled in - PyInit__datetime */ + &PyDateTime_DateType, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ @@ -7328,10 +7327,6 @@ clear_state(datetime_state *st) PyStatus _PyDateTime_InitTypes(PyInterpreterState *interp) { - // `&...` is not a constant expression according to a strict reading - // of C standards. Fill tp_base at run-time rather than statically. - // See https://bugs.python.org/issue40777 - PyDateTime_TimeZoneType.tp_base = &PyDateTime_TZInfoType; PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType; /* Bases classes must be initialized before subclasses, From d6064c43bb194bd5af4120ddadad60a3d1dfc0d7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 10:59:47 -0400 Subject: [PATCH 13/15] Add a comment. --- Lib/test/datetimetester.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 4bf1cc6a5404e9..83d97d7d85b179 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3653,6 +3653,9 @@ def test_repr_subclass(self): @support.cpython_only def test_concurrent_initialization_subinterpreter(self): + # gh-136421: Concurrent initialization of _datetime across multiple + # interpreters wasn't thread-safe due to its static types. + # Run in a subprocess to ensure we get a clean version of _datetime script = """if True: from concurrent.futures import InterpreterPoolExecutor From 9a3b1c2c71670f08feb2c616edd591797aa93150 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 11:03:41 -0400 Subject: [PATCH 14/15] Move the test. --- Lib/test/datetimetester.py | 57 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 83d97d7d85b179..3bd3a866570042 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -3651,35 +3651,6 @@ def test_repr_subclass(self): td = SubclassDatetime(2010, 10, 2, second=3) self.assertEqual(repr(td), "SubclassDatetime(2010, 10, 2, 0, 0, 3)") - @support.cpython_only - def test_concurrent_initialization_subinterpreter(self): - # gh-136421: Concurrent initialization of _datetime across multiple - # interpreters wasn't thread-safe due to its static types. - - # Run in a subprocess to ensure we get a clean version of _datetime - script = """if True: - from concurrent.futures import InterpreterPoolExecutor - - def func(): - import _datetime - print('a', end='') - - with InterpreterPoolExecutor() as executor: - for _ in range(8): - executor.submit(func) - """ - rc, out, err = script_helper.assert_python_ok("-c", script) - self.assertEqual(rc, 0) - self.assertEqual(out, b"a" * 8) - self.assertEqual(err, b"") - - # Now test against concurrent reinitialization - script = "import _datetime\n" + script - rc, out, err = script_helper.assert_python_ok("-c", script) - self.assertEqual(rc, 0) - self.assertEqual(out, b"a" * 8) - self.assertEqual(err, b"") - class TestSubclassDateTime(TestDateTime): theclass = SubclassDatetime @@ -7324,6 +7295,34 @@ def test_update_type_cache(self): """) script_helper.assert_python_ok('-c', script) + def test_concurrent_initialization_subinterpreter(self): + # gh-136421: Concurrent initialization of _datetime across multiple + # interpreters wasn't thread-safe due to its static types. + + # Run in a subprocess to ensure we get a clean version of _datetime + script = """if True: + from concurrent.futures import InterpreterPoolExecutor + + def func(): + import _datetime + print('a', end='') + + with InterpreterPoolExecutor() as executor: + for _ in range(8): + executor.submit(func) + """ + rc, out, err = script_helper.assert_python_ok("-c", script) + self.assertEqual(rc, 0) + self.assertEqual(out, b"a" * 8) + self.assertEqual(err, b"") + + # Now test against concurrent reinitialization + script = "import _datetime\n" + script + rc, out, err = script_helper.assert_python_ok("-c", script) + self.assertEqual(rc, 0) + self.assertEqual(out, b"a" * 8) + self.assertEqual(err, b"") + def load_tests(loader, standard_tests, pattern): standard_tests.addTest(ZoneInfoCompleteTest()) From 3520514922dd050e814b7b0472c5435a912a5ebc Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sun, 20 Jul 2025 19:47:12 +0200 Subject: [PATCH 15/15] Remove dead tp_base code. --- Modules/_datetimemodule.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index a1f6b3bb58f1d4..01039dfeec0719 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -7327,8 +7327,6 @@ clear_state(datetime_state *st) PyStatus _PyDateTime_InitTypes(PyInterpreterState *interp) { - PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType; - /* Bases classes must be initialized before subclasses, * so capi_types must have the types in the appropriate order. */ for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) {