Skip to content

Commit c3d9ac8

Browse files
bpo-45324: Capture data in FrozenImporter.find_spec() to use in exec_module(). (gh-28633)
Before this change we end up duplicating effort and throwing away data in FrozenImporter.find_spec(). Now we do the work once in find_spec() and the only thing we do in FrozenImporter.exec_module() is turn the raw frozen data into a code object and then exec it. We've added _imp.find_frozen(), add an arg to _imp.get_frozen_object(), and updated FrozenImporter. We've also moved some code around to reduce duplication, get a little more consistency in outcomes, and be more efficient. Note that this change is mostly necessary if we want to set __file__ on frozen stdlib modules. (See https://bugs.python.org/issue21736.) https://bugs.python.org/issue45324
1 parent b9bb748 commit c3d9ac8

File tree

6 files changed

+332
-121
lines changed

6 files changed

+332
-121
lines changed

Lib/importlib/_bootstrap.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -826,10 +826,15 @@ def module_repr(m):
826826

827827
@classmethod
828828
def find_spec(cls, fullname, path=None, target=None):
829-
if _imp.is_frozen(fullname):
830-
return spec_from_loader(fullname, cls, origin=cls._ORIGIN)
831-
else:
829+
info = _call_with_frames_removed(_imp.find_frozen, fullname)
830+
if info is None:
832831
return None
832+
data, ispkg = info
833+
spec = spec_from_loader(fullname, cls,
834+
origin=cls._ORIGIN,
835+
is_package=ispkg)
836+
spec.loader_state = data
837+
return spec
833838

834839
@classmethod
835840
def find_module(cls, fullname, path=None):
@@ -849,11 +854,22 @@ def create_module(spec):
849854

850855
@staticmethod
851856
def exec_module(module):
852-
name = module.__spec__.name
853-
if not _imp.is_frozen(name):
854-
raise ImportError('{!r} is not a frozen module'.format(name),
855-
name=name)
856-
code = _call_with_frames_removed(_imp.get_frozen_object, name)
857+
spec = module.__spec__
858+
name = spec.name
859+
try:
860+
data = spec.loader_state
861+
except AttributeError:
862+
if not _imp.is_frozen(name):
863+
raise ImportError('{!r} is not a frozen module'.format(name),
864+
name=name)
865+
data = None
866+
else:
867+
# We clear the extra data we got from the finder, to save memory.
868+
# Note that if this method is called again (e.g. by
869+
# importlib.reload()) then _imp.get_frozen_object() will notice
870+
# no data was provided and will look it up.
871+
spec.loader_state = None
872+
code = _call_with_frames_removed(_imp.get_frozen_object, name, data)
857873
exec(code, module.__dict__)
858874

859875
@classmethod

Lib/test/test_importlib/frozen/test_finder.py

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from .. import abc
2-
import os.path
32
from .. import util
43

54
machinery = util.import_importlib('importlib.machinery')
65

6+
import _imp
7+
import marshal
8+
import os.path
79
import unittest
810
import warnings
911

10-
from test.support import import_helper
12+
from test.support import import_helper, REPO_ROOT
1113

1214

1315
class FindSpecTests(abc.FinderTests):
@@ -19,39 +21,67 @@ def find(self, name, **kwargs):
1921
with import_helper.frozen_modules():
2022
return finder.find_spec(name, **kwargs)
2123

22-
def check(self, spec, name):
24+
def check_basic(self, spec, name, ispkg=False):
2325
self.assertEqual(spec.name, name)
2426
self.assertIs(spec.loader, self.machinery.FrozenImporter)
2527
self.assertEqual(spec.origin, 'frozen')
2628
self.assertFalse(spec.has_location)
29+
if ispkg:
30+
self.assertIsNotNone(spec.submodule_search_locations)
31+
else:
32+
self.assertIsNone(spec.submodule_search_locations)
33+
self.assertIsNotNone(spec.loader_state)
34+
35+
def check_search_location(self, spec, source=None):
36+
# Frozen packages do not have any path entries.
37+
# (See https://bugs.python.org/issue21736.)
38+
expected = []
39+
self.assertListEqual(spec.submodule_search_locations, expected)
40+
41+
def check_data(self, spec, source=None, ispkg=None):
42+
with import_helper.frozen_modules():
43+
expected = _imp.get_frozen_object(spec.name)
44+
data = spec.loader_state
45+
# We can't compare the marshaled data directly because
46+
# marshal.dumps() would mark "expected" as a ref, which slightly
47+
# changes the output. (See https://bugs.python.org/issue34093.)
48+
code = marshal.loads(data)
49+
self.assertEqual(code, expected)
2750

2851
def test_module(self):
29-
names = [
30-
'__hello__',
31-
'__hello_alias__',
32-
'__hello_only__',
33-
'__phello__.__init__',
34-
'__phello__.spam',
35-
'__phello__.ham.__init__',
36-
'__phello__.ham.eggs',
37-
]
38-
for name in names:
52+
modules = {
53+
'__hello__': None,
54+
'__phello__.__init__': None,
55+
'__phello__.spam': None,
56+
'__phello__.ham.__init__': None,
57+
'__phello__.ham.eggs': None,
58+
'__hello_alias__': '__hello__',
59+
}
60+
for name, source in modules.items():
3961
with self.subTest(name):
4062
spec = self.find(name)
41-
self.check(spec, name)
42-
self.assertEqual(spec.submodule_search_locations, None)
63+
self.check_basic(spec, name)
64+
self.check_data(spec, source)
4365

4466
def test_package(self):
45-
names = [
46-
'__phello__',
47-
'__phello__.ham',
48-
'__phello_alias__',
49-
]
50-
for name in names:
67+
modules = {
68+
'__phello__': None,
69+
'__phello__.ham': None,
70+
'__phello_alias__': '__hello__',
71+
}
72+
for name, source in modules.items():
5173
with self.subTest(name):
5274
spec = self.find(name)
53-
self.check(spec, name)
54-
self.assertEqual(spec.submodule_search_locations, [])
75+
self.check_basic(spec, name, ispkg=True)
76+
self.check_search_location(spec, source)
77+
self.check_data(spec, source, ispkg=True)
78+
79+
def test_frozen_only(self):
80+
name = '__hello_only__'
81+
source = os.path.join(REPO_ROOT, 'Tools', 'freeze', 'flag.py')
82+
spec = self.find(name)
83+
self.check_basic(spec, name)
84+
self.check_data(spec, source)
5585

5686
# These are covered by test_module() and test_package().
5787
test_module_in_package = None

Lib/test/test_importlib/frozen/test_loader.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
machinery = util.import_importlib('importlib.machinery')
55

66
from test.support import captured_stdout, import_helper
7+
import _imp
78
import contextlib
9+
import marshal
810
import types
911
import unittest
1012
import warnings
@@ -33,11 +35,14 @@ class ExecModuleTests(abc.LoaderTests):
3335
def exec_module(self, name):
3436
with import_helper.frozen_modules():
3537
is_package = self.machinery.FrozenImporter.is_package(name)
38+
code = _imp.get_frozen_object(name)
39+
data = marshal.dumps(code)
3640
spec = self.machinery.ModuleSpec(
3741
name,
3842
self.machinery.FrozenImporter,
3943
origin='frozen',
4044
is_package=is_package,
45+
loader_state=data,
4146
)
4247
module = types.ModuleType(name)
4348
module.__spec__ = spec
@@ -61,6 +66,7 @@ def test_module(self):
6166
self.assertEqual(getattr(module, attr), value)
6267
self.assertEqual(output, 'Hello world!\n')
6368
self.assertTrue(hasattr(module, '__spec__'))
69+
self.assertIsNone(module.__spec__.loader_state)
6470

6571
def test_package(self):
6672
name = '__phello__'
@@ -73,6 +79,7 @@ def test_package(self):
7379
name=name, attr=attr, given=attr_value,
7480
expected=value))
7581
self.assertEqual(output, 'Hello world!\n')
82+
self.assertIsNone(module.__spec__.loader_state)
7683

7784
def test_lacking_parent(self):
7885
name = '__phello__.spam'
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
In FrozenImporter.find_spec(), we now preserve the information needed in
2+
exec_module() to load the module. This change mostly impacts internal
3+
details, rather than changing the importer's behavior.

Python/clinic/import.c.h

Lines changed: 57 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)