Skip to content

Commit 39fa69c

Browse files
committed
install: Support symlinking .pyc files
Signed-off-by: Michał Górny <mgorny@gentoo.org>
1 parent 6d44fd9 commit 39fa69c

File tree

3 files changed

+55
-5
lines changed

3 files changed

+55
-5
lines changed

gpep517/__main__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ def add_install_args(parser):
157157
group.add_argument("--overwrite",
158158
action="store_true",
159159
help="Permit overwriting files in destdir")
160+
group.add_argument("--symlink-pyc",
161+
action="store_true",
162+
help="Symlink .pyc files between optimization levels "
163+
"if their contents match")
164+
160165
group.add_argument("--symlink-to",
161166
type=PurePath,
162167
help="Install symlinks to another directory rather "

gpep517/install.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import filecmp
55
import functools
6+
import importlib.util
67
import os
78
import os.path
89
import typing
@@ -24,12 +25,14 @@ def __init__(
2425
self,
2526
*args,
2627
overwrite: bool,
28+
symlink_pyc: bool,
2729
symlink_to: typing.Optional[PurePath],
2830
**kwargs,
2931
) -> None:
3032
super().__init__(*args, **kwargs)
3133

3234
self.overwrite = overwrite
35+
self.symlink_pyc = symlink_pyc
3336
self.symlink_to = symlink_to
3437

3538
if symlink_to is not None:
@@ -101,6 +104,32 @@ def write_to_fs(
101104

102105
return ret
103106

107+
def _compile_bytecode(self,
108+
scheme: Scheme,
109+
record: RecordEntry,
110+
) -> None:
111+
super()._compile_bytecode(scheme, record)
112+
113+
if not self.symlink_pyc:
114+
return
115+
if scheme not in ("purelib", "platlib"):
116+
return
117+
if not record.path.endswith(".py"):
118+
return
119+
120+
relative_path = PurePath(self.scheme_dict[scheme]) / record.path
121+
py = Path(self.destdir).joinpath(
122+
relative_path.relative_to(relative_path.anchor))
123+
pycs = [
124+
Path(importlib.util.cache_from_source(
125+
py, optimization=opt if opt >= 1 else ""))
126+
for opt in self.bytecode_optimization_levels
127+
]
128+
for pyc1, pyc2 in zip(pycs, pycs[1:]):
129+
if filecmp.cmp(pyc1, pyc2):
130+
pyc2.unlink()
131+
pyc2.symlink_to(pyc1.name)
132+
104133
with WheelFile.open(wheel) as source:
105134
dest = DeduplicatingDestination(
106135
install_scheme_dict(args.prefix or DEFAULT_PREFIX,
@@ -110,6 +139,7 @@ def write_to_fs(
110139
bytecode_optimization_levels=args.optimize,
111140
destdir=str(args.destdir),
112141
overwrite=args.overwrite,
142+
symlink_pyc=args.symlink_pyc,
113143
symlink_to=args.symlink_to,
114144
)
115145
logger.info(f"Installing {wheel} into {args.destdir}")

test/test_install.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,22 @@ def all_files(top_path):
4343
@pytest.mark.parametrize("optimize", [None, "0", "1,2", "all"])
4444
@pytest.mark.parametrize("prefix", ["/usr", "/eprefix/usr"])
4545
@pytest.mark.parametrize("overwrite", [False, True])
46+
@pytest.mark.parametrize("symlink_pyc", [False, True])
4647
def test_install_wheel(tmp_path,
4748
optimize: typing.Optional[str],
4849
prefix: str,
49-
overwrite: bool):
50+
overwrite: bool,
51+
symlink_pyc: bool,
52+
):
5053
args = (["", "install-wheel",
5154
"--destdir", str(tmp_path),
5255
"--interpreter", "/usr/bin/pythontest",
5356
"test/test-pkg/dist/test-1-py3-none-any.whl"] +
5457
(["--prefix", prefix] if prefix != "/usr" else []) +
5558
(["--optimize", optimize]
56-
if optimize is not None else []))
59+
if optimize is not None else []) +
60+
(["--symlink-pyc"] if symlink_pyc else [])
61+
)
5762
assert 0 == main(args)
5863

5964
expected_overwrite = (contextlib.nullcontext() if overwrite
@@ -99,7 +104,10 @@ def test_install_wheel(tmp_path,
99104
pyc = importlib.util.cache_from_source(
100105
init_mod, optimization=opt if opt != 0 else "")
101106
expected[pathlib.Path(pyc)] = (
102-
nonexec, False, pathlib.Path(f"/{init_mod}"))
107+
nonexec,
108+
# our only symlinking opportunity is 1 -> 0
109+
symlink_pyc and opt == 1 and 0 in opt_levels,
110+
pathlib.Path(f"/{init_mod}"))
103111

104112
assert expected == dict(all_files(tmp_path))
105113

@@ -129,14 +137,18 @@ def test_install_self(tmp_path):
129137
"remove-dir",
130138
"modify-file",
131139
])
140+
@pytest.mark.parametrize("symlink_pyc", [False, True])
132141
def test_install_symlink_to(tmp_path,
133142
optimize: typing.Optional[str],
134143
modification: str,
144+
symlink_pyc: bool,
135145
) -> None:
136146
args = (["", "install-wheel",
137147
"--destdir", str(tmp_path),
138148
"test/symlink-pkg/dist/foo-0-py3-none-any.whl"] +
139-
(["--optimize", optimize] if optimize is not None else []))
149+
(["--optimize", optimize] if optimize is not None else []) +
150+
(["--symlink-pyc"] if symlink_pyc else [])
151+
)
140152
assert 0 == main(args + ["--prefix", "/first"])
141153

142154
sitedir = pathlib.PurePath(
@@ -196,7 +208,10 @@ def test_install_symlink_to(tmp_path,
196208
pyc = importlib.util.cache_from_source(
197209
path, optimization=opt if opt != 0 else "")
198210
expected[pathlib.Path(pyc)] = (
199-
False, False, pathlib.Path("/", path))
211+
False,
212+
symlink_pyc and
213+
(opt == 1 or (opt != 0 and path.name == "__init__.py")),
214+
pathlib.Path("/", path))
200215

201216
if modification == "remove-files":
202217
del expected[pathlib.Path(f"first/{sitedir}/foo/a.py")]

0 commit comments

Comments
 (0)