Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-104108: Add the Py_mod_multiple_interpreters Module Def Slot #104148

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,16 @@ struct PyModuleDef_Slot {

#define Py_mod_create 1
#define Py_mod_exec 2
#define Py_mod_multiple_interpreters 3

#ifndef Py_LIMITED_API
#define _Py_mod_LAST_SLOT 2
#define _Py_mod_LAST_SLOT 3
#endif

/* for Py_mod_multiple_interpreters: */
#define Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED ((void *)0)
#define Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED ((void *)1)

#endif /* New in 3.5 */

struct PyModuleDef {
Expand Down
82 changes: 61 additions & 21 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1652,26 +1652,44 @@ def pipe(self):
os.set_blocking(r, False)
return (r, w)

def import_script(self, name, fd, check_override=None):
def import_script(self, name, fd, filename=None, check_override=None):
override_text = ''
if check_override is not None:
override_text = f'''
import _imp
_imp._override_multi_interp_extensions_check({check_override})
'''
return textwrap.dedent(f'''
import os, sys
{override_text}
try:
import {name}
except ImportError as exc:
text = 'ImportError: ' + str(exc)
else:
text = 'okay'
os.write({fd}, text.encode('utf-8'))
''')
import _imp
_imp._override_multi_interp_extensions_check({check_override})
'''
if filename:
return textwrap.dedent(f'''
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import ExtensionFileLoader
import os, sys
{override_text}
loader = ExtensionFileLoader({name!r}, {filename!r})
spec = spec_from_loader({name!r}, loader)
try:
module = module_from_spec(spec)
loader.exec_module(module)
except ImportError as exc:
text = 'ImportError: ' + str(exc)
else:
text = 'okay'
os.write({fd}, text.encode('utf-8'))
''')
else:
return textwrap.dedent(f'''
import os, sys
{override_text}
try:
import {name}
except ImportError as exc:
text = 'ImportError: ' + str(exc)
else:
text = 'okay'
os.write({fd}, text.encode('utf-8'))
''')

def run_here(self, name, *,
def run_here(self, name, filename=None, *,
check_singlephase_setting=False,
check_singlephase_override=None,
isolated=False,
Expand Down Expand Up @@ -1700,26 +1718,30 @@ def run_here(self, name, *,
)

r, w = self.pipe()
script = self.import_script(name, w, check_singlephase_override)
script = self.import_script(name, w, filename,
check_singlephase_override)

ret = run_in_subinterp_with_config(script, **kwargs)
self.assertEqual(ret, 0)
return os.read(r, 100)

def check_compatible_here(self, name, *, strict=False, isolated=False):
def check_compatible_here(self, name, filename=None, *,
strict=False,
isolated=False,
):
# Verify that the named module may be imported in a subinterpreter.
# (See run_here() for more info.)
out = self.run_here(name,
out = self.run_here(name, filename,
check_singlephase_setting=strict,
isolated=isolated,
)
self.assertEqual(out, b'okay')

def check_incompatible_here(self, name, *, isolated=False):
def check_incompatible_here(self, name, filename=None, *, isolated=False):
# Differences from check_compatible_here():
# * verify that import fails
# * "strict" is always True
out = self.run_here(name,
out = self.run_here(name, filename,
check_singlephase_setting=True,
isolated=isolated,
)
Expand Down Expand Up @@ -1820,6 +1842,24 @@ def test_multi_init_extension_compat(self):
with self.subTest(f'{module}: strict, fresh'):
self.check_compatible_fresh(module, strict=True)

@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
def test_multi_init_extension_non_isolated_compat(self):
modname = '_test_non_isolated'
filename = _testmultiphase.__file__
loader = ExtensionFileLoader(modname, filename)
spec = importlib.util.spec_from_loader(modname, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
sys.modules[modname] = module

require_extension(module)
with self.subTest(f'{modname}: isolated'):
self.check_incompatible_here(modname, filename, isolated=True)
with self.subTest(f'{modname}: not isolated'):
self.check_incompatible_here(modname, filename, isolated=False)
with self.subTest(f'{modname}: not strict'):
self.check_compatible_here(modname, filename, strict=False)

def test_python_compat(self):
module = 'threading'
require_pure_python(module)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Multi-phase init extension modules may now indicate whether or not they
actually support multiple interpreters. By default such modules are
expected to support use in multiple interpreters. In the uncommon case that
one does not, it may use the new ``Py_mod_multiple_interpreters`` module def
slot. A value of ``0`` means the module does not support them. ``1`` means
it does. The default is ``1``.
19 changes: 19 additions & 0 deletions Modules/_testmultiphase.c
Original file line number Diff line number Diff line change
Expand Up @@ -884,3 +884,22 @@ PyInit__test_module_state_shared(void)
}
return module;
}


/* multiple interpreters supports */

static PyModuleDef_Slot non_isolated_slots[] = {
{Py_mod_exec, execfunc},
{Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
{0, NULL},
};

static PyModuleDef non_isolated_def = TEST_MODULE_DEF("_test_non_isolated",
non_isolated_slots,
testexport_methods);

PyMODINIT_FUNC
PyInit__test_non_isolated(void)
{
return PyModuleDef_Init(&non_isolated_def);
}
30 changes: 30 additions & 0 deletions Objects/moduleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio
PyObject *(*create)(PyObject *, PyModuleDef*) = NULL;
PyObject *nameobj;
PyObject *m = NULL;
int has_multiple_interpreters_slot = 0;
void *multiple_interpreters = (void *)0;
int has_execution_slots = 0;
const char *name;
int ret;
Expand Down Expand Up @@ -287,6 +289,17 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio
case Py_mod_exec:
has_execution_slots = 1;
break;
case Py_mod_multiple_interpreters:
if (has_multiple_interpreters_slot) {
PyErr_Format(
PyExc_SystemError,
"module %s has more than one 'multiple interpreters' slots",
name);
goto error;
}
multiple_interpreters = cur_slot->value;
has_multiple_interpreters_slot = 1;
break;
default:
assert(cur_slot->slot < 0 || cur_slot->slot > _Py_mod_LAST_SLOT);
PyErr_Format(
Expand All @@ -297,6 +310,20 @@ PyModule_FromDefAndSpec2(PyModuleDef* def, PyObject *spec, int module_api_versio
}
}

/* By default, multi-phase init modules are expected
to work under multiple interpreters. */
if (!has_multiple_interpreters_slot) {
multiple_interpreters = Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED;
}
if (multiple_interpreters == Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED) {
PyInterpreterState *interp = _PyInterpreterState_GET();
if (!_Py_IsMainInterpreter(interp)
&& _PyImport_CheckSubinterpIncompatibleExtensionAllowed(name) < 0)
{
goto error;
}
}

if (create) {
m = create(spec, def);
if (m == NULL) {
Expand Down Expand Up @@ -421,6 +448,9 @@ PyModule_ExecDef(PyObject *module, PyModuleDef *def)
return -1;
}
break;
case Py_mod_multiple_interpreters:
/* handled in PyModule_FromDefAndSpec2 */
break;
default:
PyErr_Format(
PyExc_SystemError,
Expand Down