diff --git a/Doc/c-api/import.rst b/Doc/c-api/import.rst index 7a3e5357e0c2cac..36e7994203afe2f 100644 --- a/Doc/c-api/import.rst +++ b/Doc/c-api/import.rst @@ -417,6 +417,20 @@ Importing Modules (Custom importers should do this in their :py:meth:`~importlib.abc.Loader.exec_module` method.) + If *initfunc* uses legacy single-phase initialization (that is, it + creates the module with :c:func:`PyModule_Create`), the module is fully + initialized by *initfunc* itself, and it is also added to + :data:`sys.modules` under the spec's name, as is done for modules + registered with :c:func:`PyImport_AppendInittab`. + Calling :c:func:`PyModule_Exec` on such a module is still safe. + + The spec's name identifies the module for the purposes of the import + system, in the same way as for built-in modules. + If a single-phase init module was previously created under the same name + (by this function, or in another interpreter), the existing module + definition is reused and *initfunc* is not called, so a later call with + the same name but a different *initfunc* has no effect. + On error, return NULL with an exception set. .. versionadded:: 3.15 diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 1ff600e30bf4cbd..52ce1ab96673a17 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -273,12 +273,20 @@ def test_create_module_from_initfunc(self): out, err = self.run_embedded_interpreter("test_create_module_from_initfunc") self.assertEqual(self._nogil_filtered_err(err, "embedded_ext"), "") self.assertEqual(out, - "\n" - "my_test_extension.executed='yes'\n" - "my_test_extension.exec_slot_ran='yes'\n" - "\n" - "embedded_ext.executed='yes'\n" - ) + # multi-phase init: not in sys.modules until importlib adds it + "created my_test_extension: in sys.modules=False\n" + "\n" + "my_test_extension.executed='yes'\n" + "my_test_extension.exec_slot_ran='yes'\n" + # single-phase init: added to sys.modules by the init function + "created embedded_ext: in sys.modules=True\n" + "\n" + "embedded_ext.executed='yes'\n" + # same name, different initfuncs: the cached module is + # returned and the second initfunc is never called + "a.which='A' b.which='A' a is b=True\n" + "create_static_module.initfunc_calls()=(1, 0)\n" + ) def test_inittab_submodule_multiphase(self): out, err = self.run_embedded_interpreter("test_inittab_submodule_multiphase") diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 418609abc5f6b82..0174ba43c21c058 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2410,6 +2410,58 @@ static int test_repeated_init_and_inittab(void) return 0; } +// Two different single-phase init functions for the same module name. +static int cmfi_initfunc_a_calls = 0; +static int cmfi_initfunc_b_calls = 0; + +static PyModuleDef cmfi_same_name_a_def = { + PyModuleDef_HEAD_INIT, + .m_name = "same_name", + .m_size = -1, +}; + +static PyModuleDef cmfi_same_name_b_def = { + PyModuleDef_HEAD_INIT, + .m_name = "same_name", + .m_size = -1, +}; + +static PyObject* +PyInit_cmfi_same_name_a(void) +{ + cmfi_initfunc_a_calls++; + PyObject *mod = PyModule_Create(&cmfi_same_name_a_def); + if (mod == NULL || PyModule_AddStringConstant(mod, "which", "A") < 0) { + Py_XDECREF(mod); + return NULL; + } + return mod; +} + +static PyObject* +PyInit_cmfi_same_name_b(void) +{ + cmfi_initfunc_b_calls++; + PyObject *mod = PyModule_Create(&cmfi_same_name_b_def); + if (mod == NULL || PyModule_AddStringConstant(mod, "which", "B") < 0) { + Py_XDECREF(mod); + return NULL; + } + return mod; +} + +static PyObject* +create_same_name_b(PyObject* self, PyObject* spec) +{ + return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_same_name_b); +} + +static PyObject* +initfunc_calls(PyObject* self, PyObject* Py_UNUSED(args)) +{ + return Py_BuildValue("(ii)", cmfi_initfunc_a_calls, cmfi_initfunc_b_calls); +} + static PyObject* create_module(PyObject* self, PyObject* spec) { @@ -2425,6 +2477,10 @@ create_module(PyObject* self, PyObject* spec) Py_DECREF(name); return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext); } + if (PyUnicode_EqualToUTF8(name, "same_name")) { + Py_DECREF(name); + return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_same_name_a); + } PyErr_Format(PyExc_LookupError, "static module %R not found", name); Py_DECREF(name); return NULL; @@ -2441,6 +2497,8 @@ exec_module(PyObject* self, PyObject* mod) static PyMethodDef create_static_module_methods[] = { {"create_module", create_module, METH_O, NULL}, + {"create_same_name_b", create_same_name_b, METH_O, NULL}, + {"initfunc_calls", initfunc_calls, METH_NOARGS, NULL}, {"exec_module", exec_module, METH_O, NULL}, {NULL} }; @@ -2472,6 +2530,12 @@ test_create_module_from_initfunc(void) L"import embedded_ext;" L"print(embedded_ext);" L"print(f'{embedded_ext.executed=}');" + // Same name, different initfuncs: the first one wins + L"spec = spec_from_loader('same_name', StaticExtensionImporter);" + L"a = create_static_module.create_module(spec);" + L"b = create_static_module.create_same_name_b(spec);" + L"print(f'{a.which=} {b.which=} {a is b=}');" + L"print(f'{create_static_module.initfunc_calls()=}');" }; PyConfig config; if (PyImport_AppendInittab("create_static_module", @@ -2496,7 +2560,10 @@ test_create_module_from_initfunc(void) " return None\n" " @staticmethod\n" " def create_module(spec):\n" - " return create_static_module.create_module(spec)\n" + " mod = create_static_module.create_module(spec)\n" + " print(f'created {spec.name}: '\n" + " f'in sys.modules={spec.name in sys.modules}')\n" + " return mod\n" " @staticmethod\n" " def exec_module(module):\n" " create_static_module.exec_module(module)\n"