gh-117649: Raise ImportError for unsupported modules in free-threaded build (#117651)

The free-threaded build does not currently support the combination of
single-phase init modules and non-isolated subinterpreters. Ensure that
`check_multi_interp_extensions` is always `True` for subinterpreters in
the free-threaded build so that importing these modules raises an
`ImportError`.
This commit is contained in:
Sam Gross 2024-04-11 15:00:54 -04:00 committed by GitHub
parent 39d381f91e
commit 25f6ff5d3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 103 additions and 32 deletions

View file

@ -63,6 +63,15 @@ typedef struct {
.gil = PyInterpreterConfig_OWN_GIL, \ .gil = PyInterpreterConfig_OWN_GIL, \
} }
// gh-117649: The free-threaded build does not currently support single-phase
// init extensions in subinterpreters. For now, we ensure that
// `check_multi_interp_extensions` is always `1`, even in the legacy config.
#ifdef Py_GIL_DISABLED
# define _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS 1
#else
# define _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS 0
#endif
#define _PyInterpreterConfig_LEGACY_INIT \ #define _PyInterpreterConfig_LEGACY_INIT \
{ \ { \
.use_main_obmalloc = 1, \ .use_main_obmalloc = 1, \
@ -70,7 +79,7 @@ typedef struct {
.allow_exec = 1, \ .allow_exec = 1, \
.allow_threads = 1, \ .allow_threads = 1, \
.allow_daemon_threads = 1, \ .allow_daemon_threads = 1, \
.check_multi_interp_extensions = 0, \ .check_multi_interp_extensions = _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS, \
.gil = PyInterpreterConfig_SHARED_GIL, \ .gil = PyInterpreterConfig_SHARED_GIL, \
} }

View file

@ -842,6 +842,12 @@ def requires_gil_enabled(msg="needs the GIL enabled"):
"""Decorator for skipping tests on the free-threaded build.""" """Decorator for skipping tests on the free-threaded build."""
return unittest.skipIf(Py_GIL_DISABLED, msg) return unittest.skipIf(Py_GIL_DISABLED, msg)
def expected_failure_if_gil_disabled():
"""Expect test failure if the GIL is disabled."""
if Py_GIL_DISABLED:
return unittest.expectedFailure
return lambda test_case: test_case
if Py_GIL_DISABLED: if Py_GIL_DISABLED:
_header = 'PHBBInP' _header = 'PHBBInP'
else: else:

View file

@ -26,6 +26,8 @@ from test.support import import_helper
from test.support import threading_helper from test.support import threading_helper
from test.support import warnings_helper from test.support import warnings_helper
from test.support import requires_limited_api from test.support import requires_limited_api
from test.support import requires_gil_enabled, expected_failure_if_gil_disabled
from test.support import Py_GIL_DISABLED
from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end
try: try:
import _posixsubprocess import _posixsubprocess
@ -2023,15 +2025,30 @@ class SubinterpreterTest(unittest.TestCase):
kwlist[-2] = 'check_multi_interp_extensions' kwlist[-2] = 'check_multi_interp_extensions'
kwlist[-1] = 'own_gil' kwlist[-1] = 'own_gil'
# expected to work expected_to_work = {
for config, expected in {
(True, True, True, True, True, True, True): (True, True, True, True, True, True, True):
(ALL_FLAGS, True), (ALL_FLAGS, True),
(True, False, False, False, False, False, False): (True, False, False, False, False, False, False):
(OBMALLOC, False), (OBMALLOC, False),
(False, False, False, True, False, True, False): (False, False, False, True, False, True, False):
(THREADS | EXTENSIONS, False), (THREADS | EXTENSIONS, False),
}.items(): }
expected_to_fail = {
(False, False, False, False, False, False, False),
}
# gh-117649: The free-threaded build does not currently allow
# setting check_multi_interp_extensions to False.
if Py_GIL_DISABLED:
for config in list(expected_to_work.keys()):
kwargs = dict(zip(kwlist, config))
if not kwargs['check_multi_interp_extensions']:
del expected_to_work[config]
expected_to_fail.add(config)
# expected to work
for config, expected in expected_to_work.items():
kwargs = dict(zip(kwlist, config)) kwargs = dict(zip(kwlist, config))
exp_flags, exp_gil = expected exp_flags, exp_gil = expected
expected = { expected = {
@ -2055,9 +2072,7 @@ class SubinterpreterTest(unittest.TestCase):
self.assertEqual(settings, expected) self.assertEqual(settings, expected)
# expected to fail # expected to fail
for config in [ for config in expected_to_fail:
(False, False, False, False, False, False, False),
]:
kwargs = dict(zip(kwlist, config)) kwargs = dict(zip(kwlist, config))
with self.subTest(config): with self.subTest(config):
script = textwrap.dedent(f''' script = textwrap.dedent(f'''
@ -2070,6 +2085,9 @@ class SubinterpreterTest(unittest.TestCase):
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module") @unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()") @unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
# gh-117649: The free-threaded build does not currently allow overriding
# the check_multi_interp_extensions setting.
@expected_failure_if_gil_disabled()
def test_overridden_setting_extensions_subinterp_check(self): def test_overridden_setting_extensions_subinterp_check(self):
""" """
PyInterpreterConfig.check_multi_interp_extensions can be overridden PyInterpreterConfig.check_multi_interp_extensions can be overridden
@ -2165,6 +2183,9 @@ class SubinterpreterTest(unittest.TestCase):
self.assertFalse(hasattr(binascii.Error, "foobar")) self.assertFalse(hasattr(binascii.Error, "foobar"))
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
# gh-117649: The free-threaded build does not currently support sharing
# extension module state between interpreters.
@expected_failure_if_gil_disabled()
def test_module_state_shared_in_global(self): def test_module_state_shared_in_global(self):
""" """
bpo-44050: Extension module state should be shared between interpreters bpo-44050: Extension module state should be shared between interpreters
@ -2223,7 +2244,7 @@ class InterpreterConfigTests(unittest.TestCase):
allow_exec=True, allow_exec=True,
allow_threads=True, allow_threads=True,
allow_daemon_threads=True, allow_daemon_threads=True,
check_multi_interp_extensions=False, check_multi_interp_extensions=bool(Py_GIL_DISABLED),
gil='shared', gil='shared',
), ),
'empty': types.SimpleNamespace( 'empty': types.SimpleNamespace(
@ -2386,6 +2407,8 @@ class InterpreterConfigTests(unittest.TestCase):
check_multi_interp_extensions=False check_multi_interp_extensions=False
), ),
] ]
if Py_GIL_DISABLED:
invalid.append(dict(check_multi_interp_extensions=False))
def match(config, override_cases): def match(config, override_cases):
ns = vars(config) ns = vars(config)
for overrides in override_cases: for overrides in override_cases:
@ -2427,6 +2450,8 @@ class InterpreterConfigTests(unittest.TestCase):
with self.subTest('main'): with self.subTest('main'):
expected = _interpreters.new_config('legacy') expected = _interpreters.new_config('legacy')
expected.gil = 'own' expected.gil = 'own'
if Py_GIL_DISABLED:
expected.check_multi_interp_extensions = False
interpid, *_ = _interpreters.get_main() interpid, *_ = _interpreters.get_main()
config = _interpreters.get_config(interpid) config = _interpreters.get_config(interpid)
self.assert_ns_equal(config, expected) self.assert_ns_equal(config, expected)
@ -2448,6 +2473,7 @@ class InterpreterConfigTests(unittest.TestCase):
'empty', 'empty',
use_main_obmalloc=True, use_main_obmalloc=True,
gil='shared', gil='shared',
check_multi_interp_extensions=bool(Py_GIL_DISABLED),
) )
with new_interp(orig) as interpid: with new_interp(orig) as interpid:
config = _interpreters.get_config(interpid) config = _interpreters.get_config(interpid)

View file

@ -30,7 +30,8 @@ import _imp
from test.support import os_helper from test.support import os_helper
from test.support import ( from test.support import (
STDLIB_DIR, swap_attr, swap_item, cpython_only, is_apple_mobile, is_emscripten, STDLIB_DIR, swap_attr, swap_item, cpython_only, is_apple_mobile, is_emscripten,
is_wasi, run_in_subinterp, run_in_subinterp_with_config, Py_TRACE_REFS) is_wasi, run_in_subinterp, run_in_subinterp_with_config, Py_TRACE_REFS,
requires_gil_enabled, Py_GIL_DISABLED)
from test.support.import_helper import ( from test.support.import_helper import (
forget, make_legacy_pyc, unlink, unload, ready_to_import, forget, make_legacy_pyc, unlink, unload, ready_to_import,
DirsOnSysPath, CleanImport, import_module) DirsOnSysPath, CleanImport, import_module)
@ -158,6 +159,9 @@ def requires_singlephase_init(meth):
finally: finally:
restore__testsinglephase() restore__testsinglephase()
meth = cpython_only(meth) meth = cpython_only(meth)
# gh-117649: free-threaded build does not currently support single-phase
# init modules in subinterpreters.
meth = requires_gil_enabled(meth)
return unittest.skipIf(_testsinglephase is None, return unittest.skipIf(_testsinglephase is None,
'test requires _testsinglephase module')(meth) 'test requires _testsinglephase module')(meth)
@ -1876,8 +1880,9 @@ class SubinterpImportTests(unittest.TestCase):
# since they still don't implement multi-phase init. # since they still don't implement multi-phase init.
module = '_imp' module = '_imp'
require_builtin(module) require_builtin(module)
with self.subTest(f'{module}: not strict'): if not Py_GIL_DISABLED:
self.check_compatible_here(module, strict=False) with self.subTest(f'{module}: not strict'):
self.check_compatible_here(module, strict=False)
with self.subTest(f'{module}: strict, not fresh'): with self.subTest(f'{module}: strict, not fresh'):
self.check_compatible_here(module, strict=True) self.check_compatible_here(module, strict=True)
@ -1888,8 +1893,9 @@ class SubinterpImportTests(unittest.TestCase):
require_frozen(module, skip=True) require_frozen(module, skip=True)
if __import__(module).__spec__.origin != 'frozen': if __import__(module).__spec__.origin != 'frozen':
raise unittest.SkipTest(f'{module} is unexpectedly not frozen') raise unittest.SkipTest(f'{module} is unexpectedly not frozen')
with self.subTest(f'{module}: not strict'): if not Py_GIL_DISABLED:
self.check_compatible_here(module, strict=False) with self.subTest(f'{module}: not strict'):
self.check_compatible_here(module, strict=False)
with self.subTest(f'{module}: strict, not fresh'): with self.subTest(f'{module}: strict, not fresh'):
self.check_compatible_here(module, strict=True) self.check_compatible_here(module, strict=True)
@ -1908,8 +1914,9 @@ class SubinterpImportTests(unittest.TestCase):
def test_multi_init_extension_compat(self): def test_multi_init_extension_compat(self):
module = '_testmultiphase' module = '_testmultiphase'
require_extension(module) require_extension(module)
with self.subTest(f'{module}: not strict'): if not Py_GIL_DISABLED:
self.check_compatible_here(module, strict=False) with self.subTest(f'{module}: not strict'):
self.check_compatible_here(module, strict=False)
with self.subTest(f'{module}: strict, not fresh'): with self.subTest(f'{module}: strict, not fresh'):
self.check_compatible_here(module, strict=True) self.check_compatible_here(module, strict=True)
with self.subTest(f'{module}: strict, fresh'): with self.subTest(f'{module}: strict, fresh'):
@ -1930,8 +1937,9 @@ class SubinterpImportTests(unittest.TestCase):
self.check_incompatible_here(modname, filename, isolated=True) self.check_incompatible_here(modname, filename, isolated=True)
with self.subTest(f'{modname}: not isolated'): with self.subTest(f'{modname}: not isolated'):
self.check_incompatible_here(modname, filename, isolated=False) self.check_incompatible_here(modname, filename, isolated=False)
with self.subTest(f'{modname}: not strict'): if not Py_GIL_DISABLED:
self.check_compatible_here(modname, filename, strict=False) with self.subTest(f'{modname}: not strict'):
self.check_compatible_here(modname, filename, strict=False)
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
def test_multi_init_extension_per_interpreter_gil_compat(self): def test_multi_init_extension_per_interpreter_gil_compat(self):
@ -1949,16 +1957,18 @@ class SubinterpImportTests(unittest.TestCase):
with self.subTest(f'{modname}: not isolated, strict'): with self.subTest(f'{modname}: not isolated, strict'):
self.check_compatible_here(modname, filename, self.check_compatible_here(modname, filename,
strict=True, isolated=False) strict=True, isolated=False)
with self.subTest(f'{modname}: not isolated, not strict'): if not Py_GIL_DISABLED:
self.check_compatible_here(modname, filename, with self.subTest(f'{modname}: not isolated, not strict'):
strict=False, isolated=False) self.check_compatible_here(modname, filename,
strict=False, isolated=False)
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
def test_python_compat(self): def test_python_compat(self):
module = 'threading' module = 'threading'
require_pure_python(module) require_pure_python(module)
with self.subTest(f'{module}: not strict'): if not Py_GIL_DISABLED:
self.check_compatible_here(module, strict=False) with self.subTest(f'{module}: not strict'):
self.check_compatible_here(module, strict=False)
with self.subTest(f'{module}: strict, not fresh'): with self.subTest(f'{module}: strict, not fresh'):
self.check_compatible_here(module, strict=True) self.check_compatible_here(module, strict=True)
with self.subTest(f'{module}: strict, fresh'): with self.subTest(f'{module}: strict, fresh'):

View file

@ -682,6 +682,9 @@ class IncompatibleExtensionModuleRestrictionsTests(unittest.TestCase):
raise ImportError(excsnap.msg) raise ImportError(excsnap.msg)
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module") @unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
# gh-117649: single-phase init modules are not currently supported in
# subinterpreters in the free-threaded build
@support.expected_failure_if_gil_disabled()
def test_single_phase_init_module(self): def test_single_phase_init_module(self):
script = textwrap.dedent(''' script = textwrap.dedent('''
from importlib.util import _incompatible_extension_module_restrictions from importlib.util import _incompatible_extension_module_restrictions
@ -706,6 +709,7 @@ class IncompatibleExtensionModuleRestrictionsTests(unittest.TestCase):
self.run_with_own_gil(script) self.run_with_own_gil(script)
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module") @unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
@support.requires_gil_enabled("gh-117649: not supported in free-threaded build")
def test_incomplete_multi_phase_init_module(self): def test_incomplete_multi_phase_init_module(self):
# Apple extensions must be distributed as frameworks. This requires # Apple extensions must be distributed as frameworks. This requires
# a specialist loader. # a specialist loader.

View file

@ -10,6 +10,7 @@ from test import support
from test.support import import_helper from test.support import import_helper
# Raise SkipTest if subinterpreters not supported. # Raise SkipTest if subinterpreters not supported.
_interpreters = import_helper.import_module('_xxsubinterpreters') _interpreters = import_helper.import_module('_xxsubinterpreters')
from test.support import Py_GIL_DISABLED
from test.support import interpreters from test.support import interpreters
from test.support.interpreters import ( from test.support.interpreters import (
InterpreterError, InterpreterNotFoundError, ExecutionFailed, InterpreterError, InterpreterNotFoundError, ExecutionFailed,
@ -1162,7 +1163,7 @@ class LowLevelTests(TestBase):
allow_exec=True, allow_exec=True,
allow_threads=True, allow_threads=True,
allow_daemon_threads=True, allow_daemon_threads=True,
check_multi_interp_extensions=False, check_multi_interp_extensions=bool(Py_GIL_DISABLED),
gil='shared', gil='shared',
), ),
'empty': types.SimpleNamespace( 'empty': types.SimpleNamespace(
@ -1361,6 +1362,7 @@ class LowLevelTests(TestBase):
with self.subTest('custom'): with self.subTest('custom'):
orig = _interpreters.new_config('empty') orig = _interpreters.new_config('empty')
orig.use_main_obmalloc = True orig.use_main_obmalloc = True
orig.check_multi_interp_extensions = bool(Py_GIL_DISABLED)
orig.gil = 'shared' orig.gil = 'shared'
interpid = _interpreters.create(orig) interpid = _interpreters.create(orig)
config = _interpreters.get_config(interpid) config = _interpreters.get_config(interpid)
@ -1410,13 +1412,8 @@ class LowLevelTests(TestBase):
with self.subTest('main'): with self.subTest('main'):
expected = _interpreters.new_config('legacy') expected = _interpreters.new_config('legacy')
expected.gil = 'own' expected.gil = 'own'
interpid, *_ = _interpreters.get_main() if Py_GIL_DISABLED:
config = _interpreters.get_config(interpid) expected.check_multi_interp_extensions = False
self.assert_ns_equal(config, expected)
with self.subTest('main'):
expected = _interpreters.new_config('legacy')
expected.gil = 'own'
interpid, *_ = _interpreters.get_main() interpid, *_ = _interpreters.get_main()
config = _interpreters.get_config(interpid) config = _interpreters.get_config(interpid)
self.assert_ns_equal(config, expected) self.assert_ns_equal(config, expected)

View file

@ -1527,6 +1527,7 @@ class SubinterpThreadingTests(BaseTestCase):
{before_start} {before_start}
t.start() t.start()
""") """)
check_multi_interp_extensions = bool(support.Py_GIL_DISABLED)
script = textwrap.dedent(f""" script = textwrap.dedent(f"""
import test.support import test.support
test.support.run_in_subinterp_with_config( test.support.run_in_subinterp_with_config(
@ -1536,7 +1537,7 @@ class SubinterpThreadingTests(BaseTestCase):
allow_exec=True, allow_exec=True,
allow_threads={allowed}, allow_threads={allowed},
allow_daemon_threads={daemon_allowed}, allow_daemon_threads={daemon_allowed},
check_multi_interp_extensions=False, check_multi_interp_extensions={check_multi_interp_extensions},
own_gil=False, own_gil=False,
) )
""") """)

View file

@ -3696,9 +3696,16 @@ _imp__override_multi_interp_extensions_check_impl(PyObject *module,
"cannot be used in the main interpreter"); "cannot be used in the main interpreter");
return NULL; return NULL;
} }
#ifdef Py_GIL_DISABLED
PyErr_SetString(PyExc_RuntimeError,
"_imp._override_multi_interp_extensions_check() "
"cannot be used in the free-threaded build");
return NULL;
#else
int oldvalue = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp); int oldvalue = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp);
OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) = override; OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) = override;
return PyLong_FromLong(oldvalue); return PyLong_FromLong(oldvalue);
#endif
} }
#ifdef HAVE_DYNAMIC_LOADING #ifdef HAVE_DYNAMIC_LOADING

View file

@ -559,6 +559,15 @@ init_interp_settings(PyInterpreterState *interp,
return _PyStatus_ERR("per-interpreter obmalloc does not support " return _PyStatus_ERR("per-interpreter obmalloc does not support "
"single-phase init extension modules"); "single-phase init extension modules");
} }
#ifdef Py_GIL_DISABLED
if (!_Py_IsMainInterpreter(interp) &&
!config->check_multi_interp_extensions)
{
return _PyStatus_ERR("The free-threaded build does not support "
"single-phase init extension modules in "
"subinterpreters");
}
#endif
if (config->allow_fork) { if (config->allow_fork) {
interp->feature_flags |= Py_RTFLAGS_FORK; interp->feature_flags |= Py_RTFLAGS_FORK;
@ -647,8 +656,10 @@ pycore_create_interpreter(_PyRuntimeState *runtime,
} }
PyInterpreterConfig config = _PyInterpreterConfig_LEGACY_INIT; PyInterpreterConfig config = _PyInterpreterConfig_LEGACY_INIT;
// The main interpreter always has its own GIL. // The main interpreter always has its own GIL and supports single-phase
// init extensions.
config.gil = PyInterpreterConfig_OWN_GIL; config.gil = PyInterpreterConfig_OWN_GIL;
config.check_multi_interp_extensions = 0;
status = init_interp_settings(interp, &config); status = init_interp_settings(interp, &config);
if (_PyStatus_EXCEPTION(status)) { if (_PyStatus_EXCEPTION(status)) {
return status; return status;