Skip to content

Commit a805803

Browse files
itamaroclaude
andcommitted
gh-157384: Accept non-ASCII names in PyImport_CreateModuleFromInitfunc()
The builtin loader info encoded the module name as ASCII, so a non-ASCII spec name failed with UnicodeEncodeError even for multi-phase init modules, which support such names when loaded dynamically. Fall back to UTF-8 and mark the name as non-ASCII, so that multi-phase init works and single-phase init is rejected with the same error as for dynamically loaded extensions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HoZE3WdRWQshvinfBZ137N
1 parent 2cd6d4b commit a805803

4 files changed

Lines changed: 80 additions & 3 deletions

File tree

Lib/test/test_embed.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,9 @@ def test_create_module_from_initfunc(self):
278278
"my_test_extension.exec_slot_ran='yes'\n"
279279
"<module 'embedded_ext' (static-extension)>\n"
280280
"embedded_ext.executed='yes'\n"
281+
"ascii(mp.__name__)=\"'m\\\\xf6dul_mp'\" mp.executed='yes'\n"
282+
"SystemError: 'initialization of m\\xf6dul_sp "
283+
"did not return PyModuleDef'\n"
281284
)
282285

283286
def test_inittab_submodule_multiphase(self):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:c:func:`PyImport_CreateModuleFromInitfunc` now accepts non-ASCII module
2+
names for multi-phase init modules, instead of raising
3+
:exc:`UnicodeEncodeError`.

Programs/_testembed.c

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2410,6 +2410,43 @@ static int test_repeated_init_and_inittab(void)
24102410
return 0;
24112411
}
24122412

2413+
// Modules with non-ASCII names: multi-phase init is supported,
2414+
// single-phase init is not.
2415+
static PyModuleDef_Slot cmfi_nonascii_mp_slots[] = {
2416+
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
2417+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
2418+
{0, NULL},
2419+
};
2420+
2421+
static PyModuleDef cmfi_nonascii_mp_def = {
2422+
PyModuleDef_HEAD_INIT,
2423+
.m_name = "nonascii_mp",
2424+
.m_size = 0,
2425+
.m_slots = cmfi_nonascii_mp_slots,
2426+
};
2427+
2428+
static PyObject*
2429+
PyInit_cmfi_nonascii_mp(void)
2430+
{
2431+
return PyModuleDef_Init(&cmfi_nonascii_mp_def);
2432+
}
2433+
2434+
static PyModuleDef cmfi_nonascii_sp_def = {
2435+
PyModuleDef_HEAD_INIT,
2436+
.m_name = "nonascii_sp",
2437+
.m_size = -1,
2438+
};
2439+
2440+
static PyObject*
2441+
PyInit_cmfi_nonascii_sp(void)
2442+
{
2443+
return PyModule_Create(&cmfi_nonascii_sp_def);
2444+
}
2445+
2446+
// "m\xf6dul_mp" and "m\xf6dul_sp" as UTF-8
2447+
#define CMFI_NONASCII_MP_NAME "m\xc3\xb6" "dul_mp"
2448+
#define CMFI_NONASCII_SP_NAME "m\xc3\xb6" "dul_sp"
2449+
24132450
static PyObject*
24142451
create_module(PyObject* self, PyObject* spec)
24152452
{
@@ -2425,6 +2462,14 @@ create_module(PyObject* self, PyObject* spec)
24252462
Py_DECREF(name);
24262463
return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext);
24272464
}
2465+
if (PyUnicode_EqualToUTF8(name, CMFI_NONASCII_MP_NAME)) {
2466+
Py_DECREF(name);
2467+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_nonascii_mp);
2468+
}
2469+
if (PyUnicode_EqualToUTF8(name, CMFI_NONASCII_SP_NAME)) {
2470+
Py_DECREF(name);
2471+
return PyImport_CreateModuleFromInitfunc(spec, PyInit_cmfi_nonascii_sp);
2472+
}
24282473
PyErr_Format(PyExc_LookupError, "static module %R not found", name);
24292474
Py_DECREF(name);
24302475
return NULL;
@@ -2472,6 +2517,11 @@ test_create_module_from_initfunc(void)
24722517
L"import embedded_ext;"
24732518
L"print(embedded_ext);"
24742519
L"print(f'{embedded_ext.executed=}');"
2520+
// Non-ASCII names: multi-phase init works, single-phase init doesn't
2521+
L"import importlib;"
2522+
L"mp = importlib.import_module('m\\xf6dul_mp');"
2523+
L"print(f'{ascii(mp.__name__)=} {mp.executed=}');"
2524+
L"try_import('m\\xf6dul_sp');"
24752525
};
24762526
PyConfig config;
24772527
if (PyImport_AppendInittab("create_static_module",
@@ -2491,7 +2541,8 @@ test_create_module_from_initfunc(void)
24912541
" _ORIGIN = \"static-extension\"\n"
24922542
" @classmethod\n"
24932543
" def find_spec(cls, fullname, path, target=None):\n"
2494-
" if fullname in {'my_test_extension', 'embedded_ext'}:\n"
2544+
" if fullname in {'my_test_extension', 'embedded_ext',\n"
2545+
" 'm\\xf6dul_mp', 'm\\xf6dul_sp'}:\n"
24952546
" return spec_from_loader(fullname, cls, origin=cls._ORIGIN)\n"
24962547
" return None\n"
24972548
" @staticmethod\n"
@@ -2502,6 +2553,13 @@ test_create_module_from_initfunc(void)
25022553
" create_static_module.exec_module(module)\n"
25032554
" module.executed = 'yes'\n"
25042555
"sys.meta_path.append(StaticExtensionImporter)\n"
2556+
"def try_import(name):\n"
2557+
" try:\n"
2558+
" importlib.import_module(name)\n"
2559+
" except SystemError as exc:\n"
2560+
" print(f'SystemError: {ascii(str(exc))}')\n"
2561+
" else:\n"
2562+
" print(f'no SystemError for {ascii(name)}!')\n"
25052563
);
25062564
if (result < 0) {
25072565
fprintf(stderr, "PyRun_SimpleString() failed\n");

Python/importdl.c

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,22 @@ _Py_ext_module_loader_info_init_for_builtin(
158158
assert(PyUnicode_Check(name));
159159
assert(PyUnicode_GetLength(name) > 0);
160160

161+
/* The encoded name is only used for error messages, so unlike
162+
* get_encoded_name() we keep the full dotted name. Non-ASCII names
163+
* are allowed, but only for multi-phase init modules; hook_prefixes
164+
* records which case we are in. */
165+
const struct hook_prefixes *hook_prefixes = &ascii_only_prefixes;
161166
PyObject *name_encoded = PyUnicode_AsEncodedString(name, "ascii", NULL);
162167
if (name_encoded == NULL) {
163-
return -1;
168+
if (!PyErr_ExceptionMatches(PyExc_UnicodeEncodeError)) {
169+
return -1;
170+
}
171+
PyErr_Clear();
172+
name_encoded = PyUnicode_AsUTF8String(name);
173+
if (name_encoded == NULL) {
174+
return -1;
175+
}
176+
hook_prefixes = &nonascii_prefixes;
164177
}
165178

166179
*info = (struct _Py_ext_module_loader_info){
@@ -169,7 +182,7 @@ _Py_ext_module_loader_info_init_for_builtin(
169182
/* We won't need filename. */
170183
.path=name,
171184
.origin=_Py_ext_module_origin_BUILTIN,
172-
.hook_prefixes=&ascii_only_prefixes,
185+
.hook_prefixes=hook_prefixes,
173186
.newcontext=NULL,
174187
};
175188
return 0;

0 commit comments

Comments
 (0)