Skip to content

Commit 0a8cedb

Browse files
itamarometa-codesync[bot]
authored andcommitted
Backport gh-157384: Set package context in PyImport_CreateModuleFromInitfunc()
Summary: Backport of the upstream fix (python/cpython#157387, 3.15 backport python/cpython#159316), folded into the `05-native-python-api` patch that carries our 3.14 backport of `PyImport_CreateModuleFromInitfunc`. A single-phase init function that creates its module using only the last component of a dotted name (as pybind11 2.13 does for `PYBIND11_MODULE` extensions inside a package) got a module whose `__name__` was the short name, so `def_submodule` registered submodules under the wrong `sys.modules` keys and `from pkg.ext.sub import x` failed with "`pkg.ext` is not a package". 571 failures in V66 of the py3.14 canary (D82323768) carry this signature. Reviewed By: alexmalyshev Differential Revision: D119701167 fbshipit-source-id: dc50c5ece93752c069548588545eee6526181cad
1 parent 0938bd4 commit 0a8cedb

4 files changed

Lines changed: 197 additions & 0 deletions

File tree

‎Doc/c-api/import.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,10 @@ Importing Modules
367367
``PyInit_<name>`` function and *spec* is the
368368
:class:`~importlib.machinery.ModuleSpec` describing the module.
369369
370+
If the spec's name is dotted and *initfunc* creates the module using only
371+
the last component of the name, the module's :attr:`~module.__name__`
372+
is set to the full name from the spec.
373+
370374
371375
.. _lazy-imports:
372376

‎Lib/test/test_embed.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,21 @@ def test_repeated_init_and_inittab(self):
246246
lines = "\n".join(lines) + "\n"
247247
self.assertEqual(out, lines)
248248

249+
def test_create_module_from_initfunc(self):
250+
out, err = self.run_embedded_interpreter("test_create_module_from_initfunc")
251+
self.assertEqual(self._nogil_filtered_err(err, "embedded_ext"), "")
252+
self.assertEqual(out,
253+
"<module 'my_test_extension' (static-extension)>\n"
254+
"my_test_extension.executed='yes'\n"
255+
"my_test_extension.exec_slot_ran='yes'\n"
256+
"<module 'embedded_ext' (static-extension)>\n"
257+
"embedded_ext.executed='yes'\n"
258+
"<module 'sp_pkg.sp_submod' (static-extension)>\n"
259+
"sp_pkg.sp_submod.__name__='sp_pkg.sp_submod'\n"
260+
"sys.modules[\"sp_pkg.sp_submod\"] is sp_pkg.sp_submod=True\n"
261+
"\"sp_submod\" in sys.modules=False\n"
262+
)
263+
249264
def test_inittab_submodule_multiphase(self):
250265
out, err = self.run_embedded_interpreter("test_inittab_submodule_multiphase")
251266
self.assertEqual(err, "")

‎Programs/_testembed.c‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,8 @@ static PyModuleDef embedded_ext = {
214214
static PyObject*
215215
PyInit_embedded_ext(void)
216216
{
217+
// keep this as a single-phase initialization module;
218+
// see test_create_module_from_initfunc
217219
return PyModule_Create(&embedded_ext);
218220
}
219221

@@ -1980,8 +1982,16 @@ static int test_initconfig_exit(void)
19801982
}
19811983

19821984

1985+
int
1986+
extension_module_exec(PyObject *mod)
1987+
{
1988+
return PyModule_AddStringConstant(mod, "exec_slot_ran", "yes");
1989+
}
1990+
1991+
19831992
static PyModuleDef_Slot extension_slots[] = {
19841993
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
1994+
{Py_mod_exec, extension_module_exec},
19851995
{0, NULL}
19861996
};
19871997

@@ -2380,6 +2390,160 @@ static int test_repeated_init_and_inittab(void)
23802390
return 0;
23812391
}
23822392

2393+
// A single-phase init submodule that only knows its short name,
2394+
// like pybind11's PYBIND11_MODULE(sp_submod, m) would.
2395+
static PyModuleDef cmfi_sp_submod_def = {
2396+
PyModuleDef_HEAD_INIT,
2397+
.m_name = "sp_submod",
2398+
.m_size = -1,
2399+
};
2400+
2401+
static PyObject*
2402+
PyInit_cmfi_sp_submod(void)
2403+
{
2404+
return PyModule_Create(&cmfi_sp_submod_def);
2405+
}
2406+
2407+
// A multi-phase init package for the submodule above.
2408+
static PyModuleDef_Slot cmfi_sp_pkg_slots[] = {
2409+
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
2410+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
2411+
{0, NULL},
2412+
};
2413+
2414+
static PyModuleDef cmfi_sp_pkg_def = {
2415+
PyModuleDef_HEAD_INIT,
2416+
.m_name = "sp_pkg",
2417+
.m_size = 0,
2418+
.m_slots = cmfi_sp_pkg_slots,
2419+
};
2420+
2421+
static PyObject*
2422+
PyInit_cmfi_sp_pkg(void)
2423+
{
2424+
return PyModuleDef_Init(&cmfi_sp_pkg_def);
2425+
}
2426+
2427+
static PyObject*
2428+
create_module(PyObject* self, PyObject* spec)
2429+
{
2430+
PyObject *name = PyObject_GetAttrString(spec, "name");
2431+
if (!name) {
2432+
return NULL;
2433+
}
2434+
if (PyUnicode_EqualToUTF8(name, "my_test_extension")) {
2435+
Py_DECREF(name);
2436+
return PyImport_CreateModuleFromInitfunc(spec, init_my_test_extension);
2437+
}
2438+
if (PyUnicode_EqualToUTF8(name, "embedded_ext")) {
2439+
Py_DECREF(name);
2440+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext);
2441+
}
2442+
if (PyUnicode_EqualToUTF8(name, "sp_pkg")) {
2443+
Py_DECREF(name);
2444+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_sp_pkg);
2445+
}
2446+
if (PyUnicode_EqualToUTF8(name, "sp_pkg.sp_submod")) {
2447+
Py_DECREF(name);
2448+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_sp_submod);
2449+
}
2450+
PyErr_Format(PyExc_LookupError, "static module %R not found", name);
2451+
Py_DECREF(name);
2452+
return NULL;
2453+
}
2454+
2455+
static PyObject*
2456+
exec_module(PyObject* self, PyObject* mod)
2457+
{
2458+
PyModuleDef *def = PyModule_GetDef(mod);
2459+
if (def == NULL) {
2460+
return NULL;
2461+
}
2462+
if (PyModule_ExecDef(mod, def) < 0) {
2463+
return NULL;
2464+
}
2465+
Py_RETURN_NONE;
2466+
}
2467+
2468+
static PyMethodDef create_static_module_methods[] = {
2469+
{"create_module", create_module, METH_O, NULL},
2470+
{"exec_module", exec_module, METH_O, NULL},
2471+
{NULL}
2472+
};
2473+
2474+
static struct PyModuleDef create_static_module_def = {
2475+
PyModuleDef_HEAD_INIT,
2476+
.m_name = "create_static_module",
2477+
.m_size = 0,
2478+
.m_methods = create_static_module_methods,
2479+
.m_slots = extension_slots,
2480+
};
2481+
2482+
PyMODINIT_FUNC PyInit_create_static_module(void) {
2483+
return PyModuleDef_Init(&create_static_module_def);
2484+
}
2485+
2486+
static int
2487+
test_create_module_from_initfunc(void)
2488+
{
2489+
wchar_t* argv[] = {
2490+
PROGRAM_NAME,
2491+
L"-c",
2492+
// Multi-phase initialization
2493+
L"import my_test_extension;"
2494+
L"print(my_test_extension);"
2495+
L"print(f'{my_test_extension.executed=}');"
2496+
L"print(f'{my_test_extension.exec_slot_ran=}');"
2497+
// Single-phase initialization
2498+
L"import embedded_ext;"
2499+
L"print(embedded_ext);"
2500+
L"print(f'{embedded_ext.executed=}');"
2501+
// Single-phase submodule whose init only knows its short name
2502+
L"import sp_pkg.sp_submod;"
2503+
L"print(sp_pkg.sp_submod);"
2504+
L"print(f'{sp_pkg.sp_submod.__name__=}');"
2505+
L"print(f'{sys.modules[\"sp_pkg.sp_submod\"] is sp_pkg.sp_submod=}');"
2506+
L"print(f'{\"sp_submod\" in sys.modules=}');"
2507+
};
2508+
PyConfig config;
2509+
if (PyImport_AppendInittab("create_static_module",
2510+
&PyInit_create_static_module) != 0) {
2511+
fprintf(stderr, "PyImport_AppendInittab() failed\n");
2512+
return 1;
2513+
}
2514+
PyConfig_InitPythonConfig(&config);
2515+
config.isolated = 1;
2516+
config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv);
2517+
init_from_config_clear(&config);
2518+
int result = PyRun_SimpleString(
2519+
"import sys\n"
2520+
"from importlib.util import spec_from_loader\n"
2521+
"import create_static_module\n"
2522+
"class StaticExtensionImporter:\n"
2523+
" _ORIGIN = \"static-extension\"\n"
2524+
" @classmethod\n"
2525+
" def find_spec(cls, fullname, path, target=None):\n"
2526+
" if fullname in {'my_test_extension', 'embedded_ext',\n"
2527+
" 'sp_pkg', 'sp_pkg.sp_submod'}:\n"
2528+
" return spec_from_loader(fullname, cls, origin=cls._ORIGIN,\n"
2529+
" is_package=(fullname == 'sp_pkg'))\n"
2530+
" return None\n"
2531+
" @staticmethod\n"
2532+
" def create_module(spec):\n"
2533+
" return create_static_module.create_module(spec)\n"
2534+
" @staticmethod\n"
2535+
" def exec_module(module):\n"
2536+
" create_static_module.exec_module(module)\n"
2537+
" module.executed = 'yes'\n"
2538+
"sys.meta_path.append(StaticExtensionImporter)\n"
2539+
);
2540+
if (result < 0) {
2541+
fprintf(stderr, "PyRun_SimpleString() failed\n");
2542+
return 1;
2543+
}
2544+
return Py_RunMain();
2545+
}
2546+
23832547
/// Multi-phase initialization package & submodule ///
23842548

23852549
int
@@ -2714,6 +2878,7 @@ static struct TestCase TestCases[] = {
27142878
{"test_get_incomplete_frame", test_get_incomplete_frame},
27152879
{"test_inittab_submodule_multiphase", test_inittab_submodule_multiphase},
27162880
{"test_inittab_submodule_singlephase", test_inittab_submodule_singlephase},
2881+
{"test_create_module_from_initfunc", test_create_module_from_initfunc},
27172882
{NULL, NULL}
27182883
};
27192884

‎Python/import.c‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2439,6 +2439,19 @@ create_builtin(
24392439
if (_Py_ext_module_loader_info_init_for_builtin(&info, name) < 0) {
24402440
return NULL;
24412441
}
2442+
if (initfunc != NULL) {
2443+
/* An explicitly provided init function (see
2444+
* PyImport_CreateModuleFromInitfunc()) may belong to a submodule
2445+
* whose single-phase init creates the module using only the last
2446+
* component of the name (as e.g. pybind11 does). Set the package
2447+
* context so that PyModule_Create() resolves the full name, the
2448+
* same as for dynamically loaded extensions. */
2449+
info.newcontext = PyUnicode_AsUTF8(info.name);
2450+
if (info.newcontext == NULL) {
2451+
_Py_ext_module_loader_info_clear(&info);
2452+
return NULL;
2453+
}
2454+
}
24422455

24432456
struct extensions_cache_value *cached = NULL;
24442457
PyObject *mod = import_find_extension(tstate, &info, &cached);

0 commit comments

Comments
 (0)