gh-123880: Allow recursive import of single-phase-init modules by encukou · Pull Request #123950 · python/cpython · GitHub
Skip to content
Merged
69 changes: 51 additions & 18 deletions Lib/test/test_import/__init__.py
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