Skip to content

gh-132413: Extend datetime C-API tests for subinterpreters #133111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
122 changes: 101 additions & 21 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -7155,48 +7155,71 @@ 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":
extension_loader = "AppleFrameworkLoader"
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
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)
_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):
Expand All @@ -7205,6 +7228,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'):
Expand Down Expand Up @@ -7270,8 +7296,62 @@ 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
""")
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 = _testcapi.get_capi_types()['timedelta']
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())
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 57 additions & 3 deletions Modules/_testcapi/datetime.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 { \
Expand Down Expand Up @@ -453,6 +476,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},
Expand All @@ -473,8 +527,10 @@ 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},
{"test_datetime_capi_newinterp",test_datetime_capi_newinterp, METH_NOARGS},
{NULL},
};

Expand All @@ -495,9 +551,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;
}

Expand Down
Loading