From 4949865b96ab0b46ac2d32a6f752f87bfe879715 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Sat, 15 Nov 2025 16:19:05 -0800 Subject: [PATCH 1/7] Modify test embed to hit the assertion with this, test_embed fails with ``` Assertion failed: (PyUnicode_FindChar(name, '.', 0, PyUnicode_GetLength(name), -1) == -1), function _Py_ext_module_loader_info_init_for_builtin, file importdl.c, line 159. ``` --- Programs/_testembed.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index d0d7d5f03fb9e3..0c8fd7c335d950 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2260,7 +2260,7 @@ static PyMethodDef create_static_module_methods[] = { static struct PyModuleDef create_static_module_def = { PyModuleDef_HEAD_INIT, - .m_name = "create_static_module", + .m_name = "test.create_static_module", .m_size = 0, .m_methods = create_static_module_methods, .m_slots = extension_slots, @@ -2287,7 +2287,7 @@ test_create_module_from_initfunc(void) L"print(f'{embedded_ext.executed=}');" }; PyConfig config; - if (PyImport_AppendInittab("create_static_module", + if (PyImport_AppendInittab("test.create_static_module", &PyInit_create_static_module) != 0) { fprintf(stderr, "PyImport_AppendInittab() failed\n"); return 1; @@ -2299,7 +2299,7 @@ test_create_module_from_initfunc(void) int result = PyRun_SimpleString( "import sys\n" "from importlib.util import spec_from_loader\n" - "import create_static_module\n" + "from test import create_static_module\n" "class StaticExtensionImporter:\n" " _ORIGIN = \"static-extension\"\n" " @classmethod\n" From 34e81ce22bf5823ca17f78f0445b4b0b6b14bc76 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Sat, 15 Nov 2025 16:19:38 -0800 Subject: [PATCH 2/7] Delete the assertion with this, test_embed succeeds again --- Python/importdl.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Python/importdl.c b/Python/importdl.c index 61a9cdaf3754c9..537e8d869dc93c 100644 --- a/Python/importdl.c +++ b/Python/importdl.c @@ -156,7 +156,6 @@ _Py_ext_module_loader_info_init_for_builtin( PyObject *name) { assert(PyUnicode_Check(name)); - assert(PyUnicode_FindChar(name, '.', 0, PyUnicode_GetLength(name), -1) == -1); assert(PyUnicode_GetLength(name) > 0); PyObject *name_encoded = PyUnicode_AsEncodedString(name, "ascii", NULL); From 783da04fc5b0be6edd1c3c6a828d3567f515c085 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Sat, 22 Nov 2025 17:50:20 -0800 Subject: [PATCH 3/7] Revert "Modify test embed to hit the assertion" This reverts commit 4949865b96ab0b46ac2d32a6f752f87bfe879715. --- Programs/_testembed.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 0c8fd7c335d950..d0d7d5f03fb9e3 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2260,7 +2260,7 @@ static PyMethodDef create_static_module_methods[] = { static struct PyModuleDef create_static_module_def = { PyModuleDef_HEAD_INIT, - .m_name = "test.create_static_module", + .m_name = "create_static_module", .m_size = 0, .m_methods = create_static_module_methods, .m_slots = extension_slots, @@ -2287,7 +2287,7 @@ test_create_module_from_initfunc(void) L"print(f'{embedded_ext.executed=}');" }; PyConfig config; - if (PyImport_AppendInittab("test.create_static_module", + if (PyImport_AppendInittab("create_static_module", &PyInit_create_static_module) != 0) { fprintf(stderr, "PyImport_AppendInittab() failed\n"); return 1; @@ -2299,7 +2299,7 @@ test_create_module_from_initfunc(void) int result = PyRun_SimpleString( "import sys\n" "from importlib.util import spec_from_loader\n" - "from test import create_static_module\n" + "import create_static_module\n" "class StaticExtensionImporter:\n" " _ORIGIN = \"static-extension\"\n" " @classmethod\n" From f091794fb3c8477ebab62a0ec627c69c67deb2f6 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Sat, 22 Nov 2025 19:08:25 -0800 Subject: [PATCH 4/7] Add tests for importing a submodule via inittab covering both single-phase init and multi-phase init embedded extensions --- Lib/test/test_embed.py | 54 ++++++++---- Programs/_testembed.c | 183 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 1078796eae84e2..0346fb81a8c146 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -241,21 +241,7 @@ def test_repeated_init_and_inittab(self): def test_create_module_from_initfunc(self): out, err = self.run_embedded_interpreter("test_create_module_from_initfunc") - if support.Py_GIL_DISABLED: - # the test imports a singlephase init extension, so it emits a warning - # under the free-threaded build - expected_runtime_warning = ( - "RuntimeWarning: The global interpreter lock (GIL)" - " has been enabled to load module 'embedded_ext'" - ) - filtered_err_lines = [ - line - for line in err.strip().splitlines() - if expected_runtime_warning not in line - ] - self.assertEqual(filtered_err_lines, []) - else: - self.assertEqual(err, "") + self.assertEqual(self._nogil_filtered_err(err, "embedded_ext"), "") self.assertEqual(out, "\n" "my_test_extension.executed='yes'\n" @@ -264,6 +250,26 @@ def test_create_module_from_initfunc(self): "embedded_ext.executed='yes'\n" ) + def test_inittab_submodule_multiphase(self): + out, err = self.run_embedded_interpreter("test_inittab_submodule_multiphase") + self.assertEqual(err, "") + self.assertEqual(out, + "\n" + "\n" + "Hello from sub-module\n" + "mp_pkg.mp_submod.mp_submod_exec_slot_ran='yes'\n" + "mp_pkg.mp_pkg_exec_slot_ran='yes'\n" + ) + + def test_inittab_submodule_singlephase(self): + out, err = self.run_embedded_interpreter("test_inittab_submodule_singlephase") + self.assertEqual(self._nogil_filtered_err(err, "sp_pkg"), "") + self.assertEqual(out, + "\n" + "\n" + "Hello from sub-module\n" + ) + def test_forced_io_encoding(self): # Checks forced configuration of embedded interpreter IO streams env = dict(os.environ, PYTHONIOENCODING="utf-8:surrogateescape") @@ -540,6 +546,24 @@ def test_getargs_reset_static_parser(self): """) out, err = self.run_embedded_interpreter("test_repeated_init_exec", code) self.assertEqual(out, '1\n2\n3\n' * INIT_LOOPS) + + @staticmethod + def _nogil_filtered_err(err: str, mod_name: str) -> str: + if not support.Py_GIL_DISABLED: + return err + + # the test imports a singlephase init extension, so it emits a warning + # under the free-threaded build + expected_runtime_warning = ( + "RuntimeWarning: The global interpreter lock (GIL)" + f" has been enabled to load module '{mod_name}'" + ) + filtered_err_lines = [ + line + for line in err.strip().splitlines() + if expected_runtime_warning not in line + ] + return "\n".join(filtered_err_lines) def config_dev_mode(preconfig, config): diff --git a/Programs/_testembed.c b/Programs/_testembed.c index d0d7d5f03fb9e3..1ec6bd985a711d 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2323,6 +2323,187 @@ test_create_module_from_initfunc(void) return Py_RunMain(); } +/// Multi-phase initialization package & submodule /// + +int +mp_pkg_exec(PyObject *mod) +{ + // make this a namespace package + PyObject *path_list = PyList_New(0); // empty list = namespace package + if (!path_list) { + return -1; + } + if (PyModule_AddObject(mod, "__path__", path_list) < 0) { + Py_DECREF(path_list); + return -1; + } + if (PyModule_AddStringConstant(mod, "mp_pkg_exec_slot_ran", "yes") < 0) { + Py_DECREF(path_list); + return -1; + } + return 0; +} + +static PyModuleDef_Slot mp_pkg_slots[] = { + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {Py_mod_exec, mp_pkg_exec}, + {0, NULL} +}; + +static struct PyModuleDef mp_pkg_def = { + PyModuleDef_HEAD_INIT, + .m_name = "mp_pkg", + .m_size = 0, + .m_slots = mp_pkg_slots, +}; + +PyMODINIT_FUNC +PyInit_mp_pkg(void) +{ + return PyModuleDef_Init(&mp_pkg_def); +} + +static PyObject * +submod_greet(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + return PyUnicode_FromString("Hello from sub-module"); +} + +static PyMethodDef submod_methods[] = { + {"greet", submod_greet, METH_NOARGS, NULL}, + {NULL}, +}; + +int +mp_submod_exec(PyObject *mod) +{ + return PyModule_AddStringConstant(mod, "mp_submod_exec_slot_ran", "yes"); +} + +static PyModuleDef_Slot mp_submod_slots[] = { + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {Py_mod_exec, mp_submod_exec}, + {0, NULL} +}; + +static struct PyModuleDef mp_submod_def = { + PyModuleDef_HEAD_INIT, + .m_name = "mp_pkg.mp_submod", + .m_size = 0, + .m_methods = submod_methods, + .m_slots = mp_submod_slots, +}; + +PyMODINIT_FUNC +PyInit_mp_submod(void) +{ + return PyModuleDef_Init(&mp_submod_def); +} + +static int +test_inittab_submodule_multiphase(void) +{ + wchar_t* argv[] = { + PROGRAM_NAME, + L"-c", + L"import sys;" + L"import mp_pkg.mp_submod;" + L"print(mp_pkg.mp_submod);" + L"print(sys.modules['mp_pkg.mp_submod']);" + L"print(mp_pkg.mp_submod.greet());" + L"print(f'{mp_pkg.mp_submod.mp_submod_exec_slot_ran=}');" + L"print(f'{mp_pkg.mp_pkg_exec_slot_ran=}');" + }; + PyConfig config; + if (PyImport_AppendInittab("mp_pkg", + &PyInit_mp_pkg) != 0) { + fprintf(stderr, "PyImport_AppendInittab() failed\n"); + return 1; + } + if (PyImport_AppendInittab("mp_pkg.mp_submod", + &PyInit_mp_submod) != 0) { + fprintf(stderr, "PyImport_AppendInittab() failed\n"); + return 1; + } + PyConfig_InitPythonConfig(&config); + config.isolated = 1; + config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv); + init_from_config_clear(&config); + return Py_RunMain(); +} + +/// Single-phase initialization package & submodule /// + +static struct PyModuleDef sp_pkg_def = { + PyModuleDef_HEAD_INIT, + .m_name = "sp_pkg", + .m_size = 0, +}; + +PyMODINIT_FUNC +PyInit_sp_pkg(void) +{ + PyObject *mod = PyModule_Create(&sp_pkg_def); + if (mod == NULL) { + return NULL; + } + // make this a namespace package + PyObject *path_list = PyList_New(0); // empty list = namespace package + if (!path_list) { + Py_DECREF(mod); + return NULL; + } + if (PyModule_AddObject(mod, "__path__", path_list) < 0) { + Py_DECREF(path_list); + Py_DECREF(mod); + return NULL; + } + return mod; +} + +static struct PyModuleDef sp_submod_def = { + PyModuleDef_HEAD_INIT, + .m_name = "sp_pkg.sp_submod", + .m_size = 0, + .m_methods = submod_methods, +}; + +PyMODINIT_FUNC +PyInit_sp_submod(void) +{ + return PyModule_Create(&sp_submod_def); +} + +static int +test_inittab_submodule_singlephase(void) +{ + wchar_t* argv[] = { + PROGRAM_NAME, + L"-c", + L"import sys;" + L"import sp_pkg.sp_submod;" + L"print(sp_pkg.sp_submod);" + L"print(sys.modules['sp_pkg.sp_submod']);" + L"print(sp_pkg.sp_submod.greet());" + }; + PyConfig config; + if (PyImport_AppendInittab("sp_pkg", + &PyInit_sp_pkg) != 0) { + fprintf(stderr, "PyImport_AppendInittab() failed\n"); + return 1; + } + if (PyImport_AppendInittab("sp_pkg.sp_submod", + &PyInit_sp_submod) != 0) { + fprintf(stderr, "PyImport_AppendInittab() failed\n"); + return 1; + } + PyConfig_InitPythonConfig(&config); + config.isolated = 1; + config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv); + init_from_config_clear(&config); + return Py_RunMain(); +} + static void wrap_allocator(PyMemAllocatorEx *allocator); static void unwrap_allocator(PyMemAllocatorEx *allocator); @@ -2507,6 +2688,8 @@ static struct TestCase TestCases[] = { {"test_get_incomplete_frame", test_get_incomplete_frame}, {"test_gilstate_after_finalization", test_gilstate_after_finalization}, {"test_create_module_from_initfunc", test_create_module_from_initfunc}, + {"test_inittab_submodule_multiphase", test_inittab_submodule_multiphase}, + {"test_inittab_submodule_singlephase", test_inittab_submodule_singlephase}, {NULL, NULL} }; From 45b1b9c0d38ba705e67dc3e89f6d37ea74441369 Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Sat, 22 Nov 2025 19:10:53 -0800 Subject: [PATCH 5/7] trim trailing whitespace --- Lib/test/test_embed.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 0346fb81a8c146..b536794122787d 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -546,7 +546,7 @@ def test_getargs_reset_static_parser(self): """) out, err = self.run_embedded_interpreter("test_repeated_init_exec", code) self.assertEqual(out, '1\n2\n3\n' * INIT_LOOPS) - + @staticmethod def _nogil_filtered_err(err: str, mod_name: str) -> str: if not support.Py_GIL_DISABLED: From 36dd0fee478463654cfae40bf539dc182e3951ca Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Tue, 25 Nov 2025 21:53:45 -0800 Subject: [PATCH 6/7] Update Programs/_testembed.c Co-authored-by: Petr Viktorin --- Programs/_testembed.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 1ec6bd985a711d..23ffa3001149ec 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2329,16 +2329,11 @@ int mp_pkg_exec(PyObject *mod) { // make this a namespace package - PyObject *path_list = PyList_New(0); // empty list = namespace package - if (!path_list) { - return -1; - } - if (PyModule_AddObject(mod, "__path__", path_list) < 0) { - Py_DECREF(path_list); + // empty list = namespace package + if (PyModule_Add(mod, "__path__", PyList_New(0)) < 0) { return -1; } if (PyModule_AddStringConstant(mod, "mp_pkg_exec_slot_ran", "yes") < 0) { - Py_DECREF(path_list); return -1; } return 0; From 9d61f7a7831f4b735dbf2e578fb19f952319eb1d Mon Sep 17 00:00:00 2001 From: Itamar Oren Date: Tue, 25 Nov 2025 23:27:15 -0800 Subject: [PATCH 7/7] Update Programs/_testembed.c --- Programs/_testembed.c | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Programs/_testembed.c b/Programs/_testembed.c index 23ffa3001149ec..c6a18249e3ccdd 100644 --- a/Programs/_testembed.c +++ b/Programs/_testembed.c @@ -2443,13 +2443,8 @@ PyInit_sp_pkg(void) return NULL; } // make this a namespace package - PyObject *path_list = PyList_New(0); // empty list = namespace package - if (!path_list) { - Py_DECREF(mod); - return NULL; - } - if (PyModule_AddObject(mod, "__path__", path_list) < 0) { - Py_DECREF(path_list); + // empty list = namespace package + if (PyModule_Add(mod, "__path__", PyList_New(0)) < 0) { Py_DECREF(mod); return NULL; }