Skip to content
Closed
121 changes: 100 additions & 21 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -7158,48 +7158,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 @@ -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'):
Expand Down Expand Up @@ -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())
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
28 changes: 25 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 @@ -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},
};

Expand All @@ -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;
}

Expand Down
Loading