Skip to content

Commit 0dbb320

Browse files
Merge branch 'main' into isolate-import-state
2 parents bdd6358 + b365d88 commit 0dbb320

File tree

5 files changed

+212
-4
lines changed

5 files changed

+212
-4
lines changed

Include/internal/pycore_import.h

+3
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ PyAPI_DATA(const struct _frozen *) _PyImport_FrozenStdlib;
161161
PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest;
162162
extern const struct _module_alias * _PyImport_FrozenAliases;
163163

164+
// for testing
165+
PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename);
166+
164167
#ifdef __cplusplus
165168
}
166169
#endif

Lib/test/test_imp.py

+73
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
from test.support import os_helper
1111
from test.support import script_helper
1212
from test.support import warnings_helper
13+
import textwrap
1314
import unittest
1415
import warnings
1516
imp = warnings_helper.import_deprecated('imp')
1617
import _imp
18+
import _testinternalcapi
19+
import _xxsubinterpreters as _interpreters
1720

1821

1922
OS_PATH_NAME = os.path.__name__
@@ -251,6 +254,71 @@ def test_issue16421_multiple_modules_in_one_dll(self):
251254
with self.assertRaises(ImportError):
252255
imp.load_dynamic('nonexistent', pathname)
253256

257+
@requires_load_dynamic
258+
def test_singlephase_multiple_interpreters(self):
259+
# Currently, for every single-phrase init module loaded
260+
# in multiple interpreters, those interpreters share a
261+
# PyModuleDef for that object, which can be a problem.
262+
263+
# This single-phase module has global state, which is shared
264+
# by the interpreters.
265+
import _testsinglephase
266+
name = _testsinglephase.__name__
267+
filename = _testsinglephase.__file__
268+
269+
del sys.modules[name]
270+
_testsinglephase._clear_globals()
271+
_testinternalcapi.clear_extension(name, filename)
272+
init_count = _testsinglephase.initialized_count()
273+
assert init_count == -1, (init_count,)
274+
275+
def clean_up():
276+
_testsinglephase._clear_globals()
277+
_testinternalcapi.clear_extension(name, filename)
278+
self.addCleanup(clean_up)
279+
280+
interp1 = _interpreters.create(isolated=False)
281+
self.addCleanup(_interpreters.destroy, interp1)
282+
interp2 = _interpreters.create(isolated=False)
283+
self.addCleanup(_interpreters.destroy, interp2)
284+
285+
script = textwrap.dedent(f'''
286+
import _testsinglephase
287+
288+
expected = %d
289+
init_count = _testsinglephase.initialized_count()
290+
if init_count != expected:
291+
raise Exception(init_count)
292+
293+
lookedup = _testsinglephase.look_up_self()
294+
if lookedup is not _testsinglephase:
295+
raise Exception((_testsinglephase, lookedup))
296+
297+
# Attrs set in the module init func are in m_copy.
298+
_initialized = _testsinglephase._initialized
299+
initialized = _testsinglephase.initialized()
300+
if _initialized != initialized:
301+
raise Exception((_initialized, initialized))
302+
303+
# Attrs set after loading are not in m_copy.
304+
if hasattr(_testsinglephase, 'spam'):
305+
raise Exception(_testsinglephase.spam)
306+
_testsinglephase.spam = expected
307+
''')
308+
309+
# Use an interpreter that gets destroyed right away.
310+
ret = support.run_in_subinterp(script % 1)
311+
self.assertEqual(ret, 0)
312+
313+
# The module's init func gets run again.
314+
# The module's globals did not get destroyed.
315+
_interpreters.run_string(interp1, script % 2)
316+
317+
# The module's init func is not run again.
318+
# The second interpreter copies the module's m_copy.
319+
# However, globals are still shared.
320+
_interpreters.run_string(interp2, script % 2)
321+
254322
@requires_load_dynamic
255323
def test_singlephase_variants(self):
256324
'''Exercise the most meaningful variants described in Python/import.c.'''
@@ -260,6 +328,11 @@ def test_singlephase_variants(self):
260328
fileobj, pathname, _ = imp.find_module(basename)
261329
fileobj.close()
262330

331+
def clean_up():
332+
import _testsinglephase
333+
_testsinglephase._clear_globals()
334+
self.addCleanup(clean_up)
335+
263336
modules = {}
264337
def load(name):
265338
assert name not in modules

Modules/_testinternalcapi.c

+15
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,20 @@ get_interp_settings(PyObject *self, PyObject *args)
671671
}
672672

673673

674+
static PyObject *
675+
clear_extension(PyObject *self, PyObject *args)
676+
{
677+
PyObject *name = NULL, *filename = NULL;
678+
if (!PyArg_ParseTuple(args, "OO:clear_extension", &name, &filename)) {
679+
return NULL;
680+
}
681+
if (_PyImport_ClearExtension(name, filename) < 0) {
682+
return NULL;
683+
}
684+
Py_RETURN_NONE;
685+
}
686+
687+
674688
static PyMethodDef module_functions[] = {
675689
{"get_configs", get_configs, METH_NOARGS},
676690
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -692,6 +706,7 @@ static PyMethodDef module_functions[] = {
692706
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
693707
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
694708
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
709+
{"clear_extension", clear_extension, METH_VARARGS, NULL},
695710
{NULL, NULL} /* sentinel */
696711
};
697712

Modules/_testsinglephase.c

+47-2
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,27 @@ typedef struct {
1717
PyObject *str_const;
1818
} module_state;
1919

20+
2021
/* Process-global state is only used by _testsinglephase
2122
since it's the only one that does not support re-init. */
2223
static struct {
2324
int initialized_count;
2425
module_state module;
25-
} global_state = { .initialized_count = -1 };
26+
} global_state = {
27+
28+
#define NOT_INITIALIZED -1
29+
.initialized_count = NOT_INITIALIZED,
30+
};
31+
32+
static void clear_state(module_state *state);
33+
34+
static void
35+
clear_global_state(void)
36+
{
37+
clear_state(&global_state.module);
38+
global_state.initialized_count = NOT_INITIALIZED;
39+
}
40+
2641

2742
static inline module_state *
2843
get_module_state(PyObject *module)
@@ -106,6 +121,7 @@ init_state(module_state *state)
106121
return -1;
107122
}
108123

124+
109125
static int
110126
init_module(PyObject *module, module_state *state)
111127
{
@@ -118,6 +134,16 @@ init_module(PyObject *module, module_state *state)
118134
if (PyModule_AddObjectRef(module, "str_const", state->str_const) != 0) {
119135
return -1;
120136
}
137+
138+
double d = _PyTime_AsSecondsDouble(state->initialized);
139+
PyObject *initialized = PyFloat_FromDouble(d);
140+
if (initialized == NULL) {
141+
return -1;
142+
}
143+
if (PyModule_AddObjectRef(module, "_initialized", initialized) != 0) {
144+
return -1;
145+
}
146+
121147
return 0;
122148
}
123149

@@ -198,10 +224,28 @@ basic_initialized_count(PyObject *self, PyObject *Py_UNUSED(ignored))
198224
}
199225

200226
#define INITIALIZED_COUNT_METHODDEF \
201-
{"initialized_count", basic_initialized_count, METH_VARARGS, \
227+
{"initialized_count", basic_initialized_count, METH_NOARGS, \
202228
basic_initialized_count_doc}
203229

204230

231+
PyDoc_STRVAR(basic__clear_globals_doc,
232+
"_clear_globals()\n\
233+
\n\
234+
Free all global state and set it to uninitialized.");
235+
236+
static PyObject *
237+
basic__clear_globals(PyObject *self, PyObject *Py_UNUSED(ignored))
238+
{
239+
assert(PyModule_GetDef(self)->m_size == -1);
240+
clear_global_state();
241+
Py_RETURN_NONE;
242+
}
243+
244+
#define _CLEAR_GLOBALS_METHODDEF \
245+
{"_clear_globals", basic__clear_globals, METH_NOARGS, \
246+
basic__clear_globals_doc}
247+
248+
205249
/*********************************************/
206250
/* the _testsinglephase module (and aliases) */
207251
/*********************************************/
@@ -223,6 +267,7 @@ static PyMethodDef TestMethods_Basic[] = {
223267
SUM_METHODDEF,
224268
INITIALIZED_METHODDEF,
225269
INITIALIZED_COUNT_METHODDEF,
270+
_CLEAR_GLOBALS_METHODDEF,
226271
{NULL, NULL} /* sentinel */
227272
};
228273

Python/import.c

+74-2
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,28 @@ exec_builtin_or_dynamic(PyObject *mod) {
637637
}
638638

639639

640+
static int clear_singlephase_extension(PyInterpreterState *interp,
641+
PyObject *name, PyObject *filename);
642+
643+
// Currently, this is only used for testing.
644+
// (See _testinternalcapi.clear_extension().)
645+
int
646+
_PyImport_ClearExtension(PyObject *name, PyObject *filename)
647+
{
648+
PyInterpreterState *interp = _PyInterpreterState_GET();
649+
650+
/* Clearing a module's C globals is up to the module. */
651+
if (clear_singlephase_extension(interp, name, filename) < 0) {
652+
return -1;
653+
}
654+
655+
// In the future we'll probably also make sure the extension's
656+
// file handle (and DL handle) is closed (requires saving it).
657+
658+
return 0;
659+
}
660+
661+
640662
/*******************/
641663

642664
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
@@ -771,8 +793,30 @@ _extensions_cache_set(PyObject *filename, PyObject *name, PyModuleDef *def)
771793
return 0;
772794
}
773795

796+
static int
797+
_extensions_cache_delete(PyObject *filename, PyObject *name)
798+
{
799+
PyObject *extensions = EXTENSIONS;
800+
if (extensions == NULL) {
801+
return 0;
802+
}
803+
PyObject *key = PyTuple_Pack(2, filename, name);
804+
if (key == NULL) {
805+
return -1;
806+
}
807+
if (PyDict_DelItem(extensions, key) < 0) {
808+
if (!PyErr_ExceptionMatches(PyExc_KeyError)) {
809+
Py_DECREF(key);
810+
return -1;
811+
}
812+
PyErr_Clear();
813+
}
814+
Py_DECREF(key);
815+
return 0;
816+
}
817+
774818
static void
775-
_extensions_cache_clear(void)
819+
_extensions_cache_clear_all(void)
776820
{
777821
Py_CLEAR(EXTENSIONS);
778822
}
@@ -895,6 +939,34 @@ import_find_extension(PyThreadState *tstate, PyObject *name,
895939
return mod;
896940
}
897941

942+
static int
943+
clear_singlephase_extension(PyInterpreterState *interp,
944+
PyObject *name, PyObject *filename)
945+
{
946+
PyModuleDef *def = _extensions_cache_get(filename, name);
947+
if (def == NULL) {
948+
if (PyErr_Occurred()) {
949+
return -1;
950+
}
951+
return 0;
952+
}
953+
954+
/* Clear data set when the module was initially loaded. */
955+
def->m_base.m_init = NULL;
956+
Py_CLEAR(def->m_base.m_copy);
957+
// We leave m_index alone since there's no reason to reset it.
958+
959+
/* Clear the PyState_*Module() cache entry. */
960+
if (_modules_by_index_check(interp, def->m_base.m_index) == NULL) {
961+
if (_modules_by_index_clear(interp, def) < 0) {
962+
return -1;
963+
}
964+
}
965+
966+
/* Clear the cached module def. */
967+
return _extensions_cache_delete(filename, name);
968+
}
969+
898970

899971
/*******************/
900972
/* builtin modules */
@@ -2638,7 +2710,7 @@ void
26382710
_PyImport_Fini(void)
26392711
{
26402712
/* Destroy the database used by _PyImport_{Fixup,Find}Extension */
2641-
_extensions_cache_clear();
2713+
_extensions_cache_clear_all();
26422714

26432715
/* Use the same memory allocator as _PyImport_Init(). */
26442716
PyMemAllocatorEx old_alloc;

0 commit comments

Comments
 (0)