diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index d1882a310bbbb0..49b66e48f27b71 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -7158,7 +7158,8 @@ def test_datetime_from_timestamp(self): self.assertEqual(dt_orig, dt_rt) - def test_type_check_in_subinterp(self): + 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": @@ -7166,10 +7167,10 @@ def test_type_check_in_subinterp(self): else: extension_loader = "ExtensionFileLoader" - script = textwrap.dedent(f""" + code = textwrap.dedent(f''' + subinterp_code = """ if {_interpreters is None}: - import _testcapi as module - module.test_datetime_capi() + import _testcapi else: import importlib.machinery import importlib.util @@ -7177,29 +7178,51 @@ def test_type_check_in_subinterp(self): 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) + _testcapi = importlib.util.module_from_spec(spec) + spec.loader.exec_module(_testcapi) + run_counter = $RUN_COUNTER$ + setup = _testcapi.test_datetime_capi_newinterp # call it if needed + $SCRIPT$ + """ + + import _testcapi + from test import support + setup = _testcapi.test_datetime_capi_newinterp + $INIT$ + + 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: + import _interpreters + 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$', script) + return script_helper.assert_python_ok('-c', code) + 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') + setup() 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_ok_in_subinterp(script) + if _interpreters is not None: + with self.subTest(name := 'legacy'): + self.assert_python_ok_in_subinterp(script, config=name) class ExtensionModuleTests(unittest.TestCase): @@ -7208,6 +7231,9 @@ def setUp(self): if self.__class__.__name__.endswith('Pure'): self.skipTest('Not relevant in pure Python') + 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): with self.subTest('simple'): @@ -7275,6 +7301,59 @@ def test_update_type_cache(self): """) script_helper.assert_python_ok('-c', script) + 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 = type(_testcapi.get_date_fromdate(False, 2000, 1, 1)) + date.today + """) + with_setup = 'setup()' + script + with self.subTest('[PyDateTime_IMPORT] main: no, sub: yes'): + 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_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_ok_in_subinterp(with_import, 'setup()') + + with_import = textwrap.dedent(""" + timedelta = type(_testcapi.get_delta_fromdsu(False, 1, 0, 0)) + timedelta(days=1) + """) + script + with self.subTest('Implicit import'): + self.assert_python_ok_in_subinterp(with_import, 'setup()') + def load_tests(loader, standard_tests, pattern): standard_tests.addTest(ZoneInfoCompleteTest()) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 46222e521aead8..184f6e87fcb59a 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 diff --git a/Modules/_testcapi/datetime.c b/Modules/_testcapi/datetime.c index b800f9b8eb3473..7304e6ea0276e5 100644 --- a/Modules/_testcapi/datetime.c +++ b/Modules/_testcapi/datetime.c @@ -35,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 { \ @@ -475,6 +498,7 @@ static PyMethodDef test_methods[] = { {"get_timezones_offset_zero", get_timezones_offset_zero, 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}, }; @@ -495,9 +519,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 PyDateTime_IMPORT return 0; }