mirror of https://github.com/python/cpython
gh-101758: Add a Test For Single-Phase Init Modules in Multiple Interpreters (gh-101920)
The test verifies the behavior of single-phase init modules when loaded in multiple interpreters. https://github.com/python/cpython/issues/101758
This commit is contained in:
parent
b2fc549278
commit
b365d88465
|
@ -153,6 +153,9 @@ PyAPI_DATA(const struct _frozen *) _PyImport_FrozenStdlib;
|
||||||
PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest;
|
PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest;
|
||||||
extern const struct _module_alias * _PyImport_FrozenAliases;
|
extern const struct _module_alias * _PyImport_FrozenAliases;
|
||||||
|
|
||||||
|
// for testing
|
||||||
|
PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -10,10 +10,13 @@ from test.support import import_helper
|
||||||
from test.support import os_helper
|
from test.support import os_helper
|
||||||
from test.support import script_helper
|
from test.support import script_helper
|
||||||
from test.support import warnings_helper
|
from test.support import warnings_helper
|
||||||
|
import textwrap
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
imp = warnings_helper.import_deprecated('imp')
|
imp = warnings_helper.import_deprecated('imp')
|
||||||
import _imp
|
import _imp
|
||||||
|
import _testinternalcapi
|
||||||
|
import _xxsubinterpreters as _interpreters
|
||||||
|
|
||||||
|
|
||||||
OS_PATH_NAME = os.path.__name__
|
OS_PATH_NAME = os.path.__name__
|
||||||
|
@ -251,6 +254,71 @@ class ImportTests(unittest.TestCase):
|
||||||
with self.assertRaises(ImportError):
|
with self.assertRaises(ImportError):
|
||||||
imp.load_dynamic('nonexistent', pathname)
|
imp.load_dynamic('nonexistent', pathname)
|
||||||
|
|
||||||
|
@requires_load_dynamic
|
||||||
|
def test_singlephase_multiple_interpreters(self):
|
||||||
|
# Currently, for every single-phrase init module loaded
|
||||||
|
# in multiple interpreters, those interpreters share a
|
||||||
|
# PyModuleDef for that object, which can be a problem.
|
||||||
|
|
||||||
|
# This single-phase module has global state, which is shared
|
||||||
|
# by the interpreters.
|
||||||
|
import _testsinglephase
|
||||||
|
name = _testsinglephase.__name__
|
||||||
|
filename = _testsinglephase.__file__
|
||||||
|
|
||||||
|
del sys.modules[name]
|
||||||
|
_testsinglephase._clear_globals()
|
||||||
|
_testinternalcapi.clear_extension(name, filename)
|
||||||
|
init_count = _testsinglephase.initialized_count()
|
||||||
|
assert init_count == -1, (init_count,)
|
||||||
|
|
||||||
|
def clean_up():
|
||||||
|
_testsinglephase._clear_globals()
|
||||||
|
_testinternalcapi.clear_extension(name, filename)
|
||||||
|
self.addCleanup(clean_up)
|
||||||
|
|
||||||
|
interp1 = _interpreters.create(isolated=False)
|
||||||
|
self.addCleanup(_interpreters.destroy, interp1)
|
||||||
|
interp2 = _interpreters.create(isolated=False)
|
||||||
|
self.addCleanup(_interpreters.destroy, interp2)
|
||||||
|
|
||||||
|
script = textwrap.dedent(f'''
|
||||||
|
import _testsinglephase
|
||||||
|
|
||||||
|
expected = %d
|
||||||
|
init_count = _testsinglephase.initialized_count()
|
||||||
|
if init_count != expected:
|
||||||
|
raise Exception(init_count)
|
||||||
|
|
||||||
|
lookedup = _testsinglephase.look_up_self()
|
||||||
|
if lookedup is not _testsinglephase:
|
||||||
|
raise Exception((_testsinglephase, lookedup))
|
||||||
|
|
||||||
|
# Attrs set in the module init func are in m_copy.
|
||||||
|
_initialized = _testsinglephase._initialized
|
||||||
|
initialized = _testsinglephase.initialized()
|
||||||
|
if _initialized != initialized:
|
||||||
|
raise Exception((_initialized, initialized))
|
||||||
|
|
||||||
|
# Attrs set after loading are not in m_copy.
|
||||||
|
if hasattr(_testsinglephase, 'spam'):
|
||||||
|
raise Exception(_testsinglephase.spam)
|
||||||
|
_testsinglephase.spam = expected
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Use an interpreter that gets destroyed right away.
|
||||||
|
ret = support.run_in_subinterp(script % 1)
|
||||||
|
self.assertEqual(ret, 0)
|
||||||
|
|
||||||
|
# The module's init func gets run again.
|
||||||
|
# The module's globals did not get destroyed.
|
||||||
|
_interpreters.run_string(interp1, script % 2)
|
||||||
|
|
||||||
|
# The module's init func is not run again.
|
||||||
|
# The second interpreter copies the module's m_copy.
|
||||||
|
# However, globals are still shared.
|
||||||
|
_interpreters.run_string(interp2, script % 2)
|
||||||
|
|
||||||
@requires_load_dynamic
|
@requires_load_dynamic
|
||||||
def test_singlephase_variants(self):
|
def test_singlephase_variants(self):
|
||||||
'''Exercise the most meaningful variants described in Python/import.c.'''
|
'''Exercise the most meaningful variants described in Python/import.c.'''
|
||||||
|
@ -260,6 +328,11 @@ class ImportTests(unittest.TestCase):
|
||||||
fileobj, pathname, _ = imp.find_module(basename)
|
fileobj, pathname, _ = imp.find_module(basename)
|
||||||
fileobj.close()
|
fileobj.close()
|
||||||
|
|
||||||
|
def clean_up():
|
||||||
|
import _testsinglephase
|
||||||
|
_testsinglephase._clear_globals()
|
||||||
|
self.addCleanup(clean_up)
|
||||||
|
|
||||||
modules = {}
|
modules = {}
|
||||||
def load(name):
|
def load(name):
|
||||||
assert name not in modules
|
assert name not in modules
|
||||||
|
|
|
@ -671,6 +671,20 @@ get_interp_settings(PyObject *self, PyObject *args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
clear_extension(PyObject *self, PyObject *args)
|
||||||
|
{
|
||||||
|
PyObject *name = NULL, *filename = NULL;
|
||||||
|
if (!PyArg_ParseTuple(args, "OO:clear_extension", &name, &filename)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (_PyImport_ClearExtension(name, filename) < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static PyMethodDef module_functions[] = {
|
static PyMethodDef module_functions[] = {
|
||||||
{"get_configs", get_configs, METH_NOARGS},
|
{"get_configs", get_configs, METH_NOARGS},
|
||||||
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
|
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
|
||||||
|
@ -692,6 +706,7 @@ static PyMethodDef module_functions[] = {
|
||||||
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
|
_TESTINTERNALCAPI_COMPILER_CODEGEN_METHODDEF
|
||||||
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
|
_TESTINTERNALCAPI_OPTIMIZE_CFG_METHODDEF
|
||||||
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
|
{"get_interp_settings", get_interp_settings, METH_VARARGS, NULL},
|
||||||
|
{"clear_extension", clear_extension, METH_VARARGS, NULL},
|
||||||
{NULL, NULL} /* sentinel */
|
{NULL, NULL} /* sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,27 @@ typedef struct {
|
||||||
PyObject *str_const;
|
PyObject *str_const;
|
||||||
} module_state;
|
} module_state;
|
||||||
|
|
||||||
|
|
||||||
/* Process-global state is only used by _testsinglephase
|
/* Process-global state is only used by _testsinglephase
|
||||||
since it's the only one that does not support re-init. */
|
since it's the only one that does not support re-init. */
|
||||||
static struct {
|
static struct {
|
||||||
int initialized_count;
|
int initialized_count;
|
||||||
module_state module;
|
module_state module;
|
||||||
} global_state = { .initialized_count = -1 };
|
} global_state = {
|
||||||
|
|
||||||
|
#define NOT_INITIALIZED -1
|
||||||
|
.initialized_count = NOT_INITIALIZED,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void clear_state(module_state *state);
|
||||||
|
|
||||||
|
static void
|
||||||
|
clear_global_state(void)
|
||||||
|
{
|
||||||
|
clear_state(&global_state.module);
|
||||||
|
global_state.initialized_count = NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static inline module_state *
|
static inline module_state *
|
||||||
get_module_state(PyObject *module)
|
get_module_state(PyObject *module)
|
||||||
|
@ -106,6 +121,7 @@ error:
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
init_module(PyObject *module, module_state *state)
|
init_module(PyObject *module, module_state *state)
|
||||||
{
|
{
|
||||||
|
@ -118,6 +134,16 @@ init_module(PyObject *module, module_state *state)
|
||||||
if (PyModule_AddObjectRef(module, "str_const", state->str_const) != 0) {
|
if (PyModule_AddObjectRef(module, "str_const", state->str_const) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double d = _PyTime_AsSecondsDouble(state->initialized);
|
||||||
|
PyObject *initialized = PyFloat_FromDouble(d);
|
||||||
|
if (initialized == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (PyModule_AddObjectRef(module, "_initialized", initialized) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,10 +224,28 @@ basic_initialized_count(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||||
}
|
}
|
||||||
|
|
||||||
#define INITIALIZED_COUNT_METHODDEF \
|
#define INITIALIZED_COUNT_METHODDEF \
|
||||||
{"initialized_count", basic_initialized_count, METH_VARARGS, \
|
{"initialized_count", basic_initialized_count, METH_NOARGS, \
|
||||||
basic_initialized_count_doc}
|
basic_initialized_count_doc}
|
||||||
|
|
||||||
|
|
||||||
|
PyDoc_STRVAR(basic__clear_globals_doc,
|
||||||
|
"_clear_globals()\n\
|
||||||
|
\n\
|
||||||
|
Free all global state and set it to uninitialized.");
|
||||||
|
|
||||||
|
static PyObject *
|
||||||
|
basic__clear_globals(PyObject *self, PyObject *Py_UNUSED(ignored))
|
||||||
|
{
|
||||||
|
assert(PyModule_GetDef(self)->m_size == -1);
|
||||||
|
clear_global_state();
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define _CLEAR_GLOBALS_METHODDEF \
|
||||||
|
{"_clear_globals", basic__clear_globals, METH_NOARGS, \
|
||||||
|
basic__clear_globals_doc}
|
||||||
|
|
||||||
|
|
||||||
/*********************************************/
|
/*********************************************/
|
||||||
/* the _testsinglephase module (and aliases) */
|
/* the _testsinglephase module (and aliases) */
|
||||||
/*********************************************/
|
/*********************************************/
|
||||||
|
@ -223,6 +267,7 @@ static PyMethodDef TestMethods_Basic[] = {
|
||||||
SUM_METHODDEF,
|
SUM_METHODDEF,
|
||||||
INITIALIZED_METHODDEF,
|
INITIALIZED_METHODDEF,
|
||||||
INITIALIZED_COUNT_METHODDEF,
|
INITIALIZED_COUNT_METHODDEF,
|
||||||
|
_CLEAR_GLOBALS_METHODDEF,
|
||||||
{NULL, NULL} /* sentinel */
|
{NULL, NULL} /* sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -632,6 +632,28 @@ exec_builtin_or_dynamic(PyObject *mod) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int clear_singlephase_extension(PyInterpreterState *interp,
|
||||||
|
PyObject *name, PyObject *filename);
|
||||||
|
|
||||||
|
// Currently, this is only used for testing.
|
||||||
|
// (See _testinternalcapi.clear_extension().)
|
||||||
|
int
|
||||||
|
_PyImport_ClearExtension(PyObject *name, PyObject *filename)
|
||||||
|
{
|
||||||
|
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||||||
|
|
||||||
|
/* Clearing a module's C globals is up to the module. */
|
||||||
|
if (clear_singlephase_extension(interp, name, filename) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the future we'll probably also make sure the extension's
|
||||||
|
// file handle (and DL handle) is closed (requires saving it).
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************/
|
/*******************/
|
||||||
|
|
||||||
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
|
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
|
||||||
|
@ -766,8 +788,30 @@ _extensions_cache_set(PyObject *filename, PyObject *name, PyModuleDef *def)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
_extensions_cache_delete(PyObject *filename, PyObject *name)
|
||||||
|
{
|
||||||
|
PyObject *extensions = EXTENSIONS;
|
||||||
|
if (extensions == NULL) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
PyObject *key = PyTuple_Pack(2, filename, name);
|
||||||
|
if (key == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (PyDict_DelItem(extensions, key) < 0) {
|
||||||
|
if (!PyErr_ExceptionMatches(PyExc_KeyError)) {
|
||||||
|
Py_DECREF(key);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
PyErr_Clear();
|
||||||
|
}
|
||||||
|
Py_DECREF(key);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
_extensions_cache_clear(void)
|
_extensions_cache_clear_all(void)
|
||||||
{
|
{
|
||||||
Py_CLEAR(EXTENSIONS);
|
Py_CLEAR(EXTENSIONS);
|
||||||
}
|
}
|
||||||
|
@ -890,6 +934,34 @@ import_find_extension(PyThreadState *tstate, PyObject *name,
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
clear_singlephase_extension(PyInterpreterState *interp,
|
||||||
|
PyObject *name, PyObject *filename)
|
||||||
|
{
|
||||||
|
PyModuleDef *def = _extensions_cache_get(filename, name);
|
||||||
|
if (def == NULL) {
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear data set when the module was initially loaded. */
|
||||||
|
def->m_base.m_init = NULL;
|
||||||
|
Py_CLEAR(def->m_base.m_copy);
|
||||||
|
// We leave m_index alone since there's no reason to reset it.
|
||||||
|
|
||||||
|
/* Clear the PyState_*Module() cache entry. */
|
||||||
|
if (_modules_by_index_check(interp, def->m_base.m_index) == NULL) {
|
||||||
|
if (_modules_by_index_clear(interp, def) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clear the cached module def. */
|
||||||
|
return _extensions_cache_delete(filename, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************/
|
/*******************/
|
||||||
/* builtin modules */
|
/* builtin modules */
|
||||||
|
@ -2633,7 +2705,7 @@ void
|
||||||
_PyImport_Fini(void)
|
_PyImport_Fini(void)
|
||||||
{
|
{
|
||||||
/* Destroy the database used by _PyImport_{Fixup,Find}Extension */
|
/* Destroy the database used by _PyImport_{Fixup,Find}Extension */
|
||||||
_extensions_cache_clear();
|
_extensions_cache_clear_all();
|
||||||
if (import_lock != NULL) {
|
if (import_lock != NULL) {
|
||||||
PyThread_free_lock(import_lock);
|
PyThread_free_lock(import_lock);
|
||||||
import_lock = NULL;
|
import_lock = NULL;
|
||||||
|
|
Loading…
Reference in New Issue