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
27 changes: 22 additions & 5 deletions 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,10 +417,27 @@ Importing Modules
(Custom importers should do this in their
:py:meth:`~importlib.abc.Loader.exec_module` method.)

If the spec's name is dotted and *initfunc* creates the module using only
the last component of the name, the module's :attr:`~module.__name__`
is set to the full name from the spec.

On error, return NULL with an exception set.

For modules using :ref:`legacy single-phase initialization <single-phase-initialization>`,
most of the import process is done in this function, rather than in
extra steps that importers typically take after
:py:meth:`~importlib.abc.Loader.create_module`.
Specifically:

- The module is added to :data:`sys.modules` under the *spec*'s
:py:attr:`~importlib.machinery.ModuleSpec.name` as part of initialization.
- If *any* single-phase module was previously imported under the
same :py:attr:`~importlib.machinery.ModuleSpec.name` (by this function
or by other import machinery,
including in another :ref:`interpreter <sub-interpreter-support>`),
Python does not call *initfunc* again,
and instead reuses the previous module's contents.
- The module will be fully initialized by *initfunc*.
Calling :c:func:`PyModule_Exec` on it will have no effect, though it is
recommended (in case the module's implementation will change).
- Also, if the *spec*'s :py:attr:`!name` contains dots, this name is used
for prepending a parent package to the new module's
:attr:`~module.__name__`.

.. versionadded:: 3.15
31 changes: 21 additions & 10 deletions Lib/test/test_embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,16 +273,27 @@ 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"
"<module 'sp_pkg.sp_submod' (static-extension)>\n"
"sp_pkg.sp_submod.__name__='sp_pkg.sp_submod'\n"
"sys.modules[\"sp_pkg.sp_submod\"] is sp_pkg.sp_submod=True\n"
"\"sp_submod\" in sys.modules=False\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"
# Static extension
"created sp_pkg: in sys.modules=False\n"
"created sp_pkg.sp_submod: in sys.modules=True\n"
"<module 'sp_pkg.sp_submod' (static-extension)>\n"
"sp_pkg.sp_submod.__name__='sp_pkg.sp_submod'\n"
"sys.modules[\"sp_pkg.sp_submod\"] is sp_pkg.sp_submod=True\n"
"\"sp_submod\" in sys.modules=False\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 @@ -2357,6 +2357,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);
}

// A single-phase init submodule that only knows its short name,
// like pybind11's PYBIND11_MODULE(sp_submod, m) would.
static PyModuleDef cmfi_sp_submod_def = {
Expand Down Expand Up @@ -2406,6 +2458,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);
}
if (PyUnicode_EqualToUTF8(name, "sp_pkg")) {
Py_DECREF(name);
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_sp_pkg);
Expand All @@ -2430,6 +2486,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 @@ -2461,6 +2519,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()=}');"
// Single-phase submodule whose init only knows its short name
L"import sp_pkg.sp_submod;"
L"print(sp_pkg.sp_submod);"
Expand Down Expand Up @@ -2493,7 +2557,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