Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-123880: Allow recursive import of single-phase-init modules #123950

Merged
merged 13 commits into from
Sep 20, 2024
67 changes: 49 additions & 18 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,24 @@ def require_frozen(module, *, skip=True):
def require_pure_python(module, *, skip=False):
_require_loader(module, SourceFileLoader, skip)

def create_extension_loader(modname, filename):
# Apple extensions must be distributed as frameworks. This requires
# a specialist loader.
if is_apple_mobile:
return AppleFrameworkLoader(modname, filename)
else:
return ExtensionFileLoader(modname, filename)

def import_extension_from_file(modname, filename, *, put_in_sys_modules=True):
loader = create_extension_loader(modname, filename)
spec = importlib.util.spec_from_loader(modname, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
if put_in_sys_modules:
sys.modules[modname] = module
return module


def remove_files(name):
for f in (name + ".py",
name + ".pyc",
Expand Down Expand Up @@ -1894,6 +1912,35 @@ def test_absolute_circular_submodule(self):
str(cm.exception),
)

def test_singlephase_circular(self):
"""Regression test for gh-123950

Import a single-phase-init module that imports itself
from the PyInit_* function (before it's added to sys.modules).
Manages its own cache (which is `static`, and so incompatible
with multiple interpreters or interpreter reset).
"""
name = '_testsinglephase_circular'
helper_name = 'test.test_import.data.circular_imports.singlephase'
with uncache(name, helper_name):
filename = _testsinglephase.__file__
# We don't put the module in sys.modules: that the *inner*
# import should do that.
mod = import_extension_from_file(name, filename,
put_in_sys_modules=False)

self.assertEqual(mod.helper_mod_name, helper_name)
self.assertIn(name, sys.modules)
self.assertIn(helper_name, sys.modules)

self.assertIn(name, sys.modules)
self.assertIn(helper_name, sys.modules)
self.assertNotIn(name, sys.modules)
self.assertNotIn(helper_name, sys.modules)
self.assertIs(mod.clear_static_var(), mod)
_testinternalcapi.clear_extension('_testsinglephase_circular',
mod.__spec__.origin)

def test_unwritable_module(self):
self.addCleanup(unload, "test.test_import.data.unwritable")
self.addCleanup(unload, "test.test_import.data.unwritable.x")
Expand Down Expand Up @@ -1933,14 +1980,6 @@ def pipe(self):
os.set_blocking(r, False)
return (r, w)

def create_extension_loader(self, modname, filename):
# Apple extensions must be distributed as frameworks. This requires
# a specialist loader.
if is_apple_mobile:
return AppleFrameworkLoader(modname, filename)
else:
return ExtensionFileLoader(modname, filename)

def import_script(self, name, fd, filename=None, check_override=None):
override_text = ''
if check_override is not None:
Expand Down Expand Up @@ -2157,11 +2196,7 @@ def test_multi_init_extension_compat(self):
def test_multi_init_extension_non_isolated_compat(self):
modname = '_test_non_isolated'
filename = _testmultiphase.__file__
loader = self.create_extension_loader(modname, filename)
spec = importlib.util.spec_from_loader(modname, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
sys.modules[modname] = module
module = import_extension_from_file(modname, filename)

require_extension(module)
with self.subTest(f'{modname}: isolated'):
Expand All @@ -2176,11 +2211,7 @@ def test_multi_init_extension_non_isolated_compat(self):
def test_multi_init_extension_per_interpreter_gil_compat(self):
modname = '_test_shared_gil_only'
filename = _testmultiphase.__file__
loader = self.create_extension_loader(modname, filename)
spec = importlib.util.spec_from_loader(modname, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
sys.modules[modname] = module
module = import_extension_from_file(modname, filename)

require_extension(module)
with self.subTest(f'{modname}: isolated, strict'):
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_import/data/circular_imports/singlephase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Cilcular import involving a single-phase-init extension.
encukou marked this conversation as resolved.
Show resolved Hide resolved

This module is imported from the _testsinglephase_circular module from
_testsinglephase, and imports that module again.
"""

import importlib
import _testsinglephase

name = '_testsinglephase_circular'
filename = _testsinglephase.__file__
loader = importlib.machinery.ExtensionFileLoader(name, filename)
spec = importlib.util.spec_from_file_location(name, filename, loader=loader)
mod = importlib._bootstrap._load(spec)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fixed a bug that prevented circular imports of extension modules that use
single-phase initialization.
63 changes: 61 additions & 2 deletions Modules/_testsinglephase.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

/* Testing module for single-phase initialization of extension modules

This file contains 8 distinct modules, meaning each as its own name
This file contains several distinct modules, meaning each as its own name
and its own init function (PyInit_...). The default import system will
only find the one matching the filename: _testsinglephase. To load the
others you must do so manually. For example:
Expand All @@ -12,9 +12,13 @@ filename = _testsinglephase.__file__
loader = importlib.machinery.ExtensionFileLoader(name, filename)
spec = importlib.util.spec_from_file_location(name, filename, loader=loader)
mod = importlib._bootstrap._load(spec)
loader.exec_module(module)
sys.modules[modname] = module
```

Here are the 8 modules:
(The last two lines are just for completeness.)

Here are the modules:

* _testsinglephase
* def: _testsinglephase_basic,
Expand Down Expand Up @@ -163,6 +167,11 @@ Here are the 8 modules:
* functions: none
* import system: same as _testsinglephase_with_state

* _testsinglephase_circular
Regression test for gh-123880.
Does not have the common attributes & methods.
See test_singlephase_circular test.test_import.SinglephaseInitTests.

Module state:

* fields
Expand Down Expand Up @@ -740,3 +749,53 @@ PyInit__testsinglephase_with_state_check_cache_first(void)
}
return PyModule_Create(&_testsinglephase_with_state_check_cache_first);
}


/****************************************/
/* the _testsinglephase_circular module */
/****************************************/

static PyObject *static_module_circular;

static PyObject *
circularmod_clear_static_var(PyObject *self, PyObject *arg)
{
PyObject *result = static_module_circular;
static_module_circular = NULL;
return result;
}

static struct PyModuleDef _testsinglephase_circular = {
PyModuleDef_HEAD_INIT,
.m_name = "_testsinglephase_circular",
.m_doc = PyDoc_STR("Test module _testsinglephase_circular"),
.m_methods = (PyMethodDef[]) {
{"clear_static_var", circularmod_clear_static_var, METH_NOARGS,
"Clear the static variable and return its previous value."},
{NULL, NULL} /* sentinel */
}
};

PyMODINIT_FUNC
PyInit__testsinglephase_circular(void)
{
if (!static_module_circular) {
static_module_circular = PyModule_Create(&_testsinglephase_circular);
if (!static_module_circular) {
return NULL;
}
}
static const char helper_mod_name[] = (
"test.test_import.data.circular_imports.singlephase");
PyObject *helper_mod = PyImport_ImportModule(helper_mod_name);
Py_XDECREF(helper_mod);
if (!helper_mod) {
return NULL;
}
if(PyModule_AddStringConstant(static_module_circular,
"helper_mod_name",
helper_mod_name) < 0) {
return NULL;
}
return Py_NewRef(static_module_circular);
}
18 changes: 13 additions & 5 deletions Python/import.c
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,8 @@ static int clear_singlephase_extension(PyInterpreterState *interp,

// Currently, this is only used for testing.
// (See _testinternalcapi.clear_extension().)
// If adding another use, be careful about modules that import themselves
// recursively (see gh-123880).
int
_PyImport_ClearExtension(PyObject *name, PyObject *filename)
{
Expand Down Expand Up @@ -1322,12 +1324,16 @@ _extensions_cache_set(PyObject *path, PyObject *name,
value = entry == NULL
? NULL
: (struct extensions_cache_value *)entry->value;
/* We should never be updating an existing cache value. */
assert(value == NULL);
if (value != NULL) {
PyErr_Format(PyExc_SystemError,
"extension module %R is already cached", name);
goto finally;
/* gh-123880: If there's an existing cache value, it means a module is
* being imported recursively from its PyInit_* or Py_mod_* function.
* (That function presumably handles returning a partially
* constructed module in such a case.)
* We can reuse the existing cache value; it is owned by the cache.
* (Entries get removed from it in exceptional circumstances,
* after interpreter shutdown, and in runtime shutdown.)
*/
goto finally_oldvalue;
}
newvalue = alloc_extensions_cache_value();
if (newvalue == NULL) {
Expand Down Expand Up @@ -1392,6 +1398,7 @@ _extensions_cache_set(PyObject *path, PyObject *name,
cleanup_old_cached_def(&olddefbase);
}

finally_oldvalue:
extensions_lock_release();
if (key != NULL) {
hashtable_destroy_str(key);
Expand Down Expand Up @@ -2128,6 +2135,7 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,
}


// Used in _PyImport_ClearExtension; see notes there.
static int
clear_singlephase_extension(PyInterpreterState *interp,
PyObject *name, PyObject *path)
Expand Down
1 change: 1 addition & 0 deletions Tools/c-analyzer/cpython/ignored.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ Modules/_testmultiphase.c - slots_nonmodule_with_exec_slots -
Modules/_testmultiphase.c - testexport_methods -
Modules/_testmultiphase.c - uninitialized_def -
Modules/_testsinglephase.c - global_state -
Modules/_testsinglephase.c - static_module_circular -
Modules/_xxtestfuzz/_xxtestfuzz.c - _fuzzmodule -
Modules/_xxtestfuzz/_xxtestfuzz.c - module_methods -
Modules/_xxtestfuzz/fuzzer.c - RE_FLAG_DEBUG -
Expand Down
Loading