summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPetr Viktorin <encukou@gmail.com>2024-09-20 08:27:34 (GMT)
committerGitHub <noreply@github.com>2024-09-20 08:27:34 (GMT)
commitaee219f4558dda619bd86e4b0e028ce47a5e4b77 (patch)
treee5dce2eb3fb98831d5367f556e2e8c83408b0617
parent3e36e5aef18e326f5d1081d73ee8d8fefa1d82f8 (diff)
downloadcpython-aee219f4558dda619bd86e4b0e028ce47a5e4b77.zip
cpython-aee219f4558dda619bd86e4b0e028ce47a5e4b77.tar.gz
cpython-aee219f4558dda619bd86e4b0e028ce47a5e4b77.tar.bz2
gh-123880: Allow recursive import of single-phase-init modules (GH-123950)
Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Co-authored-by: Brett Cannon <brett@python.org>
-rw-r--r--Lib/test/test_import/__init__.py69
-rw-r--r--Lib/test/test_import/data/circular_imports/singlephase.py13
-rw-r--r--Misc/NEWS.d/next/C_API/2024-09-12-16-16-24.gh-issue-123880.2-8vcj.rst2
-rw-r--r--Modules/_testsinglephase.c63
-rw-r--r--Python/import.c18
-rw-r--r--Tools/c-analyzer/cpython/ignored.tsv1
6 files changed, 141 insertions, 25 deletions
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index 3d89d69..5d0d024 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -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",
@@ -1894,6 +1912,37 @@ class CircularImportTests(unittest.TestCase):
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")
@@ -1933,14 +1982,6 @@ class SubinterpImportTests(unittest.TestCase):
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:
@@ -2157,11 +2198,7 @@ class SubinterpImportTests(unittest.TestCase):
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'):
@@ -2176,11 +2213,7 @@ class SubinterpImportTests(unittest.TestCase):
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'):
diff --git a/Lib/test/test_import/data/circular_imports/singlephase.py b/Lib/test/test_import/data/circular_imports/singlephase.py
new file mode 100644
index 0000000..05618bc
--- /dev/null
+++ b/Lib/test/test_import/data/circular_imports/singlephase.py
@@ -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)
diff --git a/Misc/NEWS.d/next/C_API/2024-09-12-16-16-24.gh-issue-123880.2-8vcj.rst b/Misc/NEWS.d/next/C_API/2024-09-12-16-16-24.gh-issue-123880.2-8vcj.rst
new file mode 100644
index 0000000..8a31c96
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2024-09-12-16-16-24.gh-issue-123880.2-8vcj.rst
@@ -0,0 +1,2 @@
+Fixed a bug that prevented circular imports of extension modules that use
+single-phase initialization.
diff --git a/Modules/_testsinglephase.c b/Modules/_testsinglephase.c
index 066e0db..2c59085 100644
--- a/Modules/_testsinglephase.c
+++ b/Modules/_testsinglephase.c
@@ -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:
@@ -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,
@@ -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
@@ -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);
+}
diff --git a/Python/import.c b/Python/import.c
index 26ad843..460b1fe 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -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)
{
@@ -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) {
@@ -1392,6 +1398,7 @@ finally:
cleanup_old_cached_def(&olddefbase);
}
+finally_oldvalue:
extensions_lock_release();
if (key != NULL) {
hashtable_destroy_str(key);
@@ -2128,6 +2135,7 @@ error:
}
+// Used in _PyImport_ClearExtension; see notes there.
static int
clear_singlephase_extension(PyInterpreterState *interp,
PyObject *name, PyObject *path)
diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv
index fabb5de..b002ada 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -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 -