Skip to content
Closed
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a2d90ae
Update datetimetester.py
neonene Apr 16, 2025
37d317f
Make interp-dict have a strong ref to the module
neonene Apr 16, 2025
44d09c9
Fix exc leak
neonene Apr 16, 2025
19326d4
📜🤖 Added by blurb_it.
blurb-it[bot] Apr 16, 2025
c74251d
Merge branch 'main' into fix-132413
neonene Apr 16, 2025
f805ec7
Add assertion in test
neonene Apr 17, 2025
1539cc6
More assertion
neonene Apr 17, 2025
e6aa018
Allow only module's exec() to create interp-dict
neonene Apr 17, 2025
5141a76
Safer ref cycle between mod and interp-dict
neonene Apr 17, 2025
76cc153
Respect interp-dict held in module state
neonene Apr 17, 2025
e5bb7c8
Update _datetimemodule.c
neonene Apr 20, 2025
2645282
Merge branch 'main' into fix-132413
neonene Apr 20, 2025
87aa7a2
Update datetimetester.py
neonene Apr 20, 2025
b425bc8
Reword
neonene Apr 21, 2025
19de232
typo
neonene Apr 21, 2025
f7b78d9
Reword
neonene Apr 22, 2025
05811bb
Cleanup
neonene Apr 22, 2025
1416c6f
Merge branch 'main' into fix-132413
neonene Apr 22, 2025
351ac36
assert(!_Py_IsInterpreterFinalizing())
neonene Apr 24, 2025
0e4a263
Add tests
neonene Apr 25, 2025
fa0cc40
Update tests
neonene Apr 26, 2025
0377764
Take account of interp restart
neonene Apr 26, 2025
9af13a6
Add subinterp tests
neonene Apr 27, 2025
124d650
Fix tests for free-threaded builds
neonene Apr 27, 2025
c490586
Nit
neonene Apr 27, 2025
29c45a3
Merge branch 'main' into fix-132413
neonene Apr 27, 2025
db47683
Make tests generic
neonene Apr 27, 2025
ddd1635
Fix warnings
neonene Apr 27, 2025
746bf85
Update tests
neonene Apr 30, 2025
1cdaf5a
Merge branch 'main' into fix-132413
neonene Apr 30, 2025
a8f76fa
Revert to original (main)
neonene May 7, 2025
c7fc55e
Convert IsoCalendarDate to static type (3.12)
neonene May 7, 2025
b3d8a8e
Merge branch 'main' into fix-132413
neonene May 7, 2025
539cfed
Fix warning
neonene May 7, 2025
f2373f4
Merge branch 'main' into fix-132413
neonene May 8, 2025
d9f8544
Fix refleaks
neonene May 8, 2025
07ca91f
Ditto
neonene May 8, 2025
13b065f
Move up a function
neonene May 9, 2025
0052451
Smaller patch for tests
neonene May 13, 2025
fb29db1
Merge branch 'main' into fix-132413
neonene May 13, 2025
d2e2a11
Add a non-issue test case (closure)
neonene May 18, 2025
2da198e
minimize test
neonene Jun 23, 2025
2392247
Merge branch 'main' into fix-132413
neonene Jun 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -7270,8 +7270,60 @@ 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_at_shutdown(self):
# gh-132413
script = textwrap.dedent("""
import sys
import _datetime
timedelta = _datetime.timedelta

def gen():
try:
yield
finally:
# Exceptions are ignored here
assert not sys.modules
td = _datetime.timedelta(days=1)
assert td.days == 1
td = timedelta(days=1)
assert td.days == 1
assert not sys.modules

it = gen()
next(it)
""")
res = script_helper.assert_python_ok('-c', script)
self.assertFalse(res.err)


def load_tests(loader, standard_tests, pattern):
standard_tests.addTest(ZoneInfoCompleteTest())
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,48 @@ 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_datetime_capi_at_shutdown(self):
# gh-132413
code = textwrap.dedent("""
import sys
import _testcapi
_testcapi.test_datetime_capi() # PyDateTime_IMPORT only once
timedelta = _testcapi.get_capi_types()['timedelta']

def gen():
try:
yield
finally:
assert not sys.modules
res = 0
try:
timedelta(days=1)
res = 1
except ImportError:
res = 2
print(res)

it = gen()
next(it)
""")
out, err = self.run_embedded_interpreter("test_repeated_init_exec", code)
self.assertEqual(out, '1\n' * INIT_LOOPS)

def test_static_types_inherited_slots(self):
script = textwrap.dedent("""
import test.support
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix crash in C version of :mod:`datetime` when used during interpreter shutdown.
153 changes: 75 additions & 78 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ static PyTypeObject PyDateTime_TimeType;
static PyTypeObject PyDateTime_DeltaType;
static PyTypeObject PyDateTime_TZInfoType;
static PyTypeObject PyDateTime_TimeZoneType;
static PyTypeObject PyDateTime_IsoCalendarDateType;


typedef struct {
/* Module heap types. */
PyTypeObject *isocalendar_date_type;

/* Conversion factors. */
PyObject *us_per_ms; // 1_000
PyObject *us_per_second; // 1_000_000
Expand Down Expand Up @@ -75,7 +73,7 @@ typedef struct {
#define DELTA_TYPE(st) &PyDateTime_DeltaType
#define TZINFO_TYPE(st) &PyDateTime_TZInfoType
#define TIMEZONE_TYPE(st) &PyDateTime_TimeZoneType
#define ISOCALENDAR_DATE_TYPE(st) st->isocalendar_date_type
#define ISOCALENDAR_DATE_TYPE(st) &PyDateTime_IsoCalendarDateType

#define PyDate_CAST(op) ((PyDateTime_Date *)(op))
#define PyDate_Check(op) PyObject_TypeCheck(op, DATE_TYPE(NO_STATE))
Expand All @@ -102,15 +100,52 @@ typedef struct {

#define PyIsoCalendarDate_CAST(op) ((PyDateTime_IsoCalendarDate *)(op))

#define CONST_US_PER_MS(st) st->us_per_ms
#define CONST_US_PER_SECOND(st) st->us_per_second
#define CONST_US_PER_MINUTE(st) st->us_per_minute
#define CONST_US_PER_HOUR(st) st->us_per_hour
#define CONST_US_PER_DAY(st) st->us_per_day
#define CONST_US_PER_WEEK(st) st->us_per_week
#define CONST_SEC_PER_DAY(st) st->seconds_per_day
#define CONST_EPOCH(st) st->epoch
static inline PyObject *
get_const_us_per_ms(datetime_state *st) {
return st ? st->us_per_ms : PyLong_FromLong(1000);
}

static inline PyObject *
get_const_us_per_second(datetime_state *st) {
return st ? st->us_per_second : PyLong_FromLong(1000000);
}

static inline PyObject *
get_const_us_per_minute(datetime_state *st) {
return st ? st->us_per_minute : PyLong_FromLong(60000000);
}

static inline PyObject *
get_const_us_per_hour(datetime_state *st) {
return st ? st->us_per_hour : PyLong_FromDouble(3600000000.0);
}

static inline PyObject *
get_const_us_per_day(datetime_state *st) {
return st ? st->us_per_day : PyLong_FromDouble(86400000000.0);
}

static inline PyObject *
get_const_us_per_week(datetime_state *st) {
return st ? st->us_per_week : PyLong_FromDouble(604800000000.0);
}

static inline PyObject *
get_const_sec_per_day(datetime_state *st) {
return st ? st->seconds_per_day : PyLong_FromLong(24 * 3600);
}

#define CONST_US_PER_MS(st) get_const_us_per_ms(st)
#define CONST_US_PER_SECOND(st) get_const_us_per_second(st)
#define CONST_US_PER_MINUTE(st) get_const_us_per_minute(st)
#define CONST_US_PER_HOUR(st) get_const_us_per_hour(st)
#define CONST_US_PER_DAY(st) get_const_us_per_day(st)
#define CONST_US_PER_WEEK(st) get_const_us_per_week(st)
#define CONST_SEC_PER_DAY(st) get_const_sec_per_day(st)
#define CONST_UTC(st) ((PyObject *)&utc_timezone)
#define CONST_EPOCH(st) \
(st ? ((datetime_state *)st)->epoch \
: new_datetime(1970, 1, 1, 0, 0, 0, 0, (PyObject *)&utc_timezone, 0))

static datetime_state *
get_module_state(PyObject *module)
Expand Down Expand Up @@ -173,6 +208,7 @@ _get_current_state(PyObject **p_mod)
* so we must re-import the module. */
mod = PyImport_ImportModule("_datetime");
if (mod == NULL) {
PyErr_Clear();
return NULL;
}
}
Expand All @@ -184,7 +220,7 @@ _get_current_state(PyObject **p_mod)
#define GET_CURRENT_STATE(MOD_VAR) \
_get_current_state(&MOD_VAR)
#define RELEASE_CURRENT_STATE(ST_VAR, MOD_VAR) \
Py_DECREF(MOD_VAR)
Py_XDECREF(MOD_VAR)

static int
set_current_module(PyInterpreterState *interp, PyObject *mod)
Expand Down Expand Up @@ -3691,40 +3727,19 @@ static PyMethodDef iso_calendar_date_methods[] = {
{NULL, NULL},
};

static int
iso_calendar_date_traverse(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(Py_TYPE(self));
return PyTuple_Type.tp_traverse(self, visit, arg);
}

static void
iso_calendar_date_dealloc(PyObject *self)
{
PyTypeObject *tp = Py_TYPE(self);
PyTuple_Type.tp_dealloc(self); // delegate GC-untrack as well
Py_DECREF(tp);
}

static PyType_Slot isocal_slots[] = {
{Py_tp_repr, iso_calendar_date_repr},
{Py_tp_doc, (void *)iso_calendar_date__doc__},
{Py_tp_methods, iso_calendar_date_methods},
{Py_tp_getset, iso_calendar_date_getset},
{Py_tp_new, iso_calendar_date_new},
{Py_tp_dealloc, iso_calendar_date_dealloc},
{Py_tp_traverse, iso_calendar_date_traverse},
{0, NULL},
static PyTypeObject PyDateTime_IsoCalendarDateType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "datetime.IsoCalendarDate",
.tp_basicsize = sizeof(PyDateTime_IsoCalendarDate),
.tp_repr = (reprfunc) iso_calendar_date_repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = iso_calendar_date__doc__,
.tp_methods = iso_calendar_date_methods,
.tp_getset = iso_calendar_date_getset,
// .tp_base = &PyTuple_Type, // filled in PyInit__datetime
.tp_new = iso_calendar_date_new,
};

static PyType_Spec isocal_spec = {
.name = "datetime.IsoCalendarDate",
.basicsize = sizeof(PyDateTime_IsoCalendarDate),
.flags = (Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_IMMUTABLETYPE),
.slots = isocal_slots,
};

/*[clinic input]
@classmethod
Expand Down Expand Up @@ -3773,12 +3788,8 @@ date_isocalendar(PyObject *self, PyObject *Py_UNUSED(dummy))
week = 0;
}

PyObject *current_mod = NULL;
datetime_state *st = GET_CURRENT_STATE(current_mod);

PyObject *v = iso_calendar_date_new_impl(ISOCALENDAR_DATE_TYPE(st),
PyObject *v = iso_calendar_date_new_impl(&PyDateTime_IsoCalendarDateType,
year, week + 1, day + 1);
RELEASE_CURRENT_STATE(st, current_mod);
if (v == NULL) {
return NULL;
}
Expand Down Expand Up @@ -7155,6 +7166,8 @@ static PyTypeObject * const capi_types[] = {
&PyDateTime_TZInfoType,
/* Indirectly, via the utc object. */
&PyDateTime_TimeZoneType,
/* Not exposed */
&PyDateTime_IsoCalendarDateType,
};

/* The C-API is process-global. This violates interpreter isolation
Expand Down Expand Up @@ -7214,25 +7227,10 @@ create_timezone_from_delta(int days, int sec, int ms, int normalize)
static int
init_state(datetime_state *st, PyObject *module, PyObject *old_module)
{
/* Each module gets its own heap types. */
#define ADD_TYPE(FIELD, SPEC, BASE) \
do { \
PyObject *cls = PyType_FromModuleAndSpec( \
module, SPEC, (PyObject *)BASE); \
if (cls == NULL) { \
return -1; \
} \
st->FIELD = (PyTypeObject *)cls; \
} while (0)

ADD_TYPE(isocalendar_date_type, &isocal_spec, &PyTuple_Type);
#undef ADD_TYPE

if (old_module != NULL) {
assert(old_module != module);
datetime_state *st_old = get_module_state(old_module);
*st = (datetime_state){
.isocalendar_date_type = st->isocalendar_date_type,
.us_per_ms = Py_NewRef(st_old->us_per_ms),
.us_per_second = Py_NewRef(st_old->us_per_second),
.us_per_minute = Py_NewRef(st_old->us_per_minute),
Expand All @@ -7245,42 +7243,41 @@ init_state(datetime_state *st, PyObject *module, PyObject *old_module)
return 0;
}

st->us_per_ms = PyLong_FromLong(1000);
st->us_per_ms = CONST_US_PER_MS(NULL);
if (st->us_per_ms == NULL) {
return -1;
}
st->us_per_second = PyLong_FromLong(1000000);
st->us_per_second = CONST_US_PER_SECOND(NULL);
if (st->us_per_second == NULL) {
return -1;
}
st->us_per_minute = PyLong_FromLong(60000000);
st->us_per_minute = CONST_US_PER_MINUTE(NULL);
if (st->us_per_minute == NULL) {
return -1;
}
st->seconds_per_day = PyLong_FromLong(24 * 3600);
st->seconds_per_day = CONST_SEC_PER_DAY(NULL);
if (st->seconds_per_day == NULL) {
return -1;
}

/* The rest are too big for 32-bit ints, but even
* us_per_week fits in 40 bits, so doubles should be exact.
*/
st->us_per_hour = PyLong_FromDouble(3600000000.0);
st->us_per_hour = CONST_US_PER_HOUR(NULL);
if (st->us_per_hour == NULL) {
return -1;
}
st->us_per_day = PyLong_FromDouble(86400000000.0);
st->us_per_day = CONST_US_PER_DAY(NULL);
if (st->us_per_day == NULL) {
return -1;
}
st->us_per_week = PyLong_FromDouble(604800000000.0);
st->us_per_week = CONST_US_PER_WEEK(NULL);
if (st->us_per_week == NULL) {
return -1;
}

/* Init Unix epoch */
st->epoch = new_datetime(
1970, 1, 1, 0, 0, 0, 0, (PyObject *)&utc_timezone, 0);
st->epoch = CONST_EPOCH(NULL);
if (st->epoch == NULL) {
return -1;
}
Expand All @@ -7291,16 +7288,12 @@ init_state(datetime_state *st, PyObject *module, PyObject *old_module)
static int
traverse_state(datetime_state *st, visitproc visit, void *arg)
{
/* heap types */
Py_VISIT(st->isocalendar_date_type);

return 0;
}

static int
clear_state(datetime_state *st)
{
Py_CLEAR(st->isocalendar_date_type);
Py_CLEAR(st->us_per_ms);
Py_CLEAR(st->us_per_second);
Py_CLEAR(st->us_per_minute);
Expand All @@ -7323,6 +7316,7 @@ init_static_types(PyInterpreterState *interp, int reloading)
// `&...` is not a constant expression according to a strict reading
// of C standards. Fill tp_base at run-time rather than statically.
// See https://bugs.python.org/issue40777
PyDateTime_IsoCalendarDateType.tp_base = &PyTuple_Type;
PyDateTime_TimeZoneType.tp_base = &PyDateTime_TZInfoType;
PyDateTime_DateTimeType.tp_base = &PyDateTime_DateType;

Expand Down Expand Up @@ -7369,6 +7363,9 @@ _datetime_exec(PyObject *module)

for (size_t i = 0; i < Py_ARRAY_LENGTH(capi_types); i++) {
PyTypeObject *type = capi_types[i];
if (type == &PyDateTime_IsoCalendarDateType) {
continue;
}
const char *name = _PyType_Name(type);
assert(name != NULL);
if (PyModule_AddObjectRef(module, name, (PyObject *)type) < 0) {
Expand Down
Loading
Loading