Skip to content

Commit ec4d917

Browse files
bpo-40173: Fix test.support.import_helper.import_fresh_module() (GH-28654)
* Work correctly if an additional fresh module imports other additional fresh module which imports a blocked module. * Raises ImportError if the specified module cannot be imported while all additional fresh modules are successfully imported. * Support blocking packages. * Always restore the import state of fresh and blocked modules and their submodules. * Fix test_decimal and test_xml_etree which depended on an undesired side effect of import_fresh_module().
1 parent b07fddd commit ec4d917

File tree

4 files changed

+32
-52
lines changed

4 files changed

+32
-52
lines changed

Lib/test/support/import_helper.py

+24-43
Original file line numberDiff line numberDiff line change
@@ -81,33 +81,13 @@ def import_module(name, deprecated=False, *, required_on=()):
8181
raise unittest.SkipTest(str(msg))
8282

8383

84-
def _save_and_remove_module(name, orig_modules):
85-
"""Helper function to save and remove a module from sys.modules
86-
87-
Raise ImportError if the module can't be imported.
88-
"""
89-
# try to import the module and raise an error if it can't be imported
90-
if name not in sys.modules:
91-
__import__(name)
92-
del sys.modules[name]
84+
def _save_and_remove_modules(names):
85+
orig_modules = {}
86+
prefixes = tuple(name + '.' for name in names)
9387
for modname in list(sys.modules):
94-
if modname == name or modname.startswith(name + '.'):
95-
orig_modules[modname] = sys.modules[modname]
96-
del sys.modules[modname]
97-
98-
99-
def _save_and_block_module(name, orig_modules):
100-
"""Helper function to save and block a module in sys.modules
101-
102-
Return True if the module was in sys.modules, False otherwise.
103-
"""
104-
saved = True
105-
try:
106-
orig_modules[name] = sys.modules[name]
107-
except KeyError:
108-
saved = False
109-
sys.modules[name] = None
110-
return saved
88+
if modname in names or modname.startswith(prefixes):
89+
orig_modules[modname] = sys.modules.pop(modname)
90+
return orig_modules
11191

11292

11393
@contextlib.contextmanager
@@ -136,7 +116,8 @@ def import_fresh_module(name, fresh=(), blocked=(), *,
136116
this operation.
137117
138118
*fresh* is an iterable of additional module names that are also removed
139-
from the sys.modules cache before doing the import.
119+
from the sys.modules cache before doing the import. If one of these
120+
modules can't be imported, None is returned.
140121
141122
*blocked* is an iterable of module names that are replaced with None
142123
in the module cache during the import to ensure that attempts to import
@@ -160,25 +141,25 @@ def import_fresh_module(name, fresh=(), blocked=(), *,
160141
with _ignore_deprecated_imports(deprecated):
161142
# Keep track of modules saved for later restoration as well
162143
# as those which just need a blocking entry removed
163-
orig_modules = {}
164-
names_to_remove = []
165-
_save_and_remove_module(name, orig_modules)
144+
fresh = list(fresh)
145+
blocked = list(blocked)
146+
names = {name, *fresh, *blocked}
147+
orig_modules = _save_and_remove_modules(names)
148+
for modname in blocked:
149+
sys.modules[modname] = None
150+
166151
try:
167-
for fresh_name in fresh:
168-
_save_and_remove_module(fresh_name, orig_modules)
169-
for blocked_name in blocked:
170-
if not _save_and_block_module(blocked_name, orig_modules):
171-
names_to_remove.append(blocked_name)
172152
with frozen_modules(usefrozen):
173-
fresh_module = importlib.import_module(name)
174-
except ImportError:
175-
fresh_module = None
153+
# Return None when one of the "fresh" modules can not be imported.
154+
try:
155+
for modname in fresh:
156+
__import__(modname)
157+
except ImportError:
158+
return None
159+
return importlib.import_module(name)
176160
finally:
177-
for orig_name, module in orig_modules.items():
178-
sys.modules[orig_name] = module
179-
for name_to_remove in names_to_remove:
180-
del sys.modules[name_to_remove]
181-
return fresh_module
161+
_save_and_remove_modules(names)
162+
sys.modules.update(orig_modules)
182163

183164

184165
class CleanImport(object):

Lib/test/test_decimal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
C = import_fresh_module('decimal', fresh=['_decimal'])
6464
P = import_fresh_module('decimal', blocked=['_decimal'])
65-
orig_sys_decimal = sys.modules['decimal']
65+
import decimal as orig_sys_decimal
6666

6767
# fractions module must import the correct decimal module.
6868
cfractions = import_fresh_module('fractions', fresh=['fractions'])

Lib/test/test_xml_etree.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from test import support
2727
from test.support import os_helper
2828
from test.support import warnings_helper
29-
from test.support import findfile, gc_collect, swap_attr
29+
from test.support import findfile, gc_collect, swap_attr, swap_item
3030
from test.support.import_helper import import_fresh_module
3131
from test.support.os_helper import TESTFN
3232

@@ -167,21 +167,18 @@ def setUpClass(cls):
167167
cls.modules = {pyET, ET}
168168

169169
def pickleRoundTrip(self, obj, name, dumper, loader, proto):
170-
save_m = sys.modules[name]
171170
try:
172-
sys.modules[name] = dumper
173-
temp = pickle.dumps(obj, proto)
174-
sys.modules[name] = loader
175-
result = pickle.loads(temp)
171+
with swap_item(sys.modules, name, dumper):
172+
temp = pickle.dumps(obj, proto)
173+
with swap_item(sys.modules, name, loader):
174+
result = pickle.loads(temp)
176175
except pickle.PicklingError as pe:
177176
# pyET must be second, because pyET may be (equal to) ET.
178177
human = dict([(ET, "cET"), (pyET, "pyET")])
179178
raise support.TestFailed("Failed to round-trip %r from %r to %r"
180179
% (obj,
181180
human.get(dumper, dumper),
182181
human.get(loader, loader))) from pe
183-
finally:
184-
sys.modules[name] = save_m
185182
return result
186183

187184
def assertEqualElements(self, alice, bob):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`test.support.import_helper.import_fresh_module`.
2+

0 commit comments

Comments
 (0)