From d98684b01142c7525411ce622424034cba4bb468 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Tue, 13 Oct 2026 06:35:19 -0700 Subject: [PATCH] gh-157384: Set package context in PyImport_CreateModuleFromInitfunc() (GH-157387) A single-phase init function that creates its module using only the last component of a dotted name (as pybind11 does for submodules) got a module whose __name__ was the short name. Set the package context while calling the init function, as the dynamic loader does, so the module gets the full name from the spec. (cherry picked from commit 04862280a2ccbf9be9883eb4bce886f6b1bc4ca0) Co-authored-by: Itamar Oren --- Doc/c-api/import.rst | 4 ++ Lib/test/test_embed.py | 4 ++ ...-09-12-18-37-22.gh-issue-157384.LGsy8K.rst | 3 ++ Programs/_testembed.c | 54 ++++++++++++++++++- Python/import.c | 13 +++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-09-12-18-37-22.gh-issue-157384.LGsy8K.rst diff --git a/Doc/c-api/import.rst b/Doc/c-api/import.rst index 7a3e5357e0c2ca..e9911a3efcbeb4 100644 --- a/Doc/c-api/import.rst +++ b/Doc/c-api/import.rst @@ -417,6 +417,10 @@ 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. .. versionadded:: 3.15 diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 420eb7e8c2ca52..dfa064a8e48a36 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -255,6 +255,10 @@ def test_create_module_from_initfunc(self): "my_test_extension.exec_slot_ran='yes'\n" "\n" "embedded_ext.executed='yes'\n" + "\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): diff --git a/Misc/NEWS.d/next/C_API/2026-09-12-18-37-22.gh-issue-157384.LGsy8K.rst b/Misc/NEWS.d/next/C_API/2026-09-12-18-37-22.gh-issue-157384.LGsy8K.rst new file mode 100644 index 00000000000000..9f79975e25123a --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-09-12-18-37-22.gh-issue-157384.LGsy8K.rst @@ -0,0 +1,3 @@ +:c:func:`PyImport_CreateModuleFromInitfunc` now sets the package context +while calling *initfunc*, so that a single-phase init submodule created with +only the last component of its name gets the full name from the spec. diff --git a/Programs/_testembed.c b/Programs/_testembed.c index f4df1014b7aeaf..45111f5dfc8c47 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2405,6 +2405,40 @@ static int test_repeated_init_and_inittab(void) return 0; } +// 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 = { + PyModuleDef_HEAD_INIT, + .m_name = "sp_submod", + .m_size = -1, +}; + +static PyObject* +PyInit_cmfi_sp_submod(void) +{ + return PyModule_Create(&cmfi_sp_submod_def); +} + +// A multi-phase init package for the submodule above. +static PyModuleDef_Slot cmfi_sp_pkg_slots[] = { + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {0, NULL}, +}; + +static PyModuleDef cmfi_sp_pkg_def = { + PyModuleDef_HEAD_INIT, + .m_name = "sp_pkg", + .m_size = 0, + .m_slots = cmfi_sp_pkg_slots, +}; + +static PyObject* +PyInit_cmfi_sp_pkg(void) +{ + return PyModuleDef_Init(&cmfi_sp_pkg_def); +} + static PyObject* create_module(PyObject* self, PyObject* spec) { @@ -2420,6 +2454,14 @@ create_module(PyObject* self, PyObject* spec) Py_DECREF(name); return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext); } + if (PyUnicode_EqualToUTF8(name, "sp_pkg")) { + Py_DECREF(name); + return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_sp_pkg); + } + if (PyUnicode_EqualToUTF8(name, "sp_pkg.sp_submod")) { + Py_DECREF(name); + return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_sp_submod); + } PyErr_Format(PyExc_LookupError, "static module %R not found", name); Py_DECREF(name); return NULL; @@ -2467,6 +2509,12 @@ test_create_module_from_initfunc(void) L"import embedded_ext;" L"print(embedded_ext);" L"print(f'{embedded_ext.executed=}');" + // Single-phase submodule whose init only knows its short name + L"import sp_pkg.sp_submod;" + L"print(sp_pkg.sp_submod);" + L"print(f'{sp_pkg.sp_submod.__name__=}');" + L"print(f'{sys.modules[\"sp_pkg.sp_submod\"] is sp_pkg.sp_submod=}');" + L"print(f'{\"sp_submod\" in sys.modules=}');" }; PyConfig config; if (PyImport_AppendInittab("create_static_module", @@ -2486,8 +2534,10 @@ test_create_module_from_initfunc(void) " _ORIGIN = \"static-extension\"\n" " @classmethod\n" " def find_spec(cls, fullname, path, target=None):\n" - " if fullname in {'my_test_extension', 'embedded_ext'}:\n" - " return spec_from_loader(fullname, cls, origin=cls._ORIGIN)\n" + " if fullname in {'my_test_extension', 'embedded_ext',\n" + " 'sp_pkg', 'sp_pkg.sp_submod'}:\n" + " return spec_from_loader(fullname, cls, origin=cls._ORIGIN,\n" + " is_package=(fullname == 'sp_pkg'))\n" " return None\n" " @staticmethod\n" " def create_module(spec):\n" diff --git a/Python/import.c b/Python/import.c index daa9049486e181..92bbde37af1ba2 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2512,6 +2512,19 @@ create_builtin( if (_Py_ext_module_loader_info_init_for_builtin(&info, name) < 0) { return NULL; } + if (initfunc != NULL) { + /* An explicitly provided init function (see + * PyImport_CreateModuleFromInitfunc()) may belong to a submodule + * whose single-phase init creates the module using only the last + * component of the name (as e.g. pybind11 does). Set the package + * context so that PyModule_Create() resolves the full name, the + * same as for dynamically loaded extensions. */ + info.newcontext = PyUnicode_AsUTF8(info.name); + if (info.newcontext == NULL) { + _Py_ext_module_loader_info_clear(&info); + return NULL; + } + } struct extensions_cache_value *cached = NULL; PyObject *mod = import_find_extension(tstate, &info, &cached);