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_exec;
|
||||||
int allow_threads;
|
int allow_threads;
|
||||||
int allow_daemon_threads;
|
int allow_daemon_threads;
|
||||||
|
int check_multi_interp_extensions;
|
||||||
} _PyInterpreterConfig;
|
} _PyInterpreterConfig;
|
||||||
|
|
||||||
#define _PyInterpreterConfig_INIT \
|
#define _PyInterpreterConfig_INIT \
|
||||||
@ -256,6 +257,7 @@ typedef struct {
|
|||||||
.allow_exec = 0, \
|
.allow_exec = 0, \
|
||||||
.allow_threads = 1, \
|
.allow_threads = 1, \
|
||||||
.allow_daemon_threads = 0, \
|
.allow_daemon_threads = 0, \
|
||||||
|
.check_multi_interp_extensions = 1, \
|
||||||
}
|
}
|
||||||
|
|
||||||
#define _PyInterpreterConfig_LEGACY_INIT \
|
#define _PyInterpreterConfig_LEGACY_INIT \
|
||||||
@ -264,6 +266,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, \
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Helper functions --------------------------------------- */
|
/* --- 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).
|
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. */
|
/* Set if threads are allowed. */
|
||||||
#define Py_RTFLAGS_THREADS (1UL << 10)
|
#define Py_RTFLAGS_THREADS (1UL << 10)
|
||||||
|
|
||||||
|
@ -64,6 +64,7 @@ struct _import_state {
|
|||||||
/* override for config->use_frozen_modules (for tests)
|
/* override for config->use_frozen_modules (for tests)
|
||||||
(-1: "off", 1: "on", 0: no override) */
|
(-1: "off", 1: "on", 0: no override) */
|
||||||
int override_frozen_modules;
|
int override_frozen_modules;
|
||||||
|
int override_multi_interp_extensions_check;
|
||||||
#ifdef HAVE_DLOPEN
|
#ifdef HAVE_DLOPEN
|
||||||
int dlopenflags;
|
int dlopenflags;
|
||||||
#endif
|
#endif
|
||||||
@ -153,6 +154,10 @@ PyAPI_DATA(const struct _frozen *) _PyImport_FrozenStdlib;
|
|||||||
PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest;
|
PyAPI_DATA(const struct _frozen *) _PyImport_FrozenTest;
|
||||||
extern const struct _module_alias * _PyImport_FrozenAliases;
|
extern const struct _module_alias * _PyImport_FrozenAliases;
|
||||||
|
|
||||||
|
PyAPI_FUNC(int) _PyImport_CheckSubinterpIncompatibleExtensionAllowed(
|
||||||
|
const char *name);
|
||||||
|
|
||||||
|
|
||||||
// for testing
|
// for testing
|
||||||
PyAPI_FUNC(int) _PyImport_ClearExtension(PyObject *name, PyObject *filename);
|
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)
|
_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=(), *,
|
def import_fresh_module(name, fresh=(), blocked=(), *,
|
||||||
deprecated=False,
|
deprecated=False,
|
||||||
usefrozen=False,
|
usefrozen=False,
|
||||||
|
77
Lib/test/test_capi/check_config.py
Normal file
77
Lib/test/test_capi/check_config.py
Normal file
@ -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
|
import _testmultiphase
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_testmultiphase = None
|
_testmultiphase = None
|
||||||
|
try:
|
||||||
|
import _testsinglephase
|
||||||
|
except ImportError:
|
||||||
|
_testsinglephase = None
|
||||||
|
|
||||||
# Skip this test if the _testcapi module isn't available.
|
# Skip this test if the _testcapi module isn't available.
|
||||||
_testcapi = import_helper.import_module('_testcapi')
|
_testcapi = import_helper.import_module('_testcapi')
|
||||||
@ -1297,17 +1301,20 @@ class SubinterpreterTest(unittest.TestCase):
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
EXTENSIONS = 1<<8
|
||||||
THREADS = 1<<10
|
THREADS = 1<<10
|
||||||
DAEMON_THREADS = 1<<11
|
DAEMON_THREADS = 1<<11
|
||||||
FORK = 1<<15
|
FORK = 1<<15
|
||||||
EXEC = 1<<16
|
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 = [f'allow_{n}' for n in features]
|
||||||
|
kwlist[-1] = 'check_multi_interp_extensions'
|
||||||
for config, expected in {
|
for config, expected in {
|
||||||
(True, True, True, True): FORK | EXEC | THREADS | DAEMON_THREADS,
|
(True, True, True, True, True):
|
||||||
(False, False, False, False): 0,
|
FORK | EXEC | THREADS | DAEMON_THREADS | EXTENSIONS,
|
||||||
(False, False, True, False): THREADS,
|
(False, False, False, False, False): 0,
|
||||||
|
(False, False, True, False, True): THREADS | EXTENSIONS,
|
||||||
}.items():
|
}.items():
|
||||||
kwargs = dict(zip(kwlist, config))
|
kwargs = dict(zip(kwlist, config))
|
||||||
expected = {
|
expected = {
|
||||||
@ -1322,12 +1329,93 @@ class SubinterpreterTest(unittest.TestCase):
|
|||||||
json.dump(settings, stdin)
|
json.dump(settings, stdin)
|
||||||
''')
|
''')
|
||||||
with os.fdopen(r) as stdout:
|
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()
|
out = stdout.read()
|
||||||
settings = json.loads(out)
|
settings = json.loads(out)
|
||||||
|
|
||||||
self.assertEqual(settings, expected)
|
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):
|
def test_mutate_exception(self):
|
||||||
"""
|
"""
|
||||||
Exceptions saved in global module state get shared between
|
Exceptions saved in global module state get shared between
|
||||||
|
@ -1656,13 +1656,15 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
|
|||||||
api=API_PYTHON, env=env)
|
api=API_PYTHON, env=env)
|
||||||
|
|
||||||
def test_init_main_interpreter_settings(self):
|
def test_init_main_interpreter_settings(self):
|
||||||
|
EXTENSIONS = 1<<8
|
||||||
THREADS = 1<<10
|
THREADS = 1<<10
|
||||||
DAEMON_THREADS = 1<<11
|
DAEMON_THREADS = 1<<11
|
||||||
FORK = 1<<15
|
FORK = 1<<15
|
||||||
EXEC = 1<<16
|
EXEC = 1<<16
|
||||||
expected = {
|
expected = {
|
||||||
# All optional features should be enabled.
|
# 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(
|
out, err = self.run_embedded_interpreter(
|
||||||
'test_init_main_interpreter_settings',
|
'test_init_main_interpreter_settings',
|
||||||
|
@ -21,7 +21,7 @@ from unittest import mock
|
|||||||
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_emscripten,
|
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 (
|
from test.support.import_helper import (
|
||||||
forget, make_legacy_pyc, unlink, unload, DirsOnSysPath, CleanImport)
|
forget, make_legacy_pyc, unlink, unload, DirsOnSysPath, CleanImport)
|
||||||
from test.support.os_helper import (
|
from test.support.os_helper import (
|
||||||
@ -30,6 +30,14 @@ from test.support import script_helper
|
|||||||
from test.support import threading_helper
|
from test.support import threading_helper
|
||||||
from test.test_importlib.util import uncache
|
from test.test_importlib.util import uncache
|
||||||
from types import ModuleType
|
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(
|
skip_if_dont_write_bytecode = unittest.skipIf(
|
||||||
@ -1392,6 +1400,216 @@ class CircularImportTests(unittest.TestCase):
|
|||||||
unwritable.x = 42
|
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__':
|
if __name__ == '__main__':
|
||||||
# Test needs to be a package, so we can do relative imports.
|
# Test needs to be a package, so we can do relative imports.
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -1347,6 +1347,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,
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
with test.support.SuppressCrashReport():
|
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_exec = -1;
|
||||||
int allow_threads = -1;
|
int allow_threads = -1;
|
||||||
int allow_daemon_threads = -1;
|
int allow_daemon_threads = -1;
|
||||||
|
int check_multi_interp_extensions = -1;
|
||||||
int r;
|
int r;
|
||||||
PyThreadState *substate, *mainstate;
|
PyThreadState *substate, *mainstate;
|
||||||
/* only initialise 'cflags.cf_flags' to test backwards compatibility */
|
/* 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_exec",
|
||||||
"allow_threads",
|
"allow_threads",
|
||||||
"allow_daemon_threads",
|
"allow_daemon_threads",
|
||||||
|
"check_multi_interp_extensions",
|
||||||
NULL};
|
NULL};
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
|
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,
|
&code, &allow_fork, &allow_exec,
|
||||||
&allow_threads, &allow_daemon_threads)) {
|
&allow_threads, &allow_daemon_threads,
|
||||||
|
&check_multi_interp_extensions)) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (allow_fork < 0) {
|
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");
|
PyErr_SetString(PyExc_ValueError, "missing allow_daemon_threads");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
if (check_multi_interp_extensions < 0) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "missing check_multi_interp_extensions");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
mainstate = PyThreadState_Get();
|
mainstate = PyThreadState_Get();
|
||||||
|
|
||||||
@ -1661,6 +1668,7 @@ run_in_subinterp_with_config(PyObject *self, PyObject *args, PyObject *kwargs)
|
|||||||
.allow_exec = allow_exec,
|
.allow_exec = allow_exec,
|
||||||
.allow_threads = allow_threads,
|
.allow_threads = allow_threads,
|
||||||
.allow_daemon_threads = allow_daemon_threads,
|
.allow_daemon_threads = allow_daemon_threads,
|
||||||
|
.check_multi_interp_extensions = check_multi_interp_extensions,
|
||||||
};
|
};
|
||||||
substate = _Py_NewInterpreterFromConfig(&config);
|
substate = _Py_NewInterpreterFromConfig(&config);
|
||||||
if (substate == NULL) {
|
if (substate == NULL) {
|
||||||
|
33
Python/clinic/import.c.h
generated
33
Python/clinic/import.c.h
generated
@ -442,6 +442,37 @@ exit:
|
|||||||
return return_value;
|
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)
|
#if defined(HAVE_DYNAMIC_LOADING)
|
||||||
|
|
||||||
PyDoc_STRVAR(_imp_create_dynamic__doc__,
|
PyDoc_STRVAR(_imp_create_dynamic__doc__,
|
||||||
@ -617,4 +648,4 @@ exit:
|
|||||||
#ifndef _IMP_EXEC_DYNAMIC_METHODDEF
|
#ifndef _IMP_EXEC_DYNAMIC_METHODDEF
|
||||||
#define _IMP_EXEC_DYNAMIC_METHODDEF
|
#define _IMP_EXEC_DYNAMIC_METHODDEF
|
||||||
#endif /* !defined(_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
|
(interp)->imports.modules_by_index
|
||||||
#define IMPORTLIB(interp) \
|
#define IMPORTLIB(interp) \
|
||||||
(interp)->imports.importlib
|
(interp)->imports.importlib
|
||||||
|
#define OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK(interp) \
|
||||||
|
(interp)->imports.override_multi_interp_extensions_check
|
||||||
#define OVERRIDE_FROZEN_MODULES(interp) \
|
#define OVERRIDE_FROZEN_MODULES(interp) \
|
||||||
(interp)->imports.override_frozen_modules
|
(interp)->imports.override_frozen_modules
|
||||||
#ifdef HAVE_DLOPEN
|
#ifdef HAVE_DLOPEN
|
||||||
@ -816,6 +818,38 @@ _extensions_cache_clear_all(void)
|
|||||||
Py_CLEAR(EXTENSIONS);
|
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
|
static int
|
||||||
fix_up_extension(PyObject *mod, PyObject *name, PyObject *filename)
|
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;
|
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
|
#ifdef HAVE_DYNAMIC_LOADING
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
@ -3329,18 +3391,23 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file)
|
|||||||
|
|
||||||
PyThreadState *tstate = _PyThreadState_GET();
|
PyThreadState *tstate = _PyThreadState_GET();
|
||||||
mod = import_find_extension(tstate, name, path);
|
mod = import_find_extension(tstate, name, path);
|
||||||
if (mod != NULL || PyErr_Occurred()) {
|
if (mod != NULL) {
|
||||||
Py_DECREF(name);
|
const char *name_buf = PyUnicode_AsUTF8(name);
|
||||||
Py_DECREF(path);
|
assert(name_buf != NULL);
|
||||||
return mod;
|
if (_PyImport_CheckSubinterpIncompatibleExtensionAllowed(name_buf) < 0) {
|
||||||
|
Py_DECREF(mod);
|
||||||
|
mod = NULL;
|
||||||
|
}
|
||||||
|
goto finally;
|
||||||
|
}
|
||||||
|
else if (PyErr_Occurred()) {
|
||||||
|
goto finally;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file != NULL) {
|
if (file != NULL) {
|
||||||
fp = _Py_fopen_obj(path, "r");
|
fp = _Py_fopen_obj(path, "r");
|
||||||
if (fp == NULL) {
|
if (fp == NULL) {
|
||||||
Py_DECREF(name);
|
goto finally;
|
||||||
Py_DECREF(path);
|
|
||||||
return NULL;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -3348,10 +3415,12 @@ _imp_create_dynamic_impl(PyObject *module, PyObject *spec, PyObject *file)
|
|||||||
|
|
||||||
mod = _PyImport_LoadDynamicModuleWithSpec(spec, fp);
|
mod = _PyImport_LoadDynamicModuleWithSpec(spec, fp);
|
||||||
|
|
||||||
Py_DECREF(name);
|
|
||||||
Py_DECREF(path);
|
|
||||||
if (fp)
|
if (fp)
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
|
|
||||||
|
finally:
|
||||||
|
Py_DECREF(name);
|
||||||
|
Py_DECREF(path);
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3436,6 +3505,7 @@ static PyMethodDef imp_methods[] = {
|
|||||||
_IMP_IS_FROZEN_METHODDEF
|
_IMP_IS_FROZEN_METHODDEF
|
||||||
_IMP__FROZEN_MODULE_NAMES_METHODDEF
|
_IMP__FROZEN_MODULE_NAMES_METHODDEF
|
||||||
_IMP__OVERRIDE_FROZEN_MODULES_FOR_TESTS_METHODDEF
|
_IMP__OVERRIDE_FROZEN_MODULES_FOR_TESTS_METHODDEF
|
||||||
|
_IMP__OVERRIDE_MULTI_INTERP_EXTENSIONS_CHECK_METHODDEF
|
||||||
_IMP_CREATE_DYNAMIC_METHODDEF
|
_IMP_CREATE_DYNAMIC_METHODDEF
|
||||||
_IMP_EXEC_DYNAMIC_METHODDEF
|
_IMP_EXEC_DYNAMIC_METHODDEF
|
||||||
_IMP_EXEC_BUILTIN_METHODDEF
|
_IMP_EXEC_BUILTIN_METHODDEF
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "Python.h"
|
#include "Python.h"
|
||||||
#include "pycore_call.h"
|
#include "pycore_call.h"
|
||||||
|
#include "pycore_import.h"
|
||||||
#include "pycore_pystate.h"
|
#include "pycore_pystate.h"
|
||||||
#include "pycore_runtime.h"
|
#include "pycore_runtime.h"
|
||||||
|
|
||||||
@ -203,6 +204,10 @@ _PyImport_LoadDynamicModuleWithSpec(PyObject *spec, FILE *fp)
|
|||||||
|
|
||||||
/* Fall back to single-phase init mechanism */
|
/* Fall back to single-phase init mechanism */
|
||||||
|
|
||||||
|
if (_PyImport_CheckSubinterpIncompatibleExtensionAllowed(name_buf) < 0) {
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
|
|
||||||
if (hook_prefix == nonascii_prefix) {
|
if (hook_prefix == nonascii_prefix) {
|
||||||
/* don't allow legacy init for non-ASCII module names */
|
/* don't allow legacy init for non-ASCII module names */
|
||||||
PyErr_Format(
|
PyErr_Format(
|
||||||
|
@ -565,6 +565,10 @@ init_interp_settings(PyInterpreterState *interp, const _PyInterpreterConfig *con
|
|||||||
if (config->allow_daemon_threads) {
|
if (config->allow_daemon_threads) {
|
||||||
interp->feature_flags |= Py_RTFLAGS_DAEMON_THREADS;
|
interp->feature_flags |= Py_RTFLAGS_DAEMON_THREADS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config->check_multi_interp_extensions) {
|
||||||
|
interp->feature_flags |= Py_RTFLAGS_MULTI_INTERP_EXTENSIONS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user