Skip to content

Commit 4541d1a

Browse files
gh-104310: Add importlib.util.allowing_all_extensions() (gh-104311)
(I'll be adding docs for this separately.)
1 parent 5c9ee49 commit 4541d1a

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

Lib/importlib/util.py

+37
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,43 @@ def find_spec(name, package=None):
112112
return spec
113113

114114

115+
# Normally we would use contextlib.contextmanager. However, this module
116+
# is imported by runpy, which means we want to avoid any unnecessary
117+
# dependencies. Thus we use a class.
118+
119+
class allowing_all_extensions:
120+
"""A context manager that lets users skip the compatibility check.
121+
122+
Normally, extensions that do not support multiple interpreters
123+
may not be imported in a subinterpreter. That implies modules
124+
that do not implement multi-phase init.
125+
126+
Likewise for modules import in a subinterpeter with its own GIL
127+
when the extension does not support a per-interpreter GIL. This
128+
implies the module does not have a Py_mod_multiple_interpreters slot
129+
set to Py_MOD_PER_INTERPRETER_GIL_SUPPORTED.
130+
131+
In both cases, this context manager may be used to temporarily
132+
disable the check for compatible extension modules.
133+
"""
134+
135+
def __init__(self, disable_check=True):
136+
self.disable_check = disable_check
137+
138+
def __enter__(self):
139+
self.old = _imp._override_multi_interp_extensions_check(self.override)
140+
return self
141+
142+
def __exit__(self, *args):
143+
old = self.old
144+
del self.old
145+
_imp._override_multi_interp_extensions_check(old)
146+
147+
@property
148+
def override(self):
149+
return -1 if self.disable_check else 1
150+
151+
115152
class _LazyModule(types.ModuleType):
116153

117154
"""A subclass of the module type which triggers loading upon attribute access."""

Lib/test/support/import_helper.py

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ def multi_interp_extensions_check(enabled=True):
115115
It overrides the PyInterpreterConfig.check_multi_interp_extensions
116116
setting (see support.run_in_subinterp_with_config() and
117117
_xxsubinterpreters.create()).
118+
119+
Also see importlib.utils.allowing_all_extensions().
118120
"""
119121
old = _imp._override_multi_interp_extensions_check(1 if enabled else -1)
120122
try:

Lib/test/test_importlib/test_util.py

+121
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,29 @@
88
import importlib.util
99
import os
1010
import pathlib
11+
import re
1112
import string
1213
import sys
1314
from test import support
15+
import textwrap
1416
import types
1517
import unittest
1618
import unittest.mock
1719
import warnings
1820

21+
try:
22+
import _testsinglephase
23+
except ImportError:
24+
_testsinglephase = None
25+
try:
26+
import _testmultiphase
27+
except ImportError:
28+
_testmultiphase = None
29+
try:
30+
import _xxsubinterpreters as _interpreters
31+
except ModuleNotFoundError:
32+
_interpreters = None
33+
1934

2035
class DecodeSourceBytesTests:
2136

@@ -637,5 +652,111 @@ def test_magic_number(self):
637652
self.assertEqual(EXPECTED_MAGIC_NUMBER, actual, msg)
638653

639654

655+
@unittest.skipIf(_interpreters is None, 'subinterpreters required')
656+
class AllowingAllExtensionsTests(unittest.TestCase):
657+
658+
ERROR = re.compile("^<class 'ImportError'>: module (.*) does not support loading in subinterpreters")
659+
660+
def run_with_own_gil(self, script):
661+
interpid = _interpreters.create(isolated=True)
662+
try:
663+
_interpreters.run_string(interpid, script)
664+
except _interpreters.RunFailedError as exc:
665+
if m := self.ERROR.match(str(exc)):
666+
modname, = m.groups()
667+
raise ImportError(modname)
668+
669+
def run_with_shared_gil(self, script):
670+
interpid = _interpreters.create(isolated=False)
671+
try:
672+
_interpreters.run_string(interpid, script)
673+
except _interpreters.RunFailedError as exc:
674+
if m := self.ERROR.match(str(exc)):
675+
modname, = m.groups()
676+
raise ImportError(modname)
677+
678+
@unittest.skipIf(_testsinglephase is None, "test requires _testsinglephase module")
679+
def test_single_phase_init_module(self):
680+
script = textwrap.dedent('''
681+
import importlib.util
682+
with importlib.util.allowing_all_extensions():
683+
import _testsinglephase
684+
''')
685+
with self.subTest('check disabled, shared GIL'):
686+
self.run_with_shared_gil(script)
687+
with self.subTest('check disabled, per-interpreter GIL'):
688+
self.run_with_own_gil(script)
689+
690+
script = textwrap.dedent(f'''
691+
import importlib.util
692+
with importlib.util.allowing_all_extensions(False):
693+
import _testsinglephase
694+
''')
695+
with self.subTest('check enabled, shared GIL'):
696+
with self.assertRaises(ImportError):
697+
self.run_with_shared_gil(script)
698+
with self.subTest('check enabled, per-interpreter GIL'):
699+
with self.assertRaises(ImportError):
700+
self.run_with_own_gil(script)
701+
702+
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
703+
def test_incomplete_multi_phase_init_module(self):
704+
prescript = textwrap.dedent(f'''
705+
from importlib.util import spec_from_loader, module_from_spec
706+
from importlib.machinery import ExtensionFileLoader
707+
708+
name = '_test_shared_gil_only'
709+
filename = {_testmultiphase.__file__!r}
710+
loader = ExtensionFileLoader(name, filename)
711+
spec = spec_from_loader(name, loader)
712+
713+
''')
714+
715+
script = prescript + textwrap.dedent('''
716+
import importlib.util
717+
with importlib.util.allowing_all_extensions():
718+
module = module_from_spec(spec)
719+
loader.exec_module(module)
720+
''')
721+
with self.subTest('check disabled, shared GIL'):
722+
self.run_with_shared_gil(script)
723+
with self.subTest('check disabled, per-interpreter GIL'):
724+
self.run_with_own_gil(script)
725+
726+
script = prescript + textwrap.dedent('''
727+
import importlib.util
728+
with importlib.util.allowing_all_extensions(False):
729+
module = module_from_spec(spec)
730+
loader.exec_module(module)
731+
''')
732+
with self.subTest('check enabled, shared GIL'):
733+
self.run_with_shared_gil(script)
734+
with self.subTest('check enabled, per-interpreter GIL'):
735+
with self.assertRaises(ImportError):
736+
self.run_with_own_gil(script)
737+
738+
@unittest.skipIf(_testmultiphase is None, "test requires _testmultiphase module")
739+
def test_complete_multi_phase_init_module(self):
740+
script = textwrap.dedent('''
741+
import importlib.util
742+
with importlib.util.allowing_all_extensions():
743+
import _testmultiphase
744+
''')
745+
with self.subTest('check disabled, shared GIL'):
746+
self.run_with_shared_gil(script)
747+
with self.subTest('check disabled, per-interpreter GIL'):
748+
self.run_with_own_gil(script)
749+
750+
script = textwrap.dedent(f'''
751+
import importlib.util
752+
with importlib.util.allowing_all_extensions(False):
753+
import _testmultiphase
754+
''')
755+
with self.subTest('check enabled, shared GIL'):
756+
self.run_with_shared_gil(script)
757+
with self.subTest('check enabled, per-interpreter GIL'):
758+
self.run_with_own_gil(script)
759+
760+
640761
if __name__ == '__main__':
641762
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Users may now use ``importlib.util.allowing_all_extensions()`` (a context
2+
manager) to temporarily disable the strict compatibility checks for
3+
importing extension modules in subinterpreters.

0 commit comments

Comments
 (0)