Skip to content

Commit 25f6ff5

Browse files
authored
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`.
1 parent 39d381f commit 25f6ff5

File tree

9 files changed

+103
-32
lines changed

9 files changed

+103
-32
lines changed

Include/cpython/pylifecycle.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,23 @@ typedef struct {
6363
.gil = PyInterpreterConfig_OWN_GIL, \
6464
}
6565

66+
// gh-117649: The free-threaded build does not currently support single-phase
67+
// init extensions in subinterpreters. For now, we ensure that
68+
// `check_multi_interp_extensions` is always `1`, even in the legacy config.
69+
#ifdef Py_GIL_DISABLED
70+
# define _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS 1
71+
#else
72+
# define _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS 0
73+
#endif
74+
6675
#define _PyInterpreterConfig_LEGACY_INIT \
6776
{ \
6877
.use_main_obmalloc = 1, \
6978
.allow_fork = 1, \
7079
.allow_exec = 1, \
7180
.allow_threads = 1, \
7281
.allow_daemon_threads = 1, \
73-
.check_multi_interp_extensions = 0, \
82+
.check_multi_interp_extensions = _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS, \
7483
.gil = PyInterpreterConfig_SHARED_GIL, \
7584
}
7685

Lib/test/support/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,12 @@ def requires_gil_enabled(msg="needs the GIL enabled"):
842842
"""Decorator for skipping tests on the free-threaded build."""
843843
return unittest.skipIf(Py_GIL_DISABLED, msg)
844844

845+
def expected_failure_if_gil_disabled():
846+
"""Expect test failure if the GIL is disabled."""
847+
if Py_GIL_DISABLED:
848+
return unittest.expectedFailure
849+
return lambda test_case: test_case
850+
845851
if Py_GIL_DISABLED:
846852
_header = 'PHBBInP'
847853
else:

Lib/test/test_capi/test_misc.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from test.support import threading_helper
2727
from test.support import warnings_helper
2828
from test.support import requires_limited_api
29+
from test.support import requires_gil_enabled, expected_failure_if_gil_disabled
30+
from test.support import Py_GIL_DISABLED
2931
from test.support.script_helper import assert_python_failure, assert_python_ok, run_python_until_end
3032
try:
3133
import _posixsubprocess
@@ -2023,15 +2025,30 @@ def test_configured_settings(self):
20232025
kwlist[-2] = 'check_multi_interp_extensions'
20242026
kwlist[-1] = 'own_gil'
20252027

2026-
# expected to work
2027-
for config, expected in {
2028+
expected_to_work = {
20282029
(True, True, True, True, True, True, True):
20292030
(ALL_FLAGS, True),
20302031
(True, False, False, False, False, False, False):
20312032
(OBMALLOC, False),
20322033
(False, False, False, True, False, True, False):
20332034
(THREADS | EXTENSIONS, False),
2034-
}.items():
2035+
}
2036+
2037+
expected_to_fail = {
2038+
(False, False, False, False, False, False, False),
2039+
}
2040+
2041+
# gh-117649: The free-threaded build does not currently allow
2042+
# setting check_multi_interp_extensions to False.
2043+
if Py_GIL_DISABLED:
2044+
for config in list(expected_to_work.keys()):
2045+
kwargs = dict(zip(kwlist, config))
2046+
if not kwargs['check_multi_interp_extensions']:
2047+
del expected_to_work[config]
2048+
expected_to_fail.add(config)
2049+
2050+
# expected to work
2051+
for config, expected in expected_to_work.items():
20352052
kwargs = dict(zip(kwlist, config))
20362053
exp_flags, exp_gil = expected
20372054
expected = {
@@ -2055,9 +2072,7 @@ def test_configured_settings(self):
20552072
self.assertEqual(settings, expected)
20562073

20572074
# expected to fail
2058-
for config in [
2059-
(False, False, False, False, False, False, False),
2060-
]:
2075+
for config in expected_to_fail:
20612076
kwargs = dict(zip(kwlist, config))
20622077
with self.subTest(config):
20632078
script = textwrap.dedent(f'''
@@ -2070,6 +2085,9 @@ def test_configured_settings(self):
20702085

20712086
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
20722087
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
2088+
# gh-117649: The free-threaded build does not currently allow overriding
2089+
# the check_multi_interp_extensions setting.
2090+
@expected_failure_if_gil_disabled()
20732091
def test_overridden_setting_extensions_subinterp_check(self):
20742092
"""
20752093
PyInterpreterConfig.check_multi_interp_extensions can be overridden
@@ -2165,6 +2183,9 @@ def test_mutate_exception(self):
21652183
self.assertFalse(hasattr(binascii.Error, "foobar"))
21662184

21672185
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
2186+
# gh-117649: The free-threaded build does not currently support sharing
2187+
# extension module state between interpreters.
2188+
@expected_failure_if_gil_disabled()
21682189
def test_module_state_shared_in_global(self):
21692190
"""
21702191
bpo-44050: Extension module state should be shared between interpreters
@@ -2223,7 +2244,7 @@ class InterpreterConfigTests(unittest.TestCase):
22232244
allow_exec=True,
22242245
allow_threads=True,
22252246
allow_daemon_threads=True,
2226-
check_multi_interp_extensions=False,
2247+
check_multi_interp_extensions=bool(Py_GIL_DISABLED),
22272248
gil='shared',
22282249
),
22292250
'empty': types.SimpleNamespace(
@@ -2386,6 +2407,8 @@ def test_interp_init(self):
23862407
check_multi_interp_extensions=False
23872408
),
23882409
]
2410+
if Py_GIL_DISABLED:
2411+
invalid.append(dict(check_multi_interp_extensions=False))
23892412
def match(config, override_cases):
23902413
ns = vars(config)
23912414
for overrides in override_cases:
@@ -2427,6 +2450,8 @@ def new_interp(config):
24272450
with self.subTest('main'):
24282451
expected = _interpreters.new_config('legacy')
24292452
expected.gil = 'own'
2453+
if Py_GIL_DISABLED:
2454+
expected.check_multi_interp_extensions = False
24302455
interpid, *_ = _interpreters.get_main()
24312456
config = _interpreters.get_config(interpid)
24322457
self.assert_ns_equal(config, expected)
@@ -2448,6 +2473,7 @@ def new_interp(config):
24482473
'empty',
24492474
use_main_obmalloc=True,
24502475
gil='shared',
2476+
check_multi_interp_extensions=bool(Py_GIL_DISABLED),
24512477
)
24522478
with new_interp(orig) as interpid:
24532479
config = _interpreters.get_config(interpid)

Lib/test/test_import/__init__.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
from test.support import os_helper
3131
from test.support import (
3232
STDLIB_DIR, swap_attr, swap_item, cpython_only, is_apple_mobile, is_emscripten,
33-
is_wasi, run_in_subinterp, run_in_subinterp_with_config, Py_TRACE_REFS)
33+
is_wasi, run_in_subinterp, run_in_subinterp_with_config, Py_TRACE_REFS,
34+
requires_gil_enabled, Py_GIL_DISABLED)
3435
from test.support.import_helper import (
3536
forget, make_legacy_pyc, unlink, unload, ready_to_import,
3637
DirsOnSysPath, CleanImport, import_module)
@@ -158,6 +159,9 @@ def meth(self, _meth=meth):
158159
finally:
159160
restore__testsinglephase()
160161
meth = cpython_only(meth)
162+
# gh-117649: free-threaded build does not currently support single-phase
163+
# init modules in subinterpreters.
164+
meth = requires_gil_enabled(meth)
161165
return unittest.skipIf(_testsinglephase is None,
162166
'test requires _testsinglephase module')(meth)
163167

@@ -1876,8 +1880,9 @@ def test_builtin_compat(self):
18761880
# since they still don't implement multi-phase init.
18771881
module = '_imp'
18781882
require_builtin(module)
1879-
with self.subTest(f'{module}: not strict'):
1880-
self.check_compatible_here(module, strict=False)
1883+
if not Py_GIL_DISABLED:
1884+
with self.subTest(f'{module}: not strict'):
1885+
self.check_compatible_here(module, strict=False)
18811886
with self.subTest(f'{module}: strict, not fresh'):
18821887
self.check_compatible_here(module, strict=True)
18831888

@@ -1888,8 +1893,9 @@ def test_frozen_compat(self):
18881893
require_frozen(module, skip=True)
18891894
if __import__(module).__spec__.origin != 'frozen':
18901895
raise unittest.SkipTest(f'{module} is unexpectedly not frozen')
1891-
with self.subTest(f'{module}: not strict'):
1892-
self.check_compatible_here(module, strict=False)
1896+
if not Py_GIL_DISABLED:
1897+
with self.subTest(f'{module}: not strict'):
1898+
self.check_compatible_here(module, strict=False)
18931899
with self.subTest(f'{module}: strict, not fresh'):
18941900
self.check_compatible_here(module, strict=True)
18951901

@@ -1908,8 +1914,9 @@ def test_single_init_extension_compat(self):
19081914
def test_multi_init_extension_compat(self):
19091915
module = '_testmultiphase'
19101916
require_extension(module)
1911-
with self.subTest(f'{module}: not strict'):
1912-
self.check_compatible_here(module, strict=False)
1917+
if not Py_GIL_DISABLED:
1918+
with self.subTest(f'{module}: not strict'):
1919+
self.check_compatible_here(module, strict=False)
19131920
with self.subTest(f'{module}: strict, not fresh'):
19141921
self.check_compatible_here(module, strict=True)
19151922
with self.subTest(f'{module}: strict, fresh'):
@@ -1930,8 +1937,9 @@ def test_multi_init_extension_non_isolated_compat(self):
19301937
self.check_incompatible_here(modname, filename, isolated=True)
19311938
with self.subTest(f'{modname}: not isolated'):
19321939
self.check_incompatible_here(modname, filename, isolated=False)
1933-
with self.subTest(f'{modname}: not strict'):
1934-
self.check_compatible_here(modname, filename, strict=False)
1940+
if not Py_GIL_DISABLED:
1941+
with self.subTest(f'{modname}: not strict'):
1942+
self.check_compatible_here(modname, filename, strict=False)
19351943

19361944
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
19371945
def test_multi_init_extension_per_interpreter_gil_compat(self):
@@ -1949,16 +1957,18 @@ def test_multi_init_extension_per_interpreter_gil_compat(self):
19491957
with self.subTest(f'{modname}: not isolated, strict'):
19501958
self.check_compatible_here(modname, filename,
19511959
strict=True, isolated=False)
1952-
with self.subTest(f'{modname}: not isolated, not strict'):
1953-
self.check_compatible_here(modname, filename,
1954-
strict=False, isolated=False)
1960+
if not Py_GIL_DISABLED:
1961+
with self.subTest(f'{modname}: not isolated, not strict'):
1962+
self.check_compatible_here(modname, filename,
1963+
strict=False, isolated=False)
19551964

19561965
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
19571966
def test_python_compat(self):
19581967
module = 'threading'
19591968
require_pure_python(module)
1960-
with self.subTest(f'{module}: not strict'):
1961-
self.check_compatible_here(module, strict=False)
1969+
if not Py_GIL_DISABLED:
1970+
with self.subTest(f'{module}: not strict'):
1971+
self.check_compatible_here(module, strict=False)
19621972
with self.subTest(f'{module}: strict, not fresh'):
19631973
self.check_compatible_here(module, strict=True)
19641974
with self.subTest(f'{module}: strict, fresh'):

Lib/test/test_importlib/test_util.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,9 @@ def ensure_destroyed():
682682
raise ImportError(excsnap.msg)
683683

684684
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
685+
# gh-117649: single-phase init modules are not currently supported in
686+
# subinterpreters in the free-threaded build
687+
@support.expected_failure_if_gil_disabled()
685688
def test_single_phase_init_module(self):
686689
script = textwrap.dedent('''
687690
from importlib.util import _incompatible_extension_module_restrictions
@@ -706,6 +709,7 @@ def test_single_phase_init_module(self):
706709
self.run_with_own_gil(script)
707710

708711
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
712+
@support.requires_gil_enabled("gh-117649: not supported in free-threaded build")
709713
def test_incomplete_multi_phase_init_module(self):
710714
# Apple extensions must be distributed as frameworks. This requires
711715
# a specialist loader.

Lib/test/test_interpreters/test_api.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from test.support import import_helper
1111
# Raise SkipTest if subinterpreters not supported.
1212
_interpreters = import_helper.import_module('_xxsubinterpreters')
13+
from test.support import Py_GIL_DISABLED
1314
from test.support import interpreters
1415
from test.support.interpreters import (
1516
InterpreterError, InterpreterNotFoundError, ExecutionFailed,
@@ -1162,7 +1163,7 @@ def test_new_config(self):
11621163
allow_exec=True,
11631164
allow_threads=True,
11641165
allow_daemon_threads=True,
1165-
check_multi_interp_extensions=False,
1166+
check_multi_interp_extensions=bool(Py_GIL_DISABLED),
11661167
gil='shared',
11671168
),
11681169
'empty': types.SimpleNamespace(
@@ -1361,6 +1362,7 @@ def test_create(self):
13611362
with self.subTest('custom'):
13621363
orig = _interpreters.new_config('empty')
13631364
orig.use_main_obmalloc = True
1365+
orig.check_multi_interp_extensions = bool(Py_GIL_DISABLED)
13641366
orig.gil = 'shared'
13651367
interpid = _interpreters.create(orig)
13661368
config = _interpreters.get_config(interpid)
@@ -1410,13 +1412,8 @@ def test_get_config(self):
14101412
with self.subTest('main'):
14111413
expected = _interpreters.new_config('legacy')
14121414
expected.gil = 'own'
1413-
interpid, *_ = _interpreters.get_main()
1414-
config = _interpreters.get_config(interpid)
1415-
self.assert_ns_equal(config, expected)
1416-
1417-
with self.subTest('main'):
1418-
expected = _interpreters.new_config('legacy')
1419-
expected.gil = 'own'
1415+
if Py_GIL_DISABLED:
1416+
expected.check_multi_interp_extensions = False
14201417
interpid, *_ = _interpreters.get_main()
14211418
config = _interpreters.get_config(interpid)
14221419
self.assert_ns_equal(config, expected)

Lib/test/test_threading.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,7 @@ def func():
15271527
{before_start}
15281528
t.start()
15291529
""")
1530+
check_multi_interp_extensions = bool(support.Py_GIL_DISABLED)
15301531
script = textwrap.dedent(f"""
15311532
import test.support
15321533
test.support.run_in_subinterp_with_config(
@@ -1536,7 +1537,7 @@ def func():
15361537
allow_exec=True,
15371538
allow_threads={allowed},
15381539
allow_daemon_threads={daemon_allowed},
1539-
check_multi_interp_extensions=False,
1540+
check_multi_interp_extensions={check_multi_interp_extensions},
15401541
own_gil=False,
15411542
)
15421543
""")

Python/import.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3696,9 +3696,16 @@ _imp__override_multi_interp_extensions_check_impl(PyObject *module,
36963696
"cannot be used in the main interpreter");
36973697
return NULL;
36983698
}
3699+
#ifdef Py_GIL_DISABLED
3700+
PyErr_SetString(PyExc_RuntimeError,
3701+
"_imp._override_multi_interp_extensions_check() "
3702+
"cannot be used in the free-threaded build");
3703+
return NULL;
3704+
#else
36993705
int oldvalue = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp);
37003706
OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) = override;
37013707
return PyLong_FromLong(oldvalue);
3708+
#endif
37023709
}
37033710

37043711
#ifdef HAVE_DYNAMIC_LOADING

Python/pylifecycle.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,15 @@ init_interp_settings(PyInterpreterState *interp,
559559
return _PyStatus_ERR("per-interpreter obmalloc does not support "
560560
"single-phase init extension modules");
561561
}
562+
#ifdef Py_GIL_DISABLED
563+
if (!_Py_IsMainInterpreter(interp) &&
564+
!config->check_multi_interp_extensions)
565+
{
566+
return _PyStatus_ERR("The free-threaded build does not support "
567+
"single-phase init extension modules in "
568+
"subinterpreters");
569+
}
570+
#endif
562571

563572
if (config->allow_fork) {
564573
interp->feature_flags |= Py_RTFLAGS_FORK;
@@ -647,8 +656,10 @@ pycore_create_interpreter(_PyRuntimeState *runtime,
647656
}
648657

649658
PyInterpreterConfig config = _PyInterpreterConfig_LEGACY_INIT;
650-
// The main interpreter always has its own GIL.
659+
// The main interpreter always has its own GIL and supports single-phase
660+
// init extensions.
651661
config.gil = PyInterpreterConfig_OWN_GIL;
662+
config.check_multi_interp_extensions = 0;
652663
status = init_interp_settings(interp, &config);
653664
if (_PyStatus_EXCEPTION(status)) {
654665
return status;

0 commit comments

Comments
 (0)