Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/c-api/extension-modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,11 @@ in the following ways:
again.
Instead, it creates a new module object with a new ``__dict__``, and copies
the saved contents to it.
Modules are matched to the saved contents by the module's full dotted
name; the initialization function used is not taken into account.

As part of the first initialization, Python also adds the module to
:data:`sys.modules` under its name.
For example, given a single-phase module ``_testsinglephase``
[#testsinglephase]_ that defines a function ``sum`` and an exception class
``error``:
Expand Down
13 changes: 12 additions & 1 deletion Doc/c-api/import.rst
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ Importing Modules

*spec* must be a :class:`~importlib.machinery.ModuleSpec` object.

*initfunc* must be an :ref:`initialization function <extension-export-hook>`,
*initfunc* must be an :ref:`initialization function <extension-pyinit>`,
the same as for :c:func:`PyImport_AppendInittab`.

On success, create and return a module object.
Expand All @@ -417,6 +417,17 @@ Importing Modules
(Custom importers should do this in their
:py:meth:`~importlib.abc.Loader.exec_module` method.)

If *initfunc* uses
:ref:`legacy single-phase initialization <single-phase-initialization>`,
the module is fully initialized by *initfunc* itself, and it is also
added to :data:`sys.modules` under the spec's name.
Calling :c:func:`PyModule_Exec` on such a module is still safe.
As with any single-phase module, the spec's name identifies the module:
if a single-phase module was previously created under the same name
(by this function, or in another interpreter), Python does not call
*initfunc* again but reuses the saved module contents, 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
20 changes: 14 additions & 6 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
"<module 'my_test_extension' (static-extension)>\n"
"my_test_extension.executed='yes'\n"
"my_test_extension.exec_slot_ran='yes'\n"
"<module 'embedded_ext' (static-extension)>\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"
"<module 'my_test_extension' (static-extension)>\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"
"<module 'embedded_ext' (static-extension)>\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")
Expand Down
69 changes: 68 additions & 1 deletion Programs/_testembed.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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;
Expand All @@ -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}
};
Expand Down Expand Up @@ -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",
Expand All @@ -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"
Expand Down
Loading