mirror of https://github.com/python/cpython
gh-98627: Add an Optional Check for Extension Module Subinterpreter Compatibility (gh-99040)
Enforcing (optionally) the restriction set by PEP 489 makes sense. Furthermore, this sets the stage for a potential restriction related to a per-interpreter GIL. This change includes the following: * add tests for extension module subinterpreter compatibility * add _PyInterpreterConfig.check_multi_interp_extensions * add Py_RTFLAGS_MULTI_INTERP_EXTENSIONS * add _PyImport_CheckSubinterpIncompatibleExtensionAllowed() * fail iff the module does not implement multi-phase init and the current interpreter is configured to check https://github.com/python/cpython/issues/98627
This commit is contained in:
parent
3dea4ba6c1
commit
89ac665891
|
@ -248,6 +248,7 @@ typedef struct {
|
|||
int allow_exec;
|
||||
int allow_threads;
|
||||
int allow_daemon_threads;
|
||||
int check_multi_interp_extensions;
|
||||
} _PyInterpreterConfig;
|
||||
|
||||
#define _PyInterpreterConfig_INIT \
|
||||
|
@ -256,6 +257,7 @@ typedef struct {
|
|||
.allow_exec = 0, \
|
||||
.allow_threads = 1, \
|
||||
.allow_daemon_threads = 0, \
|
||||
.check_multi_interp_extensions = 1, \
|
||||
}
|
||||
|
||||
#define _PyInterpreterConfig_LEGACY_INIT \
|
||||
|
@ -264,6 +266,7 @@ typedef struct {
|
|||
.allow_exec = 1, \
|
||||
.allow_threads = 1, \
|
||||
.allow_daemon_threads = 1, \
|
||||
.check_multi_interp_extensions = 0, \
|
||||
}
|
||||
|
||||
/* --- Helper functions --------------------------------------- */
|
||||
|
|
|
@ -11,6 +11,9 @@ is available in a given context. For example, forking the process
|
|||
might not be allowed in the current interpreter (i.e. os.fork() would fail).
|
||||
*/
|
||||
|
||||
/* Set if import should check a module for subinterpreter support. */
|
||||
#define Py_RTFLAGS_MULTI_INTERP_EXTENSIONS (1UL << 8)
|
||||
|
||||
/* Set if threads are allowed. */
|
||||
#define Py_RTFLAGS_THREADS (1UL << 10)
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@ struct _import_state {
|
|||
/* override for config->use_frozen_modules (for tests)
|
||||
(-1: "off", 1: "on", 0: no override) */
|
||||
int override_frozen_modules;
|
||||
int override_multi_interp_extensions_check;
|
||||
#ifdef HAVE_DLOPEN
|
||||
int dlopenflags;
|
||||
#endif
|
||||
|
@ -153,6 +154,10 @@ PyAPI_DATA(const struct _frozen *) _PyImport_FrozenStdlib;
|
|||
PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest;
|
||||
extern const struct _module_alias * _PyImport_FrozenAliases;
|
||||
|
||||
PyAPI_FUNC(int) _PyImport_CheckSubinterpIncompatibleExtensionAllowed(
|
||||
const char *name);
|
||||
|
||||
|
||||
// for testing
|
||||
PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename);
|
||||
|
||||
|
|
|
@ -105,6 +105,24 @@ def frozen_modules(enabled=True):
|
|||
_imp._override_frozen_modules_for_tests(0)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def multi_interp_extensions_check(enabled=True):
|
||||
"""Force legacy modules to be allowed in subinterpreters (or not).
|
||||
|
||||
("legacy" == single-phase init)
|
||||
|
||||
This only applies to modules that haven't been imported yet.
|
||||
It overrides the PyInterpreterConfig.check_multi_interp_extensions
|
||||
setting (see support.run_in_subinterp_with_config() and
|
||||
_xxsubinterpreters.create()).
|
||||
"""
|
||||
old = _imp._override_multi_interp_extensions_check(1 if enabled else -1)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_imp._override_multi_interp_extensions_check(old)
|
||||
|
||||
|
||||
def import_fresh_module(name, fresh=(), blocked=(), *,
|
||||
deprecated=False,
|
||||
usefrozen=False,
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
# This script is used by test_misc.
|
||||
|
||||
import _imp
|
||||
import _testinternalcapi
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def import_singlephase():
|
||||
assert '_testsinglephase' not in sys.modules
|
||||
try:
|
||||
import _testsinglephase
|
||||
except ImportError:
|
||||
sys.modules.pop('_testsinglephase')
|
||||
return False
|
||||
else:
|
||||
del sys.modules['_testsinglephase']
|
||||
return True
|
||||
|
||||
|
||||
def check_singlephase(override):
|
||||
# Check using the default setting.
|
||||
settings_initial = _testinternalcapi.get_interp_settings()
|
||||
allowed_initial = import_singlephase()
|
||||
assert(_testinternalcapi.get_interp_settings() == settings_initial)
|
||||
|
||||
# Apply the override and check.
|
||||
override_initial = _imp._override_multi_interp_extensions_check(override)
|
||||
settings_after = _testinternalcapi.get_interp_settings()
|
||||
allowed_after = import_singlephase()
|
||||
|
||||
# Apply the override again and check.
|
||||
noop = {}
|
||||
override_after = _imp._override_multi_interp_extensions_check(override)
|
||||
settings_noop = _testinternalcapi.get_interp_settings()
|
||||
if settings_noop != settings_after:
|
||||
noop['settings_noop'] = settings_noop
|
||||
allowed_noop = import_singlephase()
|
||||
if allowed_noop != allowed_after:
|
||||
noop['allowed_noop'] = allowed_noop
|
||||
|
||||
# Restore the original setting and check.
|
||||
override_noop = _imp._override_multi_interp_extensions_check(override_initial)
|
||||
if override_noop != override_after:
|
||||
noop['override_noop'] = override_noop
|
||||
settings_restored = _testinternalcapi.get_interp_settings()
|
||||
allowed_restored = import_singlephase()
|
||||
|
||||
# Restore the original setting again.
|
||||
override_restored = _imp._override_multi_interp_extensions_check(override_initial)
|
||||
assert(_testinternalcapi.get_interp_settings() == settings_restored)
|
||||
|
||||
return dict({
|
||||
'requested': override,
|
||||
'override__initial': override_initial,
|
||||
'override_after': override_after,
|
||||
'override_restored': override_restored,
|
||||
'settings__initial': settings_initial,
|
||||
'settings_after': settings_after,
|
||||
'settings_restored': settings_restored,
|
||||
'allowed__initial': allowed_initial,
|
||||
'allowed_after': allowed_after,
|
||||
'allowed_restored': allowed_restored,
|
||||
}, **noop)
|
||||
|
||||
|
||||
def run_singlephase_check(override, outfd):
|
||||
with os.fdopen(outfd, 'w') as outfile:
|
||||
sys.stdout = outfile
|
||||
sys.stderr = outfile
|
||||
try:
|
||||
results = check_singlephase(override)
|
||||
json.dump(results, outfile)
|
||||
finally:
|
||||
sys.stdout = sys.__stdout__
|
||||
sys.stderr = sys.__stderr__
|
|
@ -31,6 +31,10 @@ try:
|
|||
import _testmultiphase
|
||||
except ImportError:
|
||||
_testmultiphase = None
|
||||
try:
|
||||
import _testsinglephase
|
||||
except ImportError:
|
||||
_testsinglephase = None
|
||||
|
||||
# Skip this test if the _testcapi module isn't available.
|
||||
_testcapi = import_helper.import_module('_testcapi')
|
||||
|
@ -1297,17 +1301,20 @@ class SubinterpreterTest(unittest.TestCase):
|
|||
"""
|
||||
import json
|
||||
|
||||
EXTENSIONS = 1<<8
|
||||
THREADS = 1<<10
|
||||
DAEMON_THREADS = 1<<11
|
||||
FORK = 1<<15
|
||||
EXEC = 1<<16
|
||||
|
||||
features = ['fork', 'exec', 'threads', 'daemon_threads']
|
||||
features = ['fork', 'exec', 'threads', 'daemon_threads', 'extensions']
|
||||
kwlist = [f'allow_{n}' for n in features]
|
||||
kwlist[-1] = 'check_multi_interp_extensions'
|
||||
for config, expected in {
|
||||
(True, True, True, True): FORK | EXEC | THREADS | DAEMON_THREADS,
|
||||
(False, False, False, False): 0,
|
||||
(False, False, True, False): THREADS,
|
||||
(True, True, True, True, True):
|
||||
FORK | EXEC | THREADS | DAEMON_THREADS | EXTENSIONS,
|
||||
(False, False, False, False, False): 0,
|
||||
(False, False, True, False, True): THREADS | EXTENSIONS,
|
||||
}.items():
|
||||
kwargs = dict(zip(kwlist, config))
|
||||
expected = {
|
||||
|
@ -1322,12 +1329,93 @@ class SubinterpreterTest(unittest.TestCase):
|
|||
json.dump(settings, stdin)
|
||||
''')
|
||||
with os.fdopen(r) as stdout:
|
||||
support.run_in_subinterp_with_config(script, **kwargs)
|
||||
ret = support.run_in_subinterp_with_config(script, **kwargs)
|
||||
self.assertEqual(ret, 0)
|
||||
out = stdout.read()
|
||||
settings = json.loads(out)
|
||||
|
||||
self.assertEqual(settings, expected)
|
||||
|
||||
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
|
||||
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
|
||||
def test_overridden_setting_extensions_subinterp_check(self):
|
||||
"""
|
||||
PyInterpreterConfig.check_multi_interp_extensions can be overridden
|
||||
with PyInterpreterState.override_multi_interp_extensions_check.
|
||||
This verifies that the override works but does not modify
|
||||
the underlying setting.
|
||||
"""
|
||||
import json
|
||||
|
||||
EXTENSIONS = 1<<8
|
||||
THREADS = 1<<10
|
||||
DAEMON_THREADS = 1<<11
|
||||
FORK = 1<<15
|
||||
EXEC = 1<<16
|
||||
BASE_FLAGS = FORK | EXEC | THREADS | DAEMON_THREADS
|
||||
base_kwargs = {
|
||||
'allow_fork': True,
|
||||
'allow_exec': True,
|
||||
'allow_threads': True,
|
||||
'allow_daemon_threads': True,
|
||||
}
|
||||
|
||||
def check(enabled, override):
|
||||
kwargs = dict(
|
||||
base_kwargs,
|
||||
check_multi_interp_extensions=enabled,
|
||||
)
|
||||
flags = BASE_FLAGS | EXTENSIONS if enabled else BASE_FLAGS
|
||||
settings = {
|
||||
'feature_flags': flags,
|
||||
}
|
||||
|
||||
expected = {
|
||||
'requested': override,
|
||||
'override__initial': 0,
|
||||
'override_after': override,
|
||||
'override_restored': 0,
|
||||
# The override should not affect the config or settings.
|
||||
'settings__initial': settings,
|
||||
'settings_after': settings,
|
||||
'settings_restored': settings,
|
||||
# These are the most likely values to be wrong.
|
||||
'allowed__initial': not enabled,
|
||||
'allowed_after': not ((override > 0) if override else enabled),
|
||||
'allowed_restored': not enabled,
|
||||
}
|
||||
|
||||
r, w = os.pipe()
|
||||
script = textwrap.dedent(f'''
|
||||
from test.test_capi.check_config import run_singlephase_check
|
||||
run_singlephase_check({override}, {w})
|
||||
''')
|
||||
with os.fdopen(r) as stdout:
|
||||
ret = support.run_in_subinterp_with_config(script, **kwargs)
|
||||
self.assertEqual(ret, 0)
|
||||
out = stdout.read()
|
||||
results = json.loads(out)
|
||||
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
self.maxDiff = None
|
||||
|
||||
# setting: check disabled
|
||||
with self.subTest('config: check disabled; override: disabled'):
|
||||
check(False, -1)
|
||||
with self.subTest('config: check disabled; override: use config'):
|
||||
check(False, 0)
|
||||
with self.subTest('config: check disabled; override: enabled'):
|
||||
check(False, 1)
|
||||
|
||||
# setting: check enabled
|
||||
with self.subTest('config: check enabled; override: disabled'):
|
||||
check(True, -1)
|
||||
with self.subTest('config: check enabled; override: use config'):
|
||||
check(True, 0)
|
||||
with self.subTest('config: check enabled; override: enabled'):
|
||||
check(True, 1)
|
||||
|
||||
def test_mutate_exception(self):
|
||||
"""
|
||||
Exceptions saved in global module state get shared between
|
||||
|
|
|
@ -1656,13 +1656,15 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
|
|||
api=API_PYTHON, env=env)
|
||||
|
||||
def test_init_main_interpreter_settings(self):
|
||||
EXTENSIONS = 1<<8
|
||||
THREADS = 1<<10
|
||||
DAEMON_THREADS = 1<<11
|
||||
FORK = 1<<15
|
||||
EXEC = 1<<16
|
||||
expected = {
|
||||
# All optional features should be enabled.
|
||||
'feature_flags': FORK | EXEC | THREADS | DAEMON_THREADS,
|
||||
'feature_flags':
|
||||
FORK | EXEC | THREADS | DAEMON_THREADS,
|
||||
}
|
||||
out, err = self.run_embedded_interpreter(
|
||||
'test_init_main_interpreter_settings',
|
||||
|
|
|
@ -21,7 +21,7 @@ from unittest import mock
|
|||
from test.support import os_helper
|
||||
from test.support import (
|
||||
STDLIB_DIR, swap_attr, swap_item, cpython_only, is_emscripten,
|
||||
is_wasi)
|
||||
is_wasi, run_in_subinterp_with_config)
|
||||
from test.support.import_helper import (
|
||||
forget, make_legacy_pyc, unlink, unload, DirsOnSysPath, CleanImport)
|
||||
from test.support.os_helper import (
|
||||
|
@ -30,6 +30,14 @@ from test.support import script_helper
|
|||
from test.support import threading_helper
|
||||
from test.test_importlib.util import uncache
|
||||
from types import ModuleType
|
||||
try:
|
||||
import _testsinglephase
|
||||
except ImportError:
|
||||
_testsinglephase = None
|
||||
try:
|
||||
import _testmultiphase
|
||||
except ImportError:
|
||||
_testmultiphase = None
|
||||
|
||||
|
||||
skip_if_dont_write_bytecode = unittest.skipIf(
|
||||
|
@ -1392,6 +1400,216 @@ class CircularImportTests(unittest.TestCase):
|
|||
unwritable.x = 42
|
||||
|
||||
|
||||
class SubinterpImportTests(unittest.TestCase):
|
||||
|
||||
RUN_KWARGS = dict(
|
||||
allow_fork=False,
|
||||
allow_exec=False,
|
||||
allow_threads=True,
|
||||
allow_daemon_threads=False,
|
||||
)
|
||||
|
||||
@unittest.skipUnless(hasattr(os, "pipe"), "requires os.pipe()")
|
||||
def pipe(self):
|
||||
r, w = os.pipe()
|
||||
self.addCleanup(os.close, r)
|
||||
self.addCleanup(os.close, w)
|
||||
if hasattr(os, 'set_blocking'):
|
||||
os.set_blocking(r, False)
|
||||
return (r, w)
|
||||
|
||||
def import_script(self, name, fd, 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'))
|
||||
''')
|
||||
|
||||
def run_shared(self, name, *,
|
||||
check_singlephase_setting=False,
|
||||
check_singlephase_override=None,
|
||||
):
|
||||
"""
|
||||
Try importing the named module in a subinterpreter.
|
||||
|
||||
The subinterpreter will be in the current process.
|
||||
The module will have already been imported in the main interpreter.
|
||||
Thus, for extension/builtin modules, the module definition will
|
||||
have been loaded already and cached globally.
|
||||
|
||||
"check_singlephase_setting" determines whether or not
|
||||
the interpreter will be configured to check for modules
|
||||
that are not compatible with use in multiple interpreters.
|
||||
|
||||
This should always return "okay" for all modules if the
|
||||
setting is False (with no override).
|
||||
"""
|
||||
__import__(name)
|
||||
|
||||
kwargs = dict(
|
||||
**self.RUN_KWARGS,
|
||||
check_multi_interp_extensions=check_singlephase_setting,
|
||||
)
|
||||
|
||||
r, w = self.pipe()
|
||||
script = self.import_script(name, w, check_singlephase_override)
|
||||
|
||||
ret = run_in_subinterp_with_config(script, **kwargs)
|
||||
self.assertEqual(ret, 0)
|
||||
return os.read(r, 100)
|
||||
|
||||
def check_compatible_shared(self, name, *, strict=False):
|
||||
# Verify that the named module may be imported in a subinterpreter.
|
||||
# (See run_shared() for more info.)
|
||||
out = self.run_shared(name, check_singlephase_setting=strict)
|
||||
self.assertEqual(out, b'okay')
|
||||
|
||||
def check_incompatible_shared(self, name):
|
||||
# Differences from check_compatible_shared():
|
||||
# * verify that import fails
|
||||
# * "strict" is always True
|
||||
out = self.run_shared(name, check_singlephase_setting=True)
|
||||
self.assertEqual(
|
||||
out.decode('utf-8'),
|
||||
f'ImportError: module {name} does not support loading in subinterpreters',
|
||||
)
|
||||
|
||||
def check_compatible_isolated(self, name, *, strict=False):
|
||||
# Differences from check_compatible_shared():
|
||||
# * subinterpreter in a new process
|
||||
# * module has never been imported before in that process
|
||||
# * this tests importing the module for the first time
|
||||
_, out, err = script_helper.assert_python_ok('-c', textwrap.dedent(f'''
|
||||
import _testcapi, sys
|
||||
assert (
|
||||
{name!r} in sys.builtin_module_names or
|
||||
{name!r} not in sys.modules
|
||||
), repr({name!r})
|
||||
ret = _testcapi.run_in_subinterp_with_config(
|
||||
{self.import_script(name, "sys.stdout.fileno()")!r},
|
||||
**{self.RUN_KWARGS},
|
||||
check_multi_interp_extensions={strict},
|
||||
)
|
||||
assert ret == 0, ret
|
||||
'''))
|
||||
self.assertEqual(err, b'')
|
||||
self.assertEqual(out, b'okay')
|
||||
|
||||
def check_incompatible_isolated(self, name):
|
||||
# Differences from check_compatible_isolated():
|
||||
# * verify that import fails
|
||||
# * "strict" is always True
|
||||
_, out, err = script_helper.assert_python_ok('-c', textwrap.dedent(f'''
|
||||
import _testcapi, sys
|
||||
assert {name!r} not in sys.modules, {name!r}
|
||||
ret = _testcapi.run_in_subinterp_with_config(
|
||||
{self.import_script(name, "sys.stdout.fileno()")!r},
|
||||
**{self.RUN_KWARGS},
|
||||
check_multi_interp_extensions=True,
|
||||
)
|
||||
assert ret == 0, ret
|
||||
'''))
|
||||
self.assertEqual(err, b'')
|
||||
self.assertEqual(
|
||||
out.decode('utf-8'),
|
||||
f'ImportError: module {name} does not support loading in subinterpreters',
|
||||
)
|
||||
|
||||
def test_builtin_compat(self):
|
||||
module = 'sys'
|
||||
with self.subTest(f'{module}: not strict'):
|
||||
self.check_compatible_shared(module, strict=False)
|
||||
with self.subTest(f'{module}: strict, shared'):
|
||||
self.check_compatible_shared(module, strict=True)
|
||||
|
||||
@cpython_only
|
||||
def test_frozen_compat(self):
|
||||
module = '_frozen_importlib'
|
||||
if __import__(module).__spec__.origin != 'frozen':
|
||||
raise unittest.SkipTest(f'{module} is unexpectedly not frozen')
|
||||
with self.subTest(f'{module}: not strict'):
|
||||
self.check_compatible_shared(module, strict=False)
|
||||
with self.subTest(f'{module}: strict, shared'):
|
||||
self.check_compatible_shared(module, strict=True)
|
||||
|
||||
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
|
||||
def test_single_init_extension_compat(self):
|
||||
module = '_testsinglephase'
|
||||
with self.subTest(f'{module}: not strict'):
|
||||
self.check_compatible_shared(module, strict=False)
|
||||
with self.subTest(f'{module}: strict, shared'):
|
||||
self.check_incompatible_shared(module)
|
||||
with self.subTest(f'{module}: strict, isolated'):
|
||||
self.check_incompatible_isolated(module)
|
||||
|
||||
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
|
||||
def test_multi_init_extension_compat(self):
|
||||
module = '_testmultiphase'
|
||||
with self.subTest(f'{module}: not strict'):
|
||||
self.check_compatible_shared(module, strict=False)
|
||||
with self.subTest(f'{module}: strict, shared'):
|
||||
self.check_compatible_shared(module, strict=True)
|
||||
with self.subTest(f'{module}: strict, isolated'):
|
||||
self.check_compatible_isolated(module, strict=True)
|
||||
|
||||
def test_python_compat(self):
|
||||
module = 'threading'
|
||||
if __import__(module).__spec__.origin == 'frozen':
|
||||
raise unittest.SkipTest(f'{module} is unexpectedly frozen')
|
||||
with self.subTest(f'{module}: not strict'):
|
||||
self.check_compatible_shared(module, strict=False)
|
||||
with self.subTest(f'{module}: strict, shared'):
|
||||
self.check_compatible_shared(module, strict=True)
|
||||
with self.subTest(f'{module}: strict, isolated'):
|
||||
self.check_compatible_isolated(module, strict=True)
|
||||
|
||||
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
|
||||
def test_singlephase_check_with_setting_and_override(self):
|
||||
module = '_testsinglephase'
|
||||
|
||||
def check_compatible(setting, override):
|
||||
out = self.run_shared(
|
||||
module,
|
||||
check_singlephase_setting=setting,
|
||||
check_singlephase_override=override,
|
||||
)
|
||||
self.assertEqual(out, b'okay')
|
||||
|
||||
def check_incompatible(setting, override):
|
||||
out = self.run_shared(
|
||||
module,
|
||||
check_singlephase_setting=setting,
|
||||
check_singlephase_override=override,
|
||||
)
|
||||
self.assertNotEqual(out, b'okay')
|
||||
|
||||
with self.subTest('config: check enabled; override: enabled'):
|
||||
check_incompatible(True, 1)
|
||||
with self.subTest('config: check enabled; override: use config'):
|
||||
check_incompatible(True, 0)
|
||||
with self.subTest('config: check enabled; override: disabled'):
|
||||
check_compatible(True, -1)
|
||||
|
||||
with self.subTest('config: check disabled; override: enabled'):
|
||||
check_incompatible(False, 1)
|
||||
with self.subTest('config: check disabled; override: use config'):
|
||||
check_compatible(False, 0)
|
||||
with self.subTest('config: check disabled; override: disabled'):
|
||||
check_compatible(False, -1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Test needs to be a package, so we can do relative imports.
|
||||
unittest.main()
|
||||
|
|
|
@ -1347,6 +1347,7 @@ class SubinterpThreadingTests(BaseTestCase):
|
|||
allow_exec=True,
|
||||
allow_threads={allowed},
|
||||
allow_daemon_threads={daemon_allowed},
|
||||
check_multi_interp_extensions=False,
|
||||
)
|
||||
""")
|
||||
with test.support.SuppressCrashReport():
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
When an interpreter is configured to check (and only then), importing an
|
||||
extension module will now fail when the extension does not support multiple
|
||||
interpreters (i.e. doesn't implement PEP 489 multi-phase init). This does
|
||||
not apply to the main interpreter, nor to subinterpreters created with
|
||||
``Py_NewInterpreter()``.
|
|
@ -1618,6 +1618,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||
int allow_exec = -1;
|
||||
int allow_threads = -1;
|
||||
int allow_daemon_threads = -1;
|
||||
int check_multi_interp_extensions = -1;
|
||||
int r;
|
||||
PyThreadState *substate, *mainstate;
|
||||
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
|
||||
|
@ -1628,11 +1629,13 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||
"allow_exec",
|
||||
"allow_threads",
|
||||
"allow_daemon_threads",
|
||||
"check_multi_interp_extensions",
|
||||
NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
|
||||
"s$pppp:run_in_subinterp_with_config", kwlist,
|
||||
"s$ppppp:run_in_subinterp_with_config", kwlist,
|
||||
&code, &allow_fork, &allow_exec,
|
||||
&allow_threads, &allow_daemon_threads)) {
|
||||
&allow_threads, &allow_daemon_threads,
|
||||
&check_multi_interp_extensions)) {
|
||||
return NULL;
|
||||
}
|
||||
if (allow_fork < 0) {
|
||||
|
@ -1651,6 +1654,10 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||
PyErr_SetString(PyExc_ValueError, "missing allow_daemon_threads");
|
||||
return NULL;
|
||||
}
|
||||
if (check_multi_interp_extensions < 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "missing check_multi_interp_extensions");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mainstate = PyThreadState_Get();
|
||||
|
||||
|
@ -1661,6 +1668,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||
.allow_exec = allow_exec,
|
||||
.allow_threads = allow_threads,
|
||||
.allow_daemon_threads = allow_daemon_threads,
|
||||
.check_multi_interp_extensions = check_multi_interp_extensions,
|
||||
};
|
||||
substate = _Py_NewInterpreterFromConfig(&config);
|
||||
if (substate == NULL) {
|
||||
|
|
|
@ -442,6 +442,37 @@ exit:
|
|||
return return_value;
|
||||
}
|
||||
|
||||
PyDoc_STRVAR(_imp__override_multi_interp_extensions_check__doc__,
|
||||
"_override_multi_interp_extensions_check($module, override, /)\n"
|
||||
"--\n"
|
||||
"\n"
|
||||
"(internal-only) Override PyInterpreterConfig.check_multi_interp_extensions.\n"
|
||||
"\n"
|
||||
"(-1: \"never\", 1: \"always\", 0: no override)");
|
||||
|
||||
#define _IMP__OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK_METHODDEF \
|
||||
{"_override_multi_interp_extensions_check", (PyCFunction)_imp__override_multi_interp_extensions_check, METH_O, _imp__override_multi_interp_extensions_check__doc__},
|
||||
|
||||
static PyObject *
|
||||
_imp__override_multi_interp_extensions_check_impl(PyObject *module,
|
||||
int override);
|
||||
|
||||
static PyObject *
|
||||
_imp__override_multi_interp_extensions_check(PyObject *module, PyObject *arg)
|
||||
{
|
||||
PyObject *return_value = NULL;
|
||||
int override;
|
||||
|
||||
override = _PyLong_AsInt(arg);
|
||||
if (override == -1 && PyErr_Occurred()) {
|
||||
goto exit;
|
||||
}
|
||||
return_value = _imp__override_multi_interp_extensions_check_impl(module, override);
|
||||
|
||||
exit:
|
||||
return return_value;
|
||||
}
|
||||
|
||||
#if defined(HAVE_DYNAMIC_LOADING)
|
||||
|
||||
PyDoc_STRVAR(_imp_create_dynamic__doc__,
|
||||
|
@ -617,4 +648,4 @@ exit:
|
|||
#ifndef _IMP_EXEC_DYNAMIC_METHODDEF
|
||||
#define _IMP_EXEC_DYNAMIC_METHODDEF
|
||||
#endif /* !defined(_IMP_EXEC_DYNAMIC_METHODDEF) */
|
||||
/*[clinic end generated code: output=806352838c3f7008 input=a9049054013a1b77]*/
|
||||
/*[clinic end generated code: output=b18d46e0036eff49 input=a9049054013a1b77]*/
|
||||
|
|
|
@ -74,6 +74,8 @@ static struct _inittab *inittab_copy = NULL;
|
|||
(interp)->imports.modules_by_index
|
||||
#define IMPORTLIB(interp) \
|
||||
(interp)->imports.importlib
|
||||
#define OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) \
|
||||
(interp)->imports.override_multi_interp_extensions_check
|
||||
#define OVERRIDE_FROZEN_MODULES(interp) \
|
||||
(interp)->imports.override_frozen_modules
|
||||
#ifdef HAVE_DLOPEN
|
||||
|
@ -816,6 +818,38 @@ _extensions_cache_clear_all(void)
|
|||
Py_CLEAR(EXTENSIONS);
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
check_multi_interp_extensions(PyInterpreterState *interp)
|
||||
{
|
||||
int override = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp);
|
||||
if (override < 0) {
|
||||
return false;
|
||||
}
|
||||
else if (override > 0) {
|
||||
return true;
|
||||
}
|
||||
else if (_PyInterpreterState_HasFeature(
|
||||
interp, Py_RTFLAGS_MULTI_INTERP_EXTENSIONS)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int
|
||||
_PyImport_CheckSubinterpIncompatibleExtensionAllowed(const char *name)
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_Get();
|
||||
if (check_multi_interp_extensions(interp)) {
|
||||
assert(!_Py_IsMainInterpreter(interp));
|
||||
PyErr_Format(PyExc_ImportError,
|
||||
"module %s does not support loading in subinterpreters",
|
||||
name);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
fix_up_extension(PyObject *mod, PyObject *name, PyObject *filename)
|
||||
{
|
||||
|
@ -3297,6 +3331,34 @@ _imp__override_frozen_modules_for_tests_impl(PyObject *module, int override)
|
|||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
/*[clinic input]
|
||||
_imp._override_multi_interp_extensions_check
|
||||
|
||||
override: int
|
||||
/
|
||||
|
||||
(internal-only) Override PyInterpreterConfig.check_multi_interp_extensions.
|
||||
|
||||
(-1: "never", 1: "always", 0: no override)
|
||||
[clinic start generated code]*/
|
||||
|
||||
static PyObject *
|
||||
_imp__override_multi_interp_extensions_check_impl(PyObject *module,
|
||||
int override)
|
||||
/*[clinic end generated code: output=3ff043af52bbf280 input=e086a2ea181f92ae]*/
|
||||
{
|
||||
PyInterpreterState *interp = _PyInterpreterState_GET();
|
||||
if (_Py_IsMainInterpreter(interp)) {
|
||||
PyErr_SetString(PyExc_RuntimeError,
|
||||
"_imp._override_multi_interp_extensions_check() "
|
||||
"cannot be used in the main interpreter");
|
||||
return NULL;
|
||||
}
|
||||
int oldvalue = OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp);
|
||||
OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) = override;
|
||||
return PyLong_FromLong(oldvalue);
|
||||
}
|
||||
|
||||
#ifdef HAVE_DYNAMIC_LOADING
|
||||
|
||||
/*[clinic input]
|
||||
|
@ -3329,18 +3391,23 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file)
|
|||
|
||||
PyThreadState *tstate = _PyThreadState_GET();
|
||||
mod = import_find_extension(tstate, name, path);
|
||||
if (mod != NULL || PyErr_Occurred()) {
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(path);
|
||||
return mod;
|
||||
if (mod != NULL) {
|
||||
const char *name_buf = PyUnicode_AsUTF8(name);
|
||||
assert(name_buf != NULL);
|
||||
if (_PyImport_CheckSubinterpIncompatibleExtensionAllowed(name_buf) < 0) {
|
||||
Py_DECREF(mod);
|
||||
mod = NULL;
|
||||
}
|
||||
goto finally;
|
||||
}
|
||||
else if (PyErr_Occurred()) {
|
||||
goto finally;
|
||||
}
|
||||
|
||||
if (file != NULL) {
|
||||
fp = _Py_fopen_obj(path, "r");
|
||||
if (fp == NULL) {
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(path);
|
||||
return NULL;
|
||||
goto finally;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -3348,10 +3415,12 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file)
|
|||
|
||||
mod = _PyImport_LoadDynamicModuleWithSpec(spec, fp);
|
||||
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(path);
|
||||
if (fp)
|
||||
fclose(fp);
|
||||
|
||||
finally:
|
||||
Py_DECREF(name);
|
||||
Py_DECREF(path);
|
||||
return mod;
|
||||
}
|
||||
|
||||
|
@ -3436,6 +3505,7 @@ static PyMethodDef imp_methods[] = {
|
|||
_IMP_IS_FROZEN_METHODDEF
|
||||
_IMP__FROZEN_MODULE_NAMES_METHODDEF
|
||||
_IMP__OVERRIDE_FROZEN_MODULES_FOR_TESTS_METHODDEF
|
||||
_IMP__OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK_METHODDEF
|
||||
_IMP_CREATE_DYNAMIC_METHODDEF
|
||||
_IMP_EXEC_DYNAMIC_METHODDEF
|
||||
_IMP_EXEC_BUILTIN_METHODDEF
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include "Python.h"
|
||||
#include "pycore_call.h"
|
||||
#include "pycore_import.h"
|
||||
#include "pycore_pystate.h"
|
||||
#include "pycore_runtime.h"
|
||||
|
||||
|
@ -203,6 +204,10 @@ _PyImport_LoadDynamicModuleWithSpec(PyObject *spec, FILE *fp)
|
|||
|
||||
/* Fall back to single-phase init mechanism */
|
||||
|
||||
if (_PyImport_CheckSubinterpIncompatibleExtensionAllowed(name_buf) < 0) {
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (hook_prefix == nonascii_prefix) {
|
||||
/* don't allow legacy init for non-ASCII module names */
|
||||
PyErr_Format(
|
||||
|
|
|
@ -565,6 +565,10 @@ init_interp_settings(PyInterpreterState *interp, const _PyInterpreterConfig *con
|
|||
if (config->allow_daemon_threads) {
|
||||
interp->feature_flags |= Py_RTFLAGS_DAEMON_THREADS;
|
||||
}
|
||||
|
||||
if (config->check_multi_interp_extensions) {
|
||||
interp->feature_flags |= Py_RTFLAGS_MULTI_INTERP_EXTENSIONS;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue