Skip to content
Closed
141 changes: 114 additions & 27 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -7155,48 +7155,77 @@ 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":
extension_loader = "AppleFrameworkLoader"
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)

# Delegates the setup to the given script
____$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):
Expand All @@ -7205,6 +7234,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'):
Expand Down Expand Up @@ -7270,8 +7302,63 @@ 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
""")
# FIXME: Segfault
with self.subTest('[PyDateTime_IMPORT] main: yes, sub: no'):
self.assert_python_in_subinterp(False, script)

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())
Expand Down
39 changes: 34 additions & 5 deletions Modules/_testcapi/datetime.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand All @@ -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;
}

Expand Down
Loading