Skip to content

Commit 58ba5ba

Browse files
encukouhauntsaninjabrettcannon
authored andcommitted
gh-123880: Allow recursive import of single-phase-init modules (GH-123950)
(cherry picked from commit aee219f) Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Shantanu <[email protected]> Co-authored-by: Brett Cannon <[email protected]>
1 parent 112b170 commit 58ba5ba

File tree

6 files changed

+141
-25
lines changed

6 files changed

+141
-25
lines changed

Lib/test/test_import/__init__.py

+51-18
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ def require_frozen(module, *, skip=True):
111111
def require_pure_python(module, *, skip=False):
112112
_require_loader(module, SourceFileLoader, skip)
113113

114+
def create_extension_loader(modname, filename):
115+
# Apple extensions must be distributed as frameworks. This requires
116+
# a specialist loader.
117+
if is_apple_mobile:
118+
return AppleFrameworkLoader(modname, filename)
119+
else:
120+
return ExtensionFileLoader(modname, filename)
121+
122+
def import_extension_from_file(modname, filename, *, put_in_sys_modules=True):
123+
loader = create_extension_loader(modname, filename)
124+
spec = importlib.util.spec_from_loader(modname, loader)
125+
module = importlib.util.module_from_spec(spec)
126+
loader.exec_module(module)
127+
if put_in_sys_modules:
128+
sys.modules[modname] = module
129+
return module
130+
131+
114132
def remove_files(name):
115133
for f in (name + ".py",
116134
name + ".pyc",
@@ -1913,6 +1931,37 @@ def test_absolute_circular_submodule(self):
19131931
str(cm.exception),
19141932
)
19151933

1934+
@requires_singlephase_init
1935+
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
1936+
def test_singlephase_circular(self):
1937+
"""Regression test for gh-123950
1938+
1939+
Import a single-phase-init module that imports itself
1940+
from the PyInit_* function (before it's added to sys.modules).
1941+
Manages its own cache (which is `static`, and so incompatible
1942+
with multiple interpreters or interpreter reset).
1943+
"""
1944+
name = '_testsinglephase_circular'
1945+
helper_name = 'test.test_import.data.circular_imports.singlephase'
1946+
with uncache(name, helper_name):
1947+
filename = _testsinglephase.__file__
1948+
# We don't put the module in sys.modules: that the *inner*
1949+
# import should do that.
1950+
mod = import_extension_from_file(name, filename,
1951+
put_in_sys_modules=False)
1952+
1953+
self.assertEqual(mod.helper_mod_name, helper_name)
1954+
self.assertIn(name, sys.modules)
1955+
self.assertIn(helper_name, sys.modules)
1956+
1957+
self.assertIn(name, sys.modules)
1958+
self.assertIn(helper_name, sys.modules)
1959+
self.assertNotIn(name, sys.modules)
1960+
self.assertNotIn(helper_name, sys.modules)
1961+
self.assertIs(mod.clear_static_var(), mod)
1962+
_testinternalcapi.clear_extension('_testsinglephase_circular',
1963+
mod.__spec__.origin)
1964+
19161965
def test_unwritable_module(self):
19171966
self.addCleanup(unload, "test.test_import.data.unwritable")
19181967
self.addCleanup(unload, "test.test_import.data.unwritable.x")
@@ -1952,14 +2001,6 @@ def pipe(self):
19522001
os.set_blocking(r, False)
19532002
return (r, w)
19542003

1955-
def create_extension_loader(self, modname, filename):
1956-
# Apple extensions must be distributed as frameworks. This requires
1957-
# a specialist loader.
1958-
if is_apple_mobile:
1959-
return AppleFrameworkLoader(modname, filename)
1960-
else:
1961-
return ExtensionFileLoader(modname, filename)
1962-
19632004
def import_script(self, name, fd, filename=None, check_override=None):
19642005
override_text = ''
19652006
if check_override is not None:
@@ -2176,11 +2217,7 @@ def test_multi_init_extension_compat(self):
21762217
def test_multi_init_extension_non_isolated_compat(self):
21772218
modname = '_test_non_isolated'
21782219
filename = _testmultiphase.__file__
2179-
loader = self.create_extension_loader(modname, filename)
2180-
spec = importlib.util.spec_from_loader(modname, loader)
2181-
module = importlib.util.module_from_spec(spec)
2182-
loader.exec_module(module)
2183-
sys.modules[modname] = module
2220+
module = import_extension_from_file(modname, filename)
21842221

21852222
require_extension(module)
21862223
with self.subTest(f'{modname}: isolated'):
@@ -2195,11 +2232,7 @@ def test_multi_init_extension_non_isolated_compat(self):
21952232
def test_multi_init_extension_per_interpreter_gil_compat(self):
21962233
modname = '_test_shared_gil_only'
21972234
filename = _testmultiphase.__file__
2198-
loader = self.create_extension_loader(modname, filename)
2199-
spec = importlib.util.spec_from_loader(modname, loader)
2200-
module = importlib.util.module_from_spec(spec)
2201-
loader.exec_module(module)
2202-
sys.modules[modname] = module
2235+
module = import_extension_from_file(modname, filename)
22032236

22042237
require_extension(module)
22052238
with self.subTest(f'{modname}: isolated, strict'):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Circular import involving a single-phase-init extension.
2+
3+
This module is imported from the _testsinglephase_circular module from
4+
_testsinglephase, and imports that module again.
5+
"""
6+
7+
import importlib
8+
import _testsinglephase
9+
from test.test_import import import_extension_from_file
10+
11+
name = '_testsinglephase_circular'
12+
filename = _testsinglephase.__file__
13+
mod = import_extension_from_file(name, filename)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a bug that prevented circular imports of extension modules that use
2+
single-phase initialization.

Modules/_testsinglephase.c

+61-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
/* Testing module for single-phase initialization of extension modules
33
4-
This file contains 8 distinct modules, meaning each as its own name
4+
This file contains several distinct modules, meaning each as its own name
55
and its own init function (PyInit_...). The default import system will
66
only find the one matching the filename: _testsinglephase. To load the
77
others you must do so manually. For example:
@@ -12,9 +12,13 @@ filename = _testsinglephase.__file__
1212
loader = importlib.machinery.ExtensionFileLoader(name, filename)
1313
spec = importlib.util.spec_from_file_location(name, filename, loader=loader)
1414
mod = importlib._bootstrap._load(spec)
15+
loader.exec_module(module)
16+
sys.modules[modname] = module
1517
```
1618
17-
Here are the 8 modules:
19+
(The last two lines are just for completeness.)
20+
21+
Here are the modules:
1822
1923
* _testsinglephase
2024
* def: _testsinglephase_basic,
@@ -163,6 +167,11 @@ Here are the 8 modules:
163167
* functions: none
164168
* import system: same as _testsinglephase_with_state
165169
170+
* _testsinglephase_circular
171+
Regression test for gh-123880.
172+
Does not have the common attributes & methods.
173+
See test_singlephase_circular test.test_import.SinglephaseInitTests.
174+
166175
Module state:
167176
168177
* fields
@@ -740,3 +749,53 @@ PyInit__testsinglephase_with_state_check_cache_first(void)
740749
}
741750
return PyModule_Create(&_testsinglephase_with_state_check_cache_first);
742751
}
752+
753+
754+
/****************************************/
755+
/* the _testsinglephase_circular module */
756+
/****************************************/
757+
758+
static PyObject *static_module_circular;
759+
760+
static PyObject *
761+
circularmod_clear_static_var(PyObject *self, PyObject *arg)
762+
{
763+
PyObject *result = static_module_circular;
764+
static_module_circular = NULL;
765+
return result;
766+
}
767+
768+
static struct PyModuleDef _testsinglephase_circular = {
769+
PyModuleDef_HEAD_INIT,
770+
.m_name = "_testsinglephase_circular",
771+
.m_doc = PyDoc_STR("Test module _testsinglephase_circular"),
772+
.m_methods = (PyMethodDef[]) {
773+
{"clear_static_var", circularmod_clear_static_var, METH_NOARGS,
774+
"Clear the static variable and return its previous value."},
775+
{NULL, NULL} /* sentinel */
776+
}
777+
};
778+
779+
PyMODINIT_FUNC
780+
PyInit__testsinglephase_circular(void)
781+
{
782+
if (!static_module_circular) {
783+
static_module_circular = PyModule_Create(&_testsinglephase_circular);
784+
if (!static_module_circular) {
785+
return NULL;
786+
}
787+
}
788+
static const char helper_mod_name[] = (
789+
"test.test_import.data.circular_imports.singlephase");
790+
PyObject *helper_mod = PyImport_ImportModule(helper_mod_name);
791+
Py_XDECREF(helper_mod);
792+
if (!helper_mod) {
793+
return NULL;
794+
}
795+
if(PyModule_AddStringConstant(static_module_circular,
796+
"helper_mod_name",
797+
helper_mod_name) < 0) {
798+
return NULL;
799+
}
800+
return Py_NewRef(static_module_circular);
801+
}

Python/import.c

+13-5
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,8 @@ static int clear_singlephase_extension(PyInterpreterState *interp,
814814

815815
// Currently, this is only used for testing.
816816
// (See _testinternalcapi.clear_extension().)
817+
// If adding another use, be careful about modules that import themselves
818+
// recursively (see gh-123880).
817819
int
818820
_PyImport_ClearExtension(PyObject *name, PyObject *filename)
819821
{
@@ -1321,12 +1323,16 @@ _extensions_cache_set(PyObject *path, PyObject *name,
13211323
value = entry == NULL
13221324
? NULL
13231325
: (struct extensions_cache_value *)entry->value;
1324-
/* We should never be updating an existing cache value. */
1325-
assert(value == NULL);
13261326
if (value != NULL) {
1327-
PyErr_Format(PyExc_SystemError,
1328-
"extension module %R is already cached", name);
1329-
goto finally;
1327+
/* gh-123880: If there's an existing cache value, it means a module is
1328+
* being imported recursively from its PyInit_* or Py_mod_* function.
1329+
* (That function presumably handles returning a partially
1330+
* constructed module in such a case.)
1331+
* We can reuse the existing cache value; it is owned by the cache.
1332+
* (Entries get removed from it in exceptional circumstances,
1333+
* after interpreter shutdown, and in runtime shutdown.)
1334+
*/
1335+
goto finally_oldvalue;
13301336
}
13311337
newvalue = alloc_extensions_cache_value();
13321338
if (newvalue == NULL) {
@@ -1391,6 +1397,7 @@ _extensions_cache_set(PyObject *path, PyObject *name,
13911397
cleanup_old_cached_def(&olddefbase);
13921398
}
13931399

1400+
finally_oldvalue:
13941401
extensions_lock_release();
13951402
if (key != NULL) {
13961403
hashtable_destroy_str(key);
@@ -2127,6 +2134,7 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,
21272134
}
21282135

21292136

2137+
// Used in _PyImport_ClearExtension; see notes there.
21302138
static int
21312139
clear_singlephase_extension(PyInterpreterState *interp,
21322140
PyObject *name, PyObject *path)

Tools/c-analyzer/cpython/ignored.tsv

+1
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ Modules/_testmultiphase.c - slots_nonmodule_with_exec_slots -
595595
Modules/_testmultiphase.c - testexport_methods -
596596
Modules/_testmultiphase.c - uninitialized_def -
597597
Modules/_testsinglephase.c - global_state -
598+
Modules/_testsinglephase.c - static_module_circular -
598599
Modules/_xxtestfuzz/_xxtestfuzz.c - _fuzzmodule -
599600
Modules/_xxtestfuzz/_xxtestfuzz.c - module_methods -
600601
Modules/_xxtestfuzz/fuzzer.c - RE_FLAG_DEBUG -

0 commit comments

Comments
 (0)