From 30c30cdfeaeb7104cdaaa59829cd99fade846ffd Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 05:45:01 +0900 Subject: [PATCH 01/12] Update datetime.c --- Modules/_testcapi/datetime.c | 39 +++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/Modules/_testcapi/datetime.c b/Modules/_testcapi/datetime.c index b800f9b8eb3473..42bce301a51277 100644 --- a/Modules/_testcapi/datetime.c +++ b/Modules/_testcapi/datetime.c @@ -11,7 +11,6 @@ test_datetime_capi(PyObject *self, PyObject *args) if (PyDateTimeAPI) { if (test_run_counter) { /* Probably regrtest.py -R */ - Py_RETURN_NONE; } else { PyErr_SetString(PyExc_AssertionError, @@ -20,7 +19,7 @@ test_datetime_capi(PyObject *self, PyObject *args) } } test_run_counter++; - PyDateTime_IMPORT; + PyDateTime_IMPORT; // Ensure interpreters individually import a module if (PyDateTimeAPI == NULL) { return NULL; @@ -453,6 +452,37 @@ test_PyDateTime_DELTA_GET(PyObject *self, PyObject *obj) return Py_BuildValue("(iii)", days, seconds, microseconds); } +static PyObject * +get_capi_types(PyObject *self, PyObject *args) +{ + if (PyDateTimeAPI == NULL) { + Py_RETURN_NONE; + } + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + if (PyDict_SetItemString(dict, "date", (PyObject *)PyDateTimeAPI->DateType) < 0) { + goto error; + } + if (PyDict_SetItemString(dict, "time", (PyObject *)PyDateTimeAPI->TimeType) < 0) { + goto error; + } + if (PyDict_SetItemString(dict, "datetime", (PyObject *)PyDateTimeAPI->DateTimeType) < 0) { + goto error; + } + if (PyDict_SetItemString(dict, "timedelta", (PyObject *)PyDateTimeAPI->DeltaType) < 0) { + goto error; + } + if (PyDict_SetItemString(dict, "tzinfo", (PyObject *)PyDateTimeAPI->TZInfoType) < 0) { + goto error; + } + return dict; +error: + Py_DECREF(dict); + return NULL; +} + static PyMethodDef test_methods[] = { {"PyDateTime_DATE_GET", test_PyDateTime_DATE_GET, METH_O}, {"PyDateTime_DELTA_GET", test_PyDateTime_DELTA_GET, METH_O}, @@ -473,6 +503,7 @@ static PyMethodDef test_methods[] = { {"get_time_fromtimeandfold", get_time_fromtimeandfold, METH_VARARGS}, {"get_timezone_utc_capi", get_timezone_utc_capi, METH_VARARGS}, {"get_timezones_offset_zero", get_timezones_offset_zero, METH_NOARGS}, + {"get_capi_types", get_capi_types, METH_NOARGS}, {"make_timezones_capi", make_timezones_capi, METH_NOARGS}, {"test_datetime_capi", test_datetime_capi, METH_NOARGS}, {NULL}, @@ -495,9 +526,7 @@ _PyTestCapi_Init_DateTime(PyObject *mod) static int _testcapi_datetime_exec(PyObject *mod) { - if (test_datetime_capi(NULL, NULL) == NULL) { - return -1; - } + // The execution does not invoke test_datetime_capi() return 0; } From 9c118439d2269d5024ed4188fbdee6cb02c52a55 Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 05:50:45 +0900 Subject: [PATCH 02/12] Update datetimetester.py --- Lib/test/datetimetester.py | 141 ++++++++++++++++++++++++++++++------- 1 file changed, 114 insertions(+), 27 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 55844ec35a90c9..a63f2453e3e256 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7155,7 +7155,9 @@ def test_datetime_from_timestamp(self): self.assertEqual(dt_orig, dt_rt) - def test_type_check_in_subinterp(self): + def assert_python_in_subinterp(self, check_if_ok: bool, script, + setup='_testcapi.test_datetime_capi()', + config='isolated'): # iOS requires the use of the custom framework loader, # not the ExtensionFileLoader. if sys.platform == "ios": @@ -7163,40 +7165,66 @@ def test_type_check_in_subinterp(self): else: extension_loader = "ExtensionFileLoader" - script = textwrap.dedent(f""" + code = textwrap.dedent(f''' + import textwrap + from test import support + + subcode = textwrap.dedent(""" + if {_interpreters is None}: + import _testcapi + else: + import importlib.machinery + import importlib.util + fullname = '_testcapi_datetime' + origin = importlib.util.find_spec('_testcapi').origin + loader = importlib.machinery.{extension_loader}(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + _testcapi = importlib.util.module_from_spec(spec) + spec.loader.exec_module(_testcapi) + + $SCRIPT$ + """) + + import _testcapi + $SETUP$ + if {_interpreters is None}: - import _testcapi as module - module.test_datetime_capi() + ret = support.run_in_subinterp(subcode) else: - import importlib.machinery - import importlib.util - fullname = '_testcapi_datetime' - origin = importlib.util.find_spec('_testcapi').origin - loader = importlib.machinery.{extension_loader}(fullname, origin) - spec = importlib.util.spec_from_loader(fullname, loader) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) + import _interpreters + config = _interpreters.new_config('{config}').__dict__ + ret = support.run_in_subinterp_with_config(subcode, **config) + assert ret == 0 + + ''').rstrip() + code = code.replace('$SETUP$', setup) + code = code.replace('$SCRIPT$', textwrap.indent(script, '\x20'*4)) + + if check_if_ok: + res = script_helper.assert_python_ok('-c', code) + else: + res = script_helper.assert_python_failure('-c', code) + return res + + def test_type_check_in_subinterp(self): + script = textwrap.dedent(f""" def run(type_checker, obj): if not type_checker(obj, True): raise TypeError(f'{{type(obj)}} is not C API type') + _testcapi.test_datetime_capi() import _datetime - run(module.datetime_check_date, _datetime.date.today()) - run(module.datetime_check_datetime, _datetime.datetime.now()) - run(module.datetime_check_time, _datetime.time(12, 30)) - run(module.datetime_check_delta, _datetime.timedelta(1)) - run(module.datetime_check_tzinfo, _datetime.tzinfo()) - """) - if _interpreters is None: - ret = support.run_in_subinterp(script) - self.assertEqual(ret, 0) - else: - for name in ('isolated', 'legacy'): - with self.subTest(name): - config = _interpreters.new_config(name).__dict__ - ret = support.run_in_subinterp_with_config(script, **config) - self.assertEqual(ret, 0) + run(_testcapi.datetime_check_date, _datetime.date.today()) + run(_testcapi.datetime_check_datetime, _datetime.datetime.now()) + run(_testcapi.datetime_check_time, _datetime.time(12, 30)) + run(_testcapi.datetime_check_delta, _datetime.timedelta(1)) + run(_testcapi.datetime_check_tzinfo, _datetime.tzinfo()) + """) + self.assert_python_in_subinterp(True, script, '') + if _interpreters is not None: + with self.subTest(name := 'legacy'): + self.assert_python_in_subinterp(True, script, '', name) class ExtensionModuleTests(unittest.TestCase): @@ -7205,6 +7233,9 @@ def setUp(self): if self.__class__.__name__.endswith('Pure'): self.skipTest('Not relevant in pure Python') + def assert_python_in_subinterp(self, *args, **kwargs): + return CapiTest.assert_python_in_subinterp(self, *args, **kwargs) + @support.cpython_only def test_gh_120161(self): with self.subTest('simple'): @@ -7270,8 +7301,64 @@ def test_update_type_cache(self): assert isinstance(_datetime.timezone.utc, _datetime.tzinfo) del sys.modules['_datetime'] """) + res = script_helper.assert_python_ok('-c', script) + self.assertFalse(res.err) + + def test_module_free(self): + script = textwrap.dedent(""" + import sys + import gc + import weakref + ws = weakref.WeakSet() + for _ in range(3): + import _datetime + timedelta = _datetime.timedelta # static type + ws.add(_datetime) + del sys.modules['_datetime'] + del _datetime + gc.collect() + assert len(ws) == 0 + """) script_helper.assert_python_ok('-c', script) + @unittest.skipIf(not support.Py_DEBUG, "Debug builds only") + def test_no_leak(self): + script = textwrap.dedent(""" + import datetime + datetime.datetime.strptime('20000101', '%Y%m%d').strftime('%Y%m%d') + """) + res = script_helper.assert_python_ok('-X', 'showrefcount', '-c', script) + self.assertIn(b'[0 refs, 0 blocks]', res.err) + + def test_static_type_on_subinterp(self): + script = textwrap.dedent(""" + date = _testcapi.get_capi_types()['date'] + date.today + """) + # Test the script fails + with self.subTest('[PyDateTime_IMPORT] main: yes, sub: no'): + self.assert_python_in_subinterp(False, script) + + # Test the script succeeds + script2 = '_testcapi.test_datetime_capi()' + script + with self.subTest('[PyDateTime_IMPORT] main: no, sub: yes'): + self.assert_python_in_subinterp(True, script2, setup='') + + with self.subTest('[PyDateTime_IMPORT] main: yes, sub: yes'): + # Check if PyDateTime_IMPORT is invoked not only once + self.assert_python_in_subinterp(True, script2) + + script3 = 'import _datetime' + script + with self.subTest('Explicit import'): + self.assert_python_in_subinterp(True, script3) + + script4 = textwrap.dedent(""" + timedelta = _testcapi.get_capi_types()['timedelta'] + timedelta(days=1) + """) + script + with self.subTest('Implicit import'): + self.assert_python_in_subinterp(True, script4) + def load_tests(loader, standard_tests, pattern): standard_tests.addTest(ZoneInfoCompleteTest()) From da91cdba2b3f35691a89672f9721f5a0f338abff Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 08:43:41 +0900 Subject: [PATCH 03/12] Add a note --- Lib/test/datetimetester.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index a63f2453e3e256..3fb2791a34b7e5 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7182,6 +7182,8 @@ def assert_python_in_subinterp(self, check_if_ok: bool, script, _testcapi = importlib.util.module_from_spec(spec) spec.loader.exec_module(_testcapi) + # Delegates the setup to the given script + $SCRIPT$ """) From 7d79dd94458b6f54d969ce511326fd6eb0b334bc Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 17:25:57 +0900 Subject: [PATCH 04/12] Reword --- Lib/test/datetimetester.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 3fb2791a34b7e5..b1a5b1f60b0c0f 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7337,11 +7337,10 @@ def test_static_type_on_subinterp(self): date = _testcapi.get_capi_types()['date'] date.today """) - # Test the script fails + # FIXME: Segfault with self.subTest('[PyDateTime_IMPORT] main: yes, sub: no'): self.assert_python_in_subinterp(False, script) - # Test the script succeeds script2 = '_testcapi.test_datetime_capi()' + script with self.subTest('[PyDateTime_IMPORT] main: no, sub: yes'): self.assert_python_in_subinterp(True, script2, setup='') From a8624560e1ec34dcdbd98a0e64e5a76a569986eb Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:00:46 +0900 Subject: [PATCH 05/12] Rename a keyword to be replaced --- Lib/test/datetimetester.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index b1a5b1f60b0c0f..39b299a5662c67 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7183,8 +7183,7 @@ def assert_python_in_subinterp(self, check_if_ok: bool, script, spec.loader.exec_module(_testcapi) # Delegates the setup to the given script - - $SCRIPT$ + ____$SCRIPT$ """) import _testcapi @@ -7201,7 +7200,7 @@ def assert_python_in_subinterp(self, check_if_ok: bool, script, ''').rstrip() code = code.replace('$SETUP$', setup) - code = code.replace('$SCRIPT$', textwrap.indent(script, '\x20'*4)) + code = code.replace('____$SCRIPT$', textwrap.indent(script, '\x20'*4)) if check_if_ok: res = script_helper.assert_python_ok('-c', code) From be5791c396ad306c759c7937c47963bac5cd04b9 Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:15:59 +0900 Subject: [PATCH 06/12] Reconsider arguments --- Lib/test/datetimetester.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 39b299a5662c67..d112351cbb3114 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7155,9 +7155,8 @@ def test_datetime_from_timestamp(self): self.assertEqual(dt_orig, dt_rt) - def assert_python_in_subinterp(self, check_if_ok: bool, script, - setup='_testcapi.test_datetime_capi()', - config='isolated'): + def assert_python_in_subinterp(self, check_if_ok, script, + init='', fini='', config='isolated'): # iOS requires the use of the custom framework loader, # not the ExtensionFileLoader. if sys.platform == "ios": @@ -7182,12 +7181,13 @@ def assert_python_in_subinterp(self, check_if_ok: bool, script, _testcapi = importlib.util.module_from_spec(spec) spec.loader.exec_module(_testcapi) - # Delegates the setup to the given script + setup = _testcapi.test_datetime_capi ____$SCRIPT$ """) import _testcapi - $SETUP$ + setup = _testcapi.test_datetime_capi # run on the given texts + $INIT$ if {_interpreters is None}: ret = support.run_in_subinterp(subcode) @@ -7197,9 +7197,10 @@ def assert_python_in_subinterp(self, check_if_ok: bool, script, ret = support.run_in_subinterp_with_config(subcode, **config) assert ret == 0 + $FINI$ ''').rstrip() - code = code.replace('$SETUP$', setup) + code = code.replace('$INIT$', init).replace('$FINI$', fini) code = code.replace('____$SCRIPT$', textwrap.indent(script, '\x20'*4)) if check_if_ok: @@ -7214,7 +7215,7 @@ def run(type_checker, obj): if not type_checker(obj, True): raise TypeError(f'{{type(obj)}} is not C API type') - _testcapi.test_datetime_capi() + setup() import _datetime run(_testcapi.datetime_check_date, _datetime.date.today()) run(_testcapi.datetime_check_datetime, _datetime.datetime.now()) @@ -7222,10 +7223,10 @@ def run(type_checker, obj): run(_testcapi.datetime_check_delta, _datetime.timedelta(1)) run(_testcapi.datetime_check_tzinfo, _datetime.tzinfo()) """) - self.assert_python_in_subinterp(True, script, '') + self.assert_python_in_subinterp(True, script) if _interpreters is not None: with self.subTest(name := 'legacy'): - self.assert_python_in_subinterp(True, script, '', name) + self.assert_python_in_subinterp(True, script, config=name) class ExtensionModuleTests(unittest.TestCase): @@ -7336,28 +7337,28 @@ def test_static_type_on_subinterp(self): date = _testcapi.get_capi_types()['date'] date.today """) - # FIXME: Segfault with self.subTest('[PyDateTime_IMPORT] main: yes, sub: no'): - self.assert_python_in_subinterp(False, script) + # FIXME: Segfault + self.assert_python_in_subinterp(False, script, 'setup()') - script2 = '_testcapi.test_datetime_capi()' + script + with_setup = 'setup()' + script with self.subTest('[PyDateTime_IMPORT] main: no, sub: yes'): - self.assert_python_in_subinterp(True, script2, setup='') + self.assert_python_in_subinterp(True, with_setup) with self.subTest('[PyDateTime_IMPORT] main: yes, sub: yes'): # Check if PyDateTime_IMPORT is invoked not only once - self.assert_python_in_subinterp(True, script2) + self.assert_python_in_subinterp(True, with_setup, 'setup()') - script3 = 'import _datetime' + script + with_import = 'import _datetime' + script with self.subTest('Explicit import'): - self.assert_python_in_subinterp(True, script3) + self.assert_python_in_subinterp(True, with_import, 'setup()') - script4 = textwrap.dedent(""" + with_import = textwrap.dedent(""" timedelta = _testcapi.get_capi_types()['timedelta'] timedelta(days=1) """) + script with self.subTest('Implicit import'): - self.assert_python_in_subinterp(True, script4) + self.assert_python_in_subinterp(True, with_import, 'setup()') def load_tests(loader, standard_tests, pattern): From cccdd2ce8b1a555c0859fcd111132c6825aeccec Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Tue, 29 Apr 2025 23:34:54 +0900 Subject: [PATCH 07/12] Add a testcase --- Lib/test/datetimetester.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index d112351cbb3114..88edae0d12c735 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7198,8 +7198,7 @@ def assert_python_in_subinterp(self, check_if_ok, script, assert ret == 0 $FINI$ - - ''').rstrip() + ''') code = code.replace('$INIT$', init).replace('$FINI$', fini) code = code.replace('____$SCRIPT$', textwrap.indent(script, '\x20'*4)) @@ -7348,6 +7347,7 @@ def test_static_type_on_subinterp(self): with self.subTest('[PyDateTime_IMPORT] main: yes, sub: yes'): # Check if PyDateTime_IMPORT is invoked not only once self.assert_python_in_subinterp(True, with_setup, 'setup()') + self.assert_python_in_subinterp(True, 'setup()', fini=with_setup) with_import = 'import _datetime' + script with self.subTest('Explicit import'): From 4298825011499014917e96bab813fb69a84d6b05 Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Wed, 30 Apr 2025 03:43:39 +0900 Subject: [PATCH 08/12] Add a test for using subinterp twice --- Lib/test/datetimetester.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 88edae0d12c735..213de51ece45ec 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7155,8 +7155,8 @@ def test_datetime_from_timestamp(self): self.assertEqual(dt_orig, dt_rt) - def assert_python_in_subinterp(self, check_if_ok, script, - init='', fini='', config='isolated'): + def assert_python_in_subinterp(self, check_if_ok, script, init='', + fini='', repeat=1, config='isolated'): # iOS requires the use of the custom framework loader, # not the ExtensionFileLoader. if sys.platform == "ios": @@ -7168,7 +7168,7 @@ def assert_python_in_subinterp(self, check_if_ok, script, import textwrap from test import support - subcode = textwrap.dedent(""" + subinterp_code = textwrap.dedent(""" if {_interpreters is None}: import _testcapi else: @@ -7180,23 +7180,26 @@ def assert_python_in_subinterp(self, check_if_ok, script, spec = importlib.util.spec_from_loader(fullname, loader) _testcapi = importlib.util.module_from_spec(spec) spec.loader.exec_module(_testcapi) + INDEX = $INDEX$ - setup = _testcapi.test_datetime_capi + setup = _testcapi.test_datetime_capi # call it if needed ____$SCRIPT$ """) import _testcapi - setup = _testcapi.test_datetime_capi # run on the given texts + setup = _testcapi.test_datetime_capi $INIT$ - if {_interpreters is None}: - ret = support.run_in_subinterp(subcode) - else: - import _interpreters - config = _interpreters.new_config('{config}').__dict__ - ret = support.run_in_subinterp_with_config(subcode, **config) + for idx in range({repeat}): + subcode = subinterp_code.replace('$INDEX$', str(idx)) + if {_interpreters is None}: + ret = support.run_in_subinterp(subcode) + else: + import _interpreters + config = _interpreters.new_config('{config}').__dict__ + ret = support.run_in_subinterp_with_config(subcode, **config) + assert ret == 0 - assert ret == 0 $FINI$ ''') code = code.replace('$INIT$', init).replace('$FINI$', fini) @@ -7348,6 +7351,7 @@ def test_static_type_on_subinterp(self): # Check if PyDateTime_IMPORT is invoked not only once self.assert_python_in_subinterp(True, with_setup, 'setup()') self.assert_python_in_subinterp(True, 'setup()', fini=with_setup) + self.assert_python_in_subinterp(True, with_setup, repeat=2) with_import = 'import _datetime' + script with self.subTest('Explicit import'): From b3f3f2102909a265d1e33e96986e7c783b9e2d3f Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Wed, 30 Apr 2025 06:14:49 +0900 Subject: [PATCH 09/12] Do not dedent subinterp's code --- Lib/test/datetimetester.py | 40 +++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 213de51ece45ec..1c4672a103e897 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7165,28 +7165,25 @@ def assert_python_in_subinterp(self, check_if_ok, script, init='', extension_loader = "ExtensionFileLoader" code = textwrap.dedent(f''' - import textwrap - from test import support - - subinterp_code = textwrap.dedent(""" - if {_interpreters is None}: - import _testcapi - else: - import importlib.machinery - import importlib.util - fullname = '_testcapi_datetime' - origin = importlib.util.find_spec('_testcapi').origin - loader = importlib.machinery.{extension_loader}(fullname, origin) - spec = importlib.util.spec_from_loader(fullname, loader) - _testcapi = importlib.util.module_from_spec(spec) - spec.loader.exec_module(_testcapi) - INDEX = $INDEX$ - - setup = _testcapi.test_datetime_capi # call it if needed - ____$SCRIPT$ - """) + subinterp_code = """ + if {_interpreters is None}: + import _testcapi + else: + import importlib.machinery + import importlib.util + fullname = '_testcapi_datetime' + origin = importlib.util.find_spec('_testcapi').origin + loader = importlib.machinery.{extension_loader}(fullname, origin) + spec = importlib.util.spec_from_loader(fullname, loader) + _testcapi = importlib.util.module_from_spec(spec) + spec.loader.exec_module(_testcapi) + INDEX = $INDEX$ + setup = _testcapi.test_datetime_capi # call it if needed + $SCRIPT$ + """ import _testcapi + from test import support setup = _testcapi.test_datetime_capi $INIT$ @@ -7199,11 +7196,10 @@ def assert_python_in_subinterp(self, check_if_ok, script, init='', config = _interpreters.new_config('{config}').__dict__ ret = support.run_in_subinterp_with_config(subcode, **config) assert ret == 0 - $FINI$ ''') code = code.replace('$INIT$', init).replace('$FINI$', fini) - code = code.replace('____$SCRIPT$', textwrap.indent(script, '\x20'*4)) + code = code.replace('$SCRIPT$', script) if check_if_ok: res = script_helper.assert_python_ok('-c', code) From 0ee6e3d2abb24433150887bc22452168433bb376 Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Thu, 1 May 2025 02:12:59 +0900 Subject: [PATCH 10/12] Introduce test_datetime_capi_newinterp() --- Lib/test/datetimetester.py | 11 ++++------- Modules/_testcapi/datetime.c | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 1c4672a103e897..177aa6fba3dfdf 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7178,13 +7178,13 @@ def assert_python_in_subinterp(self, check_if_ok, script, init='', _testcapi = importlib.util.module_from_spec(spec) spec.loader.exec_module(_testcapi) INDEX = $INDEX$ - setup = _testcapi.test_datetime_capi # call it if needed + setup = _testcapi.test_datetime_capi_newinterp # call it if needed $SCRIPT$ """ import _testcapi from test import support - setup = _testcapi.test_datetime_capi + setup = _testcapi.test_datetime_capi_newinterp $INIT$ for idx in range({repeat}): @@ -7335,16 +7335,13 @@ def test_static_type_on_subinterp(self): date = _testcapi.get_capi_types()['date'] date.today """) - with self.subTest('[PyDateTime_IMPORT] main: yes, sub: no'): - # FIXME: Segfault - self.assert_python_in_subinterp(False, script, 'setup()') - with_setup = 'setup()' + script with self.subTest('[PyDateTime_IMPORT] main: no, sub: yes'): self.assert_python_in_subinterp(True, with_setup) with self.subTest('[PyDateTime_IMPORT] main: yes, sub: yes'): - # Check if PyDateTime_IMPORT is invoked not only once + # Fails if the setup() means test_datetime_capi() rather than + # test_datetime_capi_newinterp() self.assert_python_in_subinterp(True, with_setup, 'setup()') self.assert_python_in_subinterp(True, 'setup()', fini=with_setup) self.assert_python_in_subinterp(True, with_setup, repeat=2) diff --git a/Modules/_testcapi/datetime.c b/Modules/_testcapi/datetime.c index 42bce301a51277..375196e28fb727 100644 --- a/Modules/_testcapi/datetime.c +++ b/Modules/_testcapi/datetime.c @@ -11,6 +11,7 @@ test_datetime_capi(PyObject *self, PyObject *args) if (PyDateTimeAPI) { if (test_run_counter) { /* Probably regrtest.py -R */ + Py_RETURN_NONE; } else { PyErr_SetString(PyExc_AssertionError, @@ -19,7 +20,7 @@ test_datetime_capi(PyObject *self, PyObject *args) } } test_run_counter++; - PyDateTime_IMPORT; // Ensure interpreters individually import a module + PyDateTime_IMPORT; if (PyDateTimeAPI == NULL) { return NULL; @@ -34,6 +35,29 @@ test_datetime_capi(PyObject *self, PyObject *args) Py_RETURN_NONE; } +static PyObject * +test_datetime_capi_newinterp(PyObject *self, PyObject *args) +{ + // Call PyDateTime_IMPORT at least once in each interpreter's life + if (PyDateTimeAPI != NULL && test_run_counter == 0) { + PyErr_SetString(PyExc_AssertionError, + "PyDateTime_CAPI somehow initialized"); + return NULL; + } + test_run_counter++; + PyDateTime_IMPORT; + + if (PyDateTimeAPI == NULL) { + return NULL; + } + assert(!PyType_HasFeature(PyDateTimeAPI->DateType, Py_TPFLAGS_HEAPTYPE)); + assert(!PyType_HasFeature(PyDateTimeAPI->TimeType, Py_TPFLAGS_HEAPTYPE)); + assert(!PyType_HasFeature(PyDateTimeAPI->DateTimeType, Py_TPFLAGS_HEAPTYPE)); + assert(!PyType_HasFeature(PyDateTimeAPI->DeltaType, Py_TPFLAGS_HEAPTYPE)); + assert(!PyType_HasFeature(PyDateTimeAPI->TZInfoType, Py_TPFLAGS_HEAPTYPE)); + Py_RETURN_NONE; +} + /* Functions exposing the C API type checking for testing */ #define MAKE_DATETIME_CHECK_FUNC(check_method, exact_method) \ do { \ @@ -506,6 +530,7 @@ static PyMethodDef test_methods[] = { {"get_capi_types", get_capi_types, METH_NOARGS}, {"make_timezones_capi", make_timezones_capi, METH_NOARGS}, {"test_datetime_capi", test_datetime_capi, METH_NOARGS}, + {"test_datetime_capi_newinterp",test_datetime_capi_newinterp, METH_NOARGS}, {NULL}, }; @@ -526,7 +551,7 @@ _PyTestCapi_Init_DateTime(PyObject *mod) static int _testcapi_datetime_exec(PyObject *mod) { - // The execution does not invoke test_datetime_capi() + // The execution does not invoke PyDateTime_IMPORT return 0; } From ca98adb2d03f8e3bf022b2353dae4477153b8527 Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Thu, 1 May 2025 09:09:56 +0900 Subject: [PATCH 11/12] Cleanup --- Lib/test/datetimetester.py | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 177aa6fba3dfdf..e79cf1333b35b0 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7155,8 +7155,8 @@ def test_datetime_from_timestamp(self): self.assertEqual(dt_orig, dt_rt) - def assert_python_in_subinterp(self, check_if_ok, script, init='', - fini='', repeat=1, config='isolated'): + def assert_python_ok_in_subinterp(self, script, init='', fini='', + repeat=1, config='isolated'): # iOS requires the use of the custom framework loader, # not the ExtensionFileLoader. if sys.platform == "ios": @@ -7177,7 +7177,7 @@ def assert_python_in_subinterp(self, check_if_ok, script, init='', spec = importlib.util.spec_from_loader(fullname, loader) _testcapi = importlib.util.module_from_spec(spec) spec.loader.exec_module(_testcapi) - INDEX = $INDEX$ + run_counter = $RUN_COUNTER$ setup = _testcapi.test_datetime_capi_newinterp # call it if needed $SCRIPT$ """ @@ -7187,8 +7187,8 @@ def assert_python_in_subinterp(self, check_if_ok, script, init='', setup = _testcapi.test_datetime_capi_newinterp $INIT$ - for idx in range({repeat}): - subcode = subinterp_code.replace('$INDEX$', str(idx)) + for i in range(1, {1+repeat}): + subcode = subinterp_code.replace('$RUN_COUNTER$', str(i)) if {_interpreters is None}: ret = support.run_in_subinterp(subcode) else: @@ -7201,10 +7201,7 @@ def assert_python_in_subinterp(self, check_if_ok, script, init='', code = code.replace('$INIT$', init).replace('$FINI$', fini) code = code.replace('$SCRIPT$', script) - if check_if_ok: - res = script_helper.assert_python_ok('-c', code) - else: - res = script_helper.assert_python_failure('-c', code) + res = script_helper.assert_python_ok('-c', code) return res def test_type_check_in_subinterp(self): @@ -7221,10 +7218,10 @@ def run(type_checker, obj): run(_testcapi.datetime_check_delta, _datetime.timedelta(1)) run(_testcapi.datetime_check_tzinfo, _datetime.tzinfo()) """) - self.assert_python_in_subinterp(True, script) + self.assert_python_ok_in_subinterp(script) if _interpreters is not None: with self.subTest(name := 'legacy'): - self.assert_python_in_subinterp(True, script, config=name) + self.assert_python_ok_in_subinterp(script, config=name) class ExtensionModuleTests(unittest.TestCase): @@ -7233,8 +7230,8 @@ def setUp(self): if self.__class__.__name__.endswith('Pure'): self.skipTest('Not relevant in pure Python') - def assert_python_in_subinterp(self, *args, **kwargs): - return CapiTest.assert_python_in_subinterp(self, *args, **kwargs) + def assert_python_ok_in_subinterp(self, *args, **kwargs): + return CapiTest.assert_python_ok_in_subinterp(self, *args, **kwargs) @support.cpython_only def test_gh_120161(self): @@ -7337,25 +7334,25 @@ def test_static_type_on_subinterp(self): """) with_setup = 'setup()' + script with self.subTest('[PyDateTime_IMPORT] main: no, sub: yes'): - self.assert_python_in_subinterp(True, with_setup) + self.assert_python_ok_in_subinterp(with_setup) with self.subTest('[PyDateTime_IMPORT] main: yes, sub: yes'): # Fails if the setup() means test_datetime_capi() rather than # test_datetime_capi_newinterp() - self.assert_python_in_subinterp(True, with_setup, 'setup()') - self.assert_python_in_subinterp(True, 'setup()', fini=with_setup) - self.assert_python_in_subinterp(True, with_setup, repeat=2) + self.assert_python_ok_in_subinterp(with_setup, 'setup()') + self.assert_python_ok_in_subinterp('setup()', fini=with_setup) + self.assert_python_ok_in_subinterp(with_setup, repeat=2) with_import = 'import _datetime' + script with self.subTest('Explicit import'): - self.assert_python_in_subinterp(True, with_import, 'setup()') + self.assert_python_ok_in_subinterp(with_import, 'setup()') with_import = textwrap.dedent(""" timedelta = _testcapi.get_capi_types()['timedelta'] timedelta(days=1) """) + script with self.subTest('Implicit import'): - self.assert_python_in_subinterp(True, with_import, 'setup()') + self.assert_python_ok_in_subinterp(with_import, 'setup()') def load_tests(loader, standard_tests, pattern): From 8bcad71e6b18b63f8549c7ca840321604bbec6b1 Mon Sep 17 00:00:00 2001 From: neonene <53406459+neonene@users.noreply.github.com> Date: Thu, 1 May 2025 19:06:55 +0900 Subject: [PATCH 12/12] Add a test to test_embed --- Lib/test/datetimetester.py | 6 ++---- Lib/test/test_embed.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index e79cf1333b35b0..1bf74cc9bd01b9 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7187,7 +7187,7 @@ def assert_python_ok_in_subinterp(self, script, init='', fini='', setup = _testcapi.test_datetime_capi_newinterp $INIT$ - for i in range(1, {1+repeat}): + for i in range(1, {1 + repeat}): subcode = subinterp_code.replace('$RUN_COUNTER$', str(i)) if {_interpreters is None}: ret = support.run_in_subinterp(subcode) @@ -7200,9 +7200,7 @@ def assert_python_ok_in_subinterp(self, script, init='', fini='', ''') code = code.replace('$INIT$', init).replace('$FINI$', fini) code = code.replace('$SCRIPT$', script) - - res = script_helper.assert_python_ok('-c', code) - return res + return script_helper.assert_python_ok('-c', code) def test_type_check_in_subinterp(self): script = textwrap.dedent(f""" diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index e06e684408ca6b..16b1e0925b9836 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -440,6 +440,21 @@ def test_datetime_reset_strptime(self): out, err = self.run_embedded_interpreter("test_repeated_init_exec", code) self.assertEqual(out, '20000101\n' * INIT_LOOPS) + def test_datetime_capi_type_address(self): + # Check if the C-API types keep their addresses until runtime shutdown + code = textwrap.dedent(""" + import _datetime as d + print( + f'{id(d.date)}' + f'{id(d.time)}' + f'{id(d.datetime)}' + f'{id(d.timedelta)}' + f'{id(d.tzinfo)}' + ) + """) + out, err = self.run_embedded_interpreter("test_repeated_init_exec", code) + self.assertEqual(len(set(out.splitlines())), 1) + def test_static_types_inherited_slots(self): script = textwrap.dedent(""" import test.support