Skip to content

Commit

Permalink
pythongh-123880: Allow recursive import of single-phase-init modules (p…
Browse files Browse the repository at this point in the history
…ythonGH-123950)


Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com>
Co-authored-by: Brett Cannon <brett@python.org>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent 3e36e5a commit aee219f
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 25 deletions.
69 changes: 51 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,37 @@ def test_absolute_circular_submodule(self):
str(cm.exception),
)

@requires_singlephase_init
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
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 +1982,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 +2198,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 +2213,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
13 changes: 13 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,13 @@
"""Circular import involving a single-phase-init extension.
This module is imported from the _testsinglephase_circular module from
_testsinglephase, and imports that module again.
"""

import importlib
import _testsinglephase
from test.test_import import import_extension_from_file

name = '_testsinglephase_circular'
filename = _testsinglephase.__file__
mod = import_extension_from_file(name, filename)
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

0 comments on commit aee219f

Please sign in to comment.