Skip to content

Commit ea37e5f

Browse files
authored
Make SoImport importable (#229)
* move a bunch of stuff to plz.py * Add it to the zipfile * version
1 parent 79abc76 commit ea37e5f

File tree

7 files changed

+193
-177
lines changed

7 files changed

+193
-177
lines changed

tools/ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Version 1.6.0
2+
-------------
3+
* Import hooks are now added in the `plz` module and are hence more usefully importable (#229)
4+
15
Version 1.5.5
26
-------------
37
* Fix get_code on ModuleDirImport (#226)

tools/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.5.5
1+
1.6.0

tools/please_pex/pex/pex.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,14 @@ func (pw *Writer) Write(out, moduleDir string) error {
185185
}
186186
}
187187

188+
// Write plz.py which contains much of our import hooks etc
189+
b := mustRead("plz.py")
190+
if err := f.WriteFile(".bootstrap/plz.py", b, 0644); err != nil {
191+
return err
192+
}
193+
188194
// Always write pex_main.py, with some templating.
189-
b := mustRead("pex_main.py")
195+
b = mustRead("pex_main.py")
190196
b = bytes.Replace(b, []byte("__MODULE_DIR__"), []byte(strings.ReplaceAll(moduleDir, ".", "/")), 1)
191197
b = bytes.Replace(b, []byte("__ENTRY_POINT__"), []byte(pw.realEntryPoint), 1)
192198
b = bytes.Replace(b, []byte("__ZIP_SAFE__"), []byte(pythonBool(pw.zipSafe)), 1)

tools/please_pex/pex/pex_main.py

Lines changed: 9 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
"""Zipfile entry point which supports auto-extracting itself based on zip-safety."""
22

3-
from collections import defaultdict
4-
from importlib import import_module, machinery
5-
from importlib.abc import MetaPathFinder
6-
from importlib.metadata import Distribution
7-
from importlib.util import spec_from_loader
8-
from site import getsitepackages
9-
import itertools
103
import os
11-
import re
124
import runpy
135
import sys
14-
import tempfile
15-
import zipfile
166

177
# Put this pex on the path before anything else.
188
PEX = os.path.abspath(sys.argv[0])
@@ -26,168 +16,18 @@
2616
ZIP_SAFE = __ZIP_SAFE__
2717
PEX_STAMP = '__PEX_STAMP__'
2818

29-
# Workaround for https://bugs.python.org/issue15795
30-
class ZipFileWithPermissions(zipfile.ZipFile):
31-
""" Custom ZipFile class handling file permissions. """
32-
33-
def _extract_member(self, member, targetpath, pwd):
34-
if not isinstance(member, zipfile.ZipInfo):
35-
member = self.getinfo(member)
36-
37-
targetpath = super(ZipFileWithPermissions, self)._extract_member(
38-
member, targetpath, pwd
39-
)
40-
41-
attr = member.external_attr >> 16
42-
if attr != 0:
43-
os.chmod(targetpath, attr)
44-
return targetpath
45-
46-
class SoImport(MetaPathFinder):
47-
"""So import. Much binary. Such dynamic. Wow."""
48-
49-
def __init__(self):
50-
self.suffixes = machinery.EXTENSION_SUFFIXES # list, as importlib will not be using the file description
51-
self.suffixes_by_length = sorted(self.suffixes, key=lambda x: -len(x))
52-
# Identify all the possible modules we could handle.
53-
self.modules = {}
54-
if zipfile.is_zipfile(sys.argv[0]):
55-
zf = ZipFileWithPermissions(sys.argv[0])
56-
for name in zf.namelist():
57-
path, _ = self.splitext(name)
58-
if path:
59-
if path.startswith('.bootstrap/'):
60-
path = path[len('.bootstrap/'):]
61-
importpath = path.replace('/', '.')
62-
self.modules.setdefault(importpath, name)
63-
if path.startswith(MODULE_DIR):
64-
self.modules.setdefault(importpath[len(MODULE_DIR)+1:], name)
65-
if self.modules:
66-
self.zf = zf
67-
68-
def find_spec(self, name, path, target=None):
69-
"""Implements abc.MetaPathFinder."""
70-
if name in self.modules:
71-
return spec_from_loader(name, self)
72-
73-
def create_module(self, spec):
74-
"""Create a module object that we're going to load."""
75-
filename = self.modules[spec.name]
76-
prefix, ext = self.splitext(filename)
77-
with tempfile.NamedTemporaryFile(suffix=ext, prefix=os.path.basename(prefix)) as f:
78-
f.write(self.zf.read(filename))
79-
f.flush()
80-
spec.origin = f.name
81-
loader = machinery.ExtensionFileLoader(spec.name, f.name)
82-
spec.loader = loader
83-
mod = loader.create_module(spec)
84-
# Make it look like module came from the original location for nicer tracebacks.
85-
mod.__file__ = filename
86-
return mod
87-
88-
def exec_module(self, mod):
89-
"""Because we set spec.loader above, the ExtensionFileLoader's exec_module is called."""
90-
raise NotImplementedError("SoImport.exec_module isn't used")
91-
92-
def splitext(self, path):
93-
"""Similar to os.path.splitext, but splits our longest known suffix preferentially."""
94-
for suffix in self.suffixes_by_length:
95-
if path.endswith(suffix):
96-
return path[:-len(suffix)], suffix
97-
return None, None
98-
99-
100-
class PexDistribution(Distribution):
101-
"""Represents a distribution package that exists within a pex file (which is, ultimately, a zip
102-
file). Distribution packages are identified by the presence of a suitable dist-info or egg-info
103-
directory member inside the pex file, which need not necessarily exist at the top level if a
104-
directory prefix is specified in the constructor.
105-
"""
106-
def __init__(self, name, pex_file, zip_file, files, prefix):
107-
self._name = name
108-
self._zf = zip_file
109-
self._pex_file = pex_file
110-
self._prefix = prefix
111-
# Mapping of <path within distribution> -> <full path in zipfile>
112-
self._files = files
113-
114-
def read_text(self, filename):
115-
full_name = self._files.get(filename)
116-
if full_name:
117-
return self._zf.read(full_name).decode(encoding="utf-8")
118-
119-
def locate_file(self, path):
120-
return zipfile.Path(
121-
self._pex_file,
122-
at=os.path.join(self._prefix, path) if self._prefix else path,
123-
)
124-
125-
read_text.__doc__ = Distribution.read_text.__doc__
126-
127-
128-
class ModuleDirImport(MetaPathFinder):
129-
"""Handles imports to a directory equivalently to them being at the top level.
130-
131-
This means that if one writes `import third_party.python.six`, it's imported like `import six`,
132-
but becomes accessible under both names. This handles both the fully-qualified import names
133-
and packages importing as their expected top-level names internally.
134-
"""
135-
def __init__(self, module_dir=MODULE_DIR):
136-
self.prefix = module_dir.replace("/", ".") + "."
137-
self._distributions = self._find_all_distributions(module_dir)
138-
139-
def _find_all_distributions(self, module_dir):
140-
pex_file = sys.argv[0]
141-
if zipfile.is_zipfile(pex_file):
142-
zf = ZipFileWithPermissions(pex_file)
143-
r = re.compile(r"{module_dir}{sep}([^/]+)-[^/-]+?\.(?:dist|egg)-info/(.*)".format(
144-
module_dir=module_dir,
145-
sep = os.sep,
146-
))
147-
filenames = defaultdict(dict)
148-
for name in zf.namelist():
149-
match = r.match(name)
150-
if match:
151-
filenames[match.group(1)][match.group(2)] = name
152-
return {mod: [PexDistribution(mod, pex_file, zf, files, prefix=module_dir)]
153-
for mod, files in filenames.items()}
154-
return {}
155-
156-
def find_spec(self, name, path, target=None):
157-
"""Implements abc.MetaPathFinder."""
158-
if name.startswith(self.prefix):
159-
return spec_from_loader(name, self)
160-
161-
def create_module(self, spec):
162-
"""Actually load a module that we said we'd handle in find_module."""
163-
module = import_module(spec.name.removeprefix(self.prefix))
164-
sys.modules[spec.name] = module
165-
return module
166-
167-
def exec_module(self, mod):
168-
"""Nothing to do, create_module already did the work."""
169-
170-
def find_distributions(self, context):
171-
"""Return an iterable of all Distribution instances capable of
172-
loading the metadata for packages for the indicated ``context``.
173-
"""
174-
if context.name:
175-
# The installed directories have underscores in the place of what might be a hyphen
176-
# in the package name (e.g. the package opentelemetry-sdk installs opentelemetry_sdk).
177-
return self._distributions.get(context.name.replace("-", "_"), [])
178-
else:
179-
return itertools.chain(*self._distributions.values())
180-
181-
def get_code(self, fullname):
182-
module = import_module(fullname.removeprefix(self.prefix))
183-
return module.__loader__.get_code(fullname)
184-
185-
186-
def add_module_dir_to_sys_path(dirname):
19+
20+
def add_module_dir_to_sys_path(dirname, zip_safe=True):
18721
"""Adds the given dirname to sys.path if it's nonempty."""
22+
# Add .bootstrap dir to path, after the initial pex entry
23+
sys.path = sys.path[:1] + [os.path.join(sys.path[0], '.bootstrap')] + sys.path[1:]
24+
# Now we have .bootstrap on the path, we can import our own hooks.
25+
import plz
18826
if dirname:
18927
sys.path = sys.path[:1] + [os.path.join(sys.path[0], dirname)] + sys.path[1:]
190-
sys.meta_path.insert(0, ModuleDirImport(dirname))
28+
sys.meta_path.insert(0, plz.ModuleDirImport(dirname))
29+
if zip_safe:
30+
sys.meta_path.append(plz.SoImport(MODULE_DIR))
19131

19232

19333
def pex_basepath(temp=False):
@@ -297,8 +137,6 @@ def main():
297137
298138
N.B. This gets redefined by pex_test_main to run tests instead.
299139
"""
300-
# Add .bootstrap dir to path, after the initial pex entry
301-
sys.path = sys.path[:1] + [os.path.join(sys.path[0], '.bootstrap')] + sys.path[1:]
302140
# Starts a debugging session, if defined, before running the entry point.
303141
if os.getenv("PLZ_DEBUG") is not None:
304142
start_debugger()

tools/please_pex/pex/pex_run.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
def run(explode=False):
22
if explode or not ZIP_SAFE:
33
with explode_zip()():
4-
add_module_dir_to_sys_path(MODULE_DIR)
4+
add_module_dir_to_sys_path(MODULE_DIR, zip_safe=False)
55
return main()
66
else:
77
add_module_dir_to_sys_path(MODULE_DIR)
8-
sys.meta_path.append(SoImport())
98
return main()
109

1110

tools/please_pex/pex/pex_test_main.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ def _xml_file(self, fr, analysis, *args, **kvargs):
3131
def main():
3232
"""Runs the tests. Returns an appropriate exit code."""
3333
args = [arg for arg in sys.argv[1:]]
34-
# Add .bootstrap dir to path, after the initial pex entry
35-
sys.path = sys.path[:1] + [os.path.join(sys.path[0], '.bootstrap')] + sys.path[1:]
3634
if os.getenv('COVERAGE'):
3735
# It's important that we run coverage while we load the tests otherwise
3836
# we get no coverage for import statements etc.

0 commit comments

Comments
 (0)