Skip to content

Commit 16b70db

Browse files
itamaroclaude
andcommitted
pythongh-157384: Document and test PyImport_CreateModuleFromInitfunc() semantics
Document that single-phase init modules are added to sys.modules by the call itself (unlike multi-phase init modules), and that the spec name identifies the module, so a later call with the same name and a different init function reuses the cached module without calling the new init function. Add regression tests for both behaviors. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoZE3WdRWQshvinfBZ137N
1 parent 2cd6d4b commit 16b70db

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

‎Doc/c-api/import.rst‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,20 @@ Importing Modules
417417
(Custom importers should do this in their
418418
:py:meth:`~importlib.abc.Loader.exec_module` method.)
419419
420+
If *initfunc* uses legacy single-phase initialization (that is, it
421+
creates the module with :c:func:`PyModule_Create`), the module is fully
422+
initialized by *initfunc* itself, and it is also added to
423+
:data:`sys.modules` under the spec's name, as is done for modules
424+
registered with :c:func:`PyImport_AppendInittab`.
425+
Calling :c:func:`PyModule_Exec` on such a module is still safe.
426+
427+
The spec's name identifies the module for the purposes of the import
428+
system, in the same way as for built-in modules.
429+
If a single-phase init module was previously created under the same name
430+
(by this function, or in another interpreter), the existing module
431+
definition is reused and *initfunc* is not called, so a later call with
432+
the same name but a different *initfunc* has no effect.
433+
420434
On error, return NULL with an exception set.
421435
422436
.. versionadded:: 3.15

‎Lib/test/test_embed.py‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,20 @@ def test_create_module_from_initfunc(self):
273273
out, err = self.run_embedded_interpreter("test_create_module_from_initfunc")
274274
self.assertEqual(self._nogil_filtered_err(err, "embedded_ext"), "")
275275
self.assertEqual(out,
276-
"<module 'my_test_extension' (static-extension)>\n"
277-
"my_test_extension.executed='yes'\n"
278-
"my_test_extension.exec_slot_ran='yes'\n"
279-
"<module 'embedded_ext' (static-extension)>\n"
280-
"embedded_ext.executed='yes'\n"
281-
)
276+
# multi-phase init: not in sys.modules until importlib adds it
277+
"created my_test_extension: in sys.modules=False\n"
278+
"<module 'my_test_extension' (static-extension)>\n"
279+
"my_test_extension.executed='yes'\n"
280+
"my_test_extension.exec_slot_ran='yes'\n"
281+
# single-phase init: added to sys.modules by the init function
282+
"created embedded_ext: in sys.modules=True\n"
283+
"<module 'embedded_ext' (static-extension)>\n"
284+
"embedded_ext.executed='yes'\n"
285+
# same name, different initfuncs: the cached module is
286+
# returned and the second initfunc is never called
287+
"a.which='A' b.which='A' a is b=True\n"
288+
"create_static_module.initfunc_calls()=(1, 0)\n"
289+
)
282290

283291
def test_inittab_submodule_multiphase(self):
284292
out, err = self.run_embedded_interpreter("test_inittab_submodule_multiphase")

‎Programs/_testembed.c‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,58 @@ static int test_repeated_init_and_inittab(void)
24102410
return 0;
24112411
}
24122412

2413+
// Two different single-phase init functions for the same module name.
2414+
static int cmfi_initfunc_a_calls = 0;
2415+
static int cmfi_initfunc_b_calls = 0;
2416+
2417+
static PyModuleDef cmfi_same_name_a_def = {
2418+
PyModuleDef_HEAD_INIT,
2419+
.m_name = "same_name",
2420+
.m_size = -1,
2421+
};
2422+
2423+
static PyModuleDef cmfi_same_name_b_def = {
2424+
PyModuleDef_HEAD_INIT,
2425+
.m_name = "same_name",
2426+
.m_size = -1,
2427+
};
2428+
2429+
static PyObject*
2430+
PyInit_cmfi_same_name_a(void)
2431+
{
2432+
cmfi_initfunc_a_calls++;
2433+
PyObject *mod = PyModule_Create(&cmfi_same_name_a_def);
2434+
if (mod == NULL || PyModule_AddStringConstant(mod, "which", "A") < 0) {
2435+
Py_XDECREF(mod);
2436+
return NULL;
2437+
}
2438+
return mod;
2439+
}
2440+
2441+
static PyObject*
2442+
PyInit_cmfi_same_name_b(void)
2443+
{
2444+
cmfi_initfunc_b_calls++;
2445+
PyObject *mod = PyModule_Create(&cmfi_same_name_b_def);
2446+
if (mod == NULL || PyModule_AddStringConstant(mod, "which", "B") < 0) {
2447+
Py_XDECREF(mod);
2448+
return NULL;
2449+
}
2450+
return mod;
2451+
}
2452+
2453+
static PyObject*
2454+
create_same_name_b(PyObject* self, PyObject* spec)
2455+
{
2456+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_same_name_b);
2457+
}
2458+
2459+
static PyObject*
2460+
initfunc_calls(PyObject* self, PyObject* Py_UNUSED(args))
2461+
{
2462+
return Py_BuildValue("(ii)", cmfi_initfunc_a_calls, cmfi_initfunc_b_calls);
2463+
}
2464+
24132465
static PyObject*
24142466
create_module(PyObject* self, PyObject* spec)
24152467
{
@@ -2425,6 +2477,10 @@ create_module(PyObject* self, PyObject* spec)
24252477
Py_DECREF(name);
24262478
return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext);
24272479
}
2480+
if (PyUnicode_EqualToUTF8(name, "same_name")) {
2481+
Py_DECREF(name);
2482+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_same_name_a);
2483+
}
24282484
PyErr_Format(PyExc_LookupError, "static module %R not found", name);
24292485
Py_DECREF(name);
24302486
return NULL;
@@ -2441,6 +2497,8 @@ exec_module(PyObject* self, PyObject* mod)
24412497

24422498
static PyMethodDef create_static_module_methods[] = {
24432499
{"create_module", create_module, METH_O, NULL},
2500+
{"create_same_name_b", create_same_name_b, METH_O, NULL},
2501+
{"initfunc_calls", initfunc_calls, METH_NOARGS, NULL},
24442502
{"exec_module", exec_module, METH_O, NULL},
24452503
{NULL}
24462504
};
@@ -2472,6 +2530,12 @@ test_create_module_from_initfunc(void)
24722530
L"import embedded_ext;"
24732531
L"print(embedded_ext);"
24742532
L"print(f'{embedded_ext.executed=}');"
2533+
// Same name, different initfuncs: the first one wins
2534+
L"spec = spec_from_loader('same_name', StaticExtensionImporter);"
2535+
L"a = create_static_module.create_module(spec);"
2536+
L"b = create_static_module.create_same_name_b(spec);"
2537+
L"print(f'{a.which=} {b.which=} {a is b=}');"
2538+
L"print(f'{create_static_module.initfunc_calls()=}');"
24752539
};
24762540
PyConfig config;
24772541
if (PyImport_AppendInittab("create_static_module",
@@ -2496,7 +2560,10 @@ test_create_module_from_initfunc(void)
24962560
" return None\n"
24972561
" @staticmethod\n"
24982562
" def create_module(spec):\n"
2499-
" return create_static_module.create_module(spec)\n"
2563+
" mod = create_static_module.create_module(spec)\n"
2564+
" print(f'created {spec.name}: '\n"
2565+
" f'in sys.modules={spec.name in sys.modules}')\n"
2566+
" return mod\n"
25002567
" @staticmethod\n"
25012568
" def exec_module(module):\n"
25022569
" create_static_module.exec_module(module)\n"

0 commit comments

Comments
 (0)