diff --git a/Doc/c-api/import.rst b/Doc/c-api/import.rst index 7a3e5357e0c2ca..9a077b46f9c295 100644 --- a/Doc/c-api/import.rst +++ b/Doc/c-api/import.rst @@ -417,6 +417,11 @@ 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 (as some binding generators do for + submodules), 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 1ff600e30bf4cb..69df4bc22b099e 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -278,6 +278,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 418609abc5f6b8..9c378b6236ce3f 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2410,6 +2410,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) { @@ -2425,6 +2459,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; @@ -2472,6 +2514,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", @@ -2491,8 +2539,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 037f15d4ca2baf..55c33cfd8b4aef 100644 --- a/Python/import.c +++ b/Python/import.c @@ -2507,6 +2507,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);