Skip to content

Commit a65c868

Browse files
bpo-45020: Add -X frozen_modules=[on|off] to explicitly control use of frozen modules. (gh-28320)
Currently we freeze several modules into the runtime. For each of these modules it is essential to bootstrapping the runtime that they be frozen. Any other stdlib module that we later freeze into the runtime is not essential. We can just as well import from the .py file. This PR lets users explicitly choose which should be used, with the new "-X frozen_modules=[on|off]" CLI flag. The default is "off" for now. https://bugs.python.org/issue45020
1 parent 1aaa859 commit a65c868

File tree

16 files changed

+359
-117
lines changed

16 files changed

+359
-117
lines changed

Doc/using/cmdline.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,12 @@ Miscellaneous options
480480
objects and pyc files are desired as well as supressing the extra visual
481481
location indicators when the interpreter displays tracebacks. See also
482482
:envvar:`PYTHONNODEBUGRANGES`.
483+
* ``-X frozen_modules`` determines whether or not frozen modules are
484+
ignored by the import machinery. A value of "on" means they get
485+
imported and "off" means they are ignored. The default is "on"
486+
for non-debug builds (the normal case) and "off" for debug builds.
487+
Note that the "importlib_bootstrap" and "importlib_bootstrap_external"
488+
frozen modules are always used, even if this flag is set to "off".
483489

484490
It also allows passing arbitrary values and retrieving them through the
485491
:data:`sys._xoptions` dictionary.
@@ -518,6 +524,9 @@ Miscellaneous options
518524
.. versionadded:: 3.11
519525
The ``-X no_debug_ranges`` option.
520526

527+
.. versionadded:: 3.11
528+
The ``-X frozen_modules`` option.
529+
521530

522531
Options you shouldn't use
523532
~~~~~~~~~~~~~~~~~~~~~~~~~

Include/cpython/initconfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ typedef struct PyConfig {
172172
int legacy_windows_stdio;
173173
#endif
174174
wchar_t *check_hash_pycs_mode;
175+
int use_frozen_modules;
175176

176177
/* --- Path configuration inputs ------------ */
177178
int pathconfig_warnings;

Include/internal/pycore_initconfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ extern PyStatus _PyConfig_Copy(
155155
extern PyStatus _PyConfig_InitPathConfig(
156156
PyConfig *config,
157157
int compute_path_config);
158+
extern PyStatus _PyConfig_InitImportConfig(PyConfig *config);
158159
extern PyStatus _PyConfig_Read(PyConfig *config, int compute_path_config);
159160
extern PyStatus _PyConfig_Write(const PyConfig *config,
160161
struct pyruntimestate *runtime);

Include/internal/pycore_interp.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ struct _is {
246246
PyObject *builtins;
247247
// importlib module
248248
PyObject *importlib;
249+
// override for config->use_frozen_modules (for tests)
250+
// (-1: "off", 1: "on", 0: no override)
251+
int override_frozen_modules;
249252

250253
/* Used in Modules/_threadmodule.c. */
251254
long num_threads;

Lib/ctypes/test/test_values.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,16 @@ class struct_frozen(Structure):
7272
self.assertGreater(abs(entry.size), 10)
7373
self.assertTrue([entry.code[i] for i in range(abs(entry.size))])
7474
# Check the module's package-ness.
75-
spec = importlib.util.find_spec(modname)
75+
with import_helper.frozen_modules():
76+
spec = importlib.util.find_spec(modname)
7677
if entry.size < 0:
7778
# It's a package.
7879
self.assertIsNotNone(spec.submodule_search_locations)
7980
else:
8081
self.assertIsNone(spec.submodule_search_locations)
8182

82-
expected = imp._frozen_module_names()
83+
with import_helper.frozen_modules():
84+
expected = imp._frozen_module_names()
8385
self.maxDiff = None
8486
self.assertEqual(modules, expected, "PyImport_FrozenModules example "
8587
"in Doc/library/ctypes.rst may be out of date")

Lib/test/support/import_helper.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
import _imp
23
import importlib
34
import importlib.util
45
import os
@@ -109,7 +110,24 @@ def _save_and_block_module(name, orig_modules):
109110
return saved
110111

111112

112-
def import_fresh_module(name, fresh=(), blocked=(), deprecated=False):
113+
@contextlib.contextmanager
114+
def frozen_modules(enabled=True):
115+
"""Force frozen modules to be used (or not).
116+
117+
This only applies to modules that haven't been imported yet.
118+
Also, some essential modules will always be imported frozen.
119+
"""
120+
_imp._override_frozen_modules_for_tests(1 if enabled else -1)
121+
try:
122+
yield
123+
finally:
124+
_imp._override_frozen_modules_for_tests(0)
125+
126+
127+
def import_fresh_module(name, fresh=(), blocked=(), *,
128+
deprecated=False,
129+
usefrozen=False,
130+
):
113131
"""Import and return a module, deliberately bypassing sys.modules.
114132
115133
This function imports and returns a fresh copy of the named Python module
@@ -133,6 +151,9 @@ def import_fresh_module(name, fresh=(), blocked=(), deprecated=False):
133151
134152
This function will raise ImportError if the named module cannot be
135153
imported.
154+
155+
If "usefrozen" is False (the default) then the frozen importer is
156+
disabled (except for essential modules like importlib._bootstrap).
136157
"""
137158
# NOTE: test_heapq, test_json and test_warnings include extra sanity checks
138159
# to make sure that this utility function is working as expected
@@ -148,7 +169,8 @@ def import_fresh_module(name, fresh=(), blocked=(), deprecated=False):
148169
for blocked_name in blocked:
149170
if not _save_and_block_module(blocked_name, orig_modules):
150171
names_to_remove.append(blocked_name)
151-
fresh_module = importlib.import_module(name)
172+
with frozen_modules(usefrozen):
173+
fresh_module = importlib.import_module(name)
152174
except ImportError:
153175
fresh_module = None
154176
finally:
@@ -169,9 +191,12 @@ class CleanImport(object):
169191
170192
with CleanImport("foo"):
171193
importlib.import_module("foo") # new reference
194+
195+
If "usefrozen" is False (the default) then the frozen importer is
196+
disabled (except for essential modules like importlib._bootstrap).
172197
"""
173198

174-
def __init__(self, *module_names):
199+
def __init__(self, *module_names, usefrozen=False):
175200
self.original_modules = sys.modules.copy()
176201
for module_name in module_names:
177202
if module_name in sys.modules:
@@ -183,12 +208,15 @@ def __init__(self, *module_names):
183208
if module.__name__ != module_name:
184209
del sys.modules[module.__name__]
185210
del sys.modules[module_name]
211+
self._frozen_modules = frozen_modules(usefrozen)
186212

187213
def __enter__(self):
214+
self._frozen_modules.__enter__()
188215
return self
189216

190217
def __exit__(self, *ignore_exc):
191218
sys.modules.update(self.original_modules)
219+
self._frozen_modules.__exit__(*ignore_exc)
192220

193221

194222
class DirsOnSysPath(object):

Lib/test/support/os_helper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,10 @@ def set(self, envvar, value):
599599
def unset(self, envvar):
600600
del self[envvar]
601601

602+
def copy(self):
603+
# We do what os.environ.copy() does.
604+
return dict(self)
605+
602606
def __enter__(self):
603607
return self
604608

Lib/test/test_embed.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import shutil
1313
import subprocess
1414
import sys
15+
import sysconfig
1516
import tempfile
1617
import textwrap
1718

@@ -426,6 +427,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
426427
'pathconfig_warnings': 1,
427428
'_init_main': 1,
428429
'_isolated_interpreter': 0,
430+
'use_frozen_modules': False,
429431
}
430432
if MS_WINDOWS:
431433
CONFIG_COMPAT.update({

Lib/test/test_frozen.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@
1212

1313
import sys
1414
import unittest
15-
from test.support import captured_stdout
15+
from test.support import captured_stdout, import_helper
1616

1717

1818
class TestFrozen(unittest.TestCase):
1919
def test_frozen(self):
2020
name = '__hello__'
2121
if name in sys.modules:
2222
del sys.modules[name]
23-
with captured_stdout() as out:
24-
import __hello__
23+
with import_helper.frozen_modules():
24+
with captured_stdout() as out:
25+
import __hello__
2526
self.assertEqual(out.getvalue(), 'Hello world!\n')
2627

2728

Lib/test/test_importlib/frozen/test_finder.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
import unittest
77
import warnings
88

9+
from test.support import import_helper
10+
911

1012
class FindSpecTests(abc.FinderTests):
1113

1214
"""Test finding frozen modules."""
1315

1416
def find(self, name, path=None):
1517
finder = self.machinery.FrozenImporter
16-
return finder.find_spec(name, path)
18+
with import_helper.frozen_modules():
19+
return finder.find_spec(name, path)
1720

1821
def test_module(self):
1922
name = '__hello__'
@@ -52,7 +55,8 @@ def find(self, name, path=None):
5255
finder = self.machinery.FrozenImporter
5356
with warnings.catch_warnings():
5457
warnings.simplefilter("ignore", DeprecationWarning)
55-
return finder.find_module(name, path)
58+
with import_helper.frozen_modules():
59+
return finder.find_module(name, path)
5660

5761
def test_module(self):
5862
name = '__hello__'

0 commit comments

Comments
 (0)