From fcc0d6f14f99974316c2e490cead07a9a0b7a6ac Mon Sep 17 00:00:00 2001 From: Jeremy Volkman Date: Wed, 24 Jan 2024 05:28:11 -0800 Subject: [PATCH] Add CLI support for installing multiple wheels (#203) * Add CLI support for installing multiple wheels --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/cli/installer.md | 3 +- src/installer/__main__.py | 25 +++++++-------- tests/conftest.py | 65 ++++++++++++++++++++++----------------- tests/test_main.py | 19 ++++++++++++ 4 files changed, 71 insertions(+), 41 deletions(-) diff --git a/docs/cli/installer.md b/docs/cli/installer.md index d0a54e24..c862546f 100644 --- a/docs/cli/installer.md +++ b/docs/cli/installer.md @@ -1,6 +1,7 @@ # `python -m installer` -This interface allows you to install a specific wheel into a Python interpreter. +This interface allows you to install one or more wheels into a Python +interpreter. ```{argparse} :module: installer.__main__ diff --git a/src/installer/__main__.py b/src/installer/__main__.py index a94ad6c8..7ece8d21 100644 --- a/src/installer/__main__.py +++ b/src/installer/__main__.py @@ -15,7 +15,7 @@ def _get_main_parser() -> argparse.ArgumentParser: """Construct the main parser.""" parser = argparse.ArgumentParser() - parser.add_argument("wheel", type=str, help="wheel file to install") + parser.add_argument("wheel", type=str, nargs="+", help="wheel file to install") parser.add_argument( "--destdir", "-d", @@ -91,17 +91,18 @@ def _main(cli_args: Sequence[str], program: Optional[str] = None) -> None: elif not bytecode_levels: bytecode_levels = [0, 1] - with WheelFile.open(args.wheel) as source: - if args.validate_record != "none": - source.validate_record(validate_contents=args.validate_record == "all") - destination = SchemeDictionaryDestination( - scheme_dict=_get_scheme_dict(source.distribution, prefix=args.prefix), - interpreter=sys.executable, - script_kind=get_launcher_kind(), - bytecode_optimization_levels=bytecode_levels, - destdir=args.destdir, - ) - installer.install(source, destination, {}) + for wheel in args.wheel: + with WheelFile.open(wheel) as source: + if args.validate_record != "none": + source.validate_record(validate_contents=args.validate_record == "all") + destination = SchemeDictionaryDestination( + scheme_dict=_get_scheme_dict(source.distribution, prefix=args.prefix), + interpreter=sys.executable, + script_kind=get_launcher_kind(), + bytecode_optimization_levels=bytecode_levels, + destdir=args.destdir, + ) + installer.install(source, destination, {}) if __name__ == "__main__": # pragma: no cover diff --git a/tests/conftest.py b/tests/conftest.py index 3a258e04..9703d423 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,42 +6,51 @@ @pytest.fixture def fancy_wheel(tmp_path): - path = tmp_path / "fancy-1.0.0-py2.py3-none-any.whl" + return mock_wheel(tmp_path, "fancy") + + +@pytest.fixture +def another_fancy_wheel(tmp_path): + return mock_wheel(tmp_path, "another_fancy") + + +def mock_wheel(tmp_path, name): + path = tmp_path / f"{name}-1.0.0-py2.py3-none-any.whl" files = { - "fancy/": b"""""", - "fancy/__init__.py": b"""\ + f"{name}/": b"""""", + f"{name}/__init__.py": b"""\ def main(): print("I'm fancy.") """, - "fancy/__main__.py": b"""\ + f"{name}/__main__.py": b"""\ if __name__ == "__main__": from . import main main() """, - "fancy-1.0.0.data/data/fancy/": b"""""", - "fancy-1.0.0.data/data/fancy/data.py": b"""\ + f"{name}-1.0.0.data/data/{name}/": b"""""", + f"{name}-1.0.0.data/data/{name}/data.py": b"""\ # put me in data """, - "fancy-1.0.0.dist-info/": b"""""", - "fancy-1.0.0.dist-info/top_level.txt": b"""\ - fancy - """, - "fancy-1.0.0.dist-info/entry_points.txt": b"""\ + f"{name}-1.0.0.dist-info/": b"""""", + f"{name}-1.0.0.dist-info/top_level.txt": f"""\ + {name} + """.encode(), + f"{name}-1.0.0.dist-info/entry_points.txt": f"""\ [console_scripts] - fancy = fancy:main + {name} = {name}:main [gui_scripts] - fancy-gui = fancy:main - """, - "fancy-1.0.0.dist-info/WHEEL": b"""\ + {name}-gui = {name}:main + """.encode(), + f"{name}-1.0.0.dist-info/WHEEL": b"""\ Wheel-Version: 1.0 Generator: magic (1.0.0) Root-Is-Purelib: true Tag: py3-none-any """, - "fancy-1.0.0.dist-info/METADATA": b"""\ + f"{name}-1.0.0.dist-info/METADATA": f"""\ Metadata-Version: 2.1 - Name: fancy + Name: {name} Version: 1.0.0 Summary: A fancy package Author: Agendaless Consulting @@ -50,17 +59,17 @@ def main(): Keywords: fancy amazing Platform: UNKNOWN Classifier: Intended Audience :: Developers - """, - "fancy-1.0.0.dist-info/RECORD": b"""\ - fancy/__init__.py,sha256=qZ2qq7xVBAiUFQVv-QBHhdtCUF5p1NsWwSOiD7qdHN0,36 - fancy/__main__.py,sha256=Wd4SyWJOIMsHf_5-0oN6aNFwen8ehJnRo-erk2_K-eY,61 - fancy-1.0.0.data/data/fancy/data.py,sha256=nuFRUNQF5vP7FWE-v5ysyrrfpIaAvfzSiGOgfPpLOeI,17 - fancy-1.0.0.dist-info/top_level.txt,sha256=SW-yrrF_c8KlserorMw54inhLjZ3_YIuLz7fYT4f8ao,6 - fancy-1.0.0.dist-info/entry_points.txt,sha256=AxJl21_zgoNWjCfvSkC9u_rWSzGyCtCzhl84n979jCc,75 - fancy-1.0.0.dist-info/WHEEL,sha256=1DrXMF1THfnBjsdS5sZn-e7BKcmUn7jnMbShGeZomgc,84 - fancy-1.0.0.dist-info/METADATA,sha256=hRhZavK_Y6WqKurFFAABDnoVMjZFBH0NJRjwLOutnJI,236 - fancy-1.0.0.dist-info/RECORD,, - """, + """.encode(), + f"{name}-1.0.0.dist-info/RECORD": f"""\ + {name}/__init__.py,sha256=qZ2qq7xVBAiUFQVv-QBHhdtCUF5p1NsWwSOiD7qdHN0,36 + {name}/__main__.py,sha256=Wd4SyWJOIMsHf_5-0oN6aNFwen8ehJnRo-erk2_K-eY,61 + {name}-1.0.0.data/data/{name}/data.py,sha256=nuFRUNQF5vP7FWE-v5ysyrrfpIaAvfzSiGOgfPpLOeI,17 + {name}-1.0.0.dist-info/top_level.txt,sha256=SW-yrrF_c8KlserorMw54inhLjZ3_YIuLz7fYT4f8ao,6 + {name}-1.0.0.dist-info/entry_points.txt,sha256=AxJl21_zgoNWjCfvSkC9u_rWSzGyCtCzhl84n979jCc,75 + {name}-1.0.0.dist-info/WHEEL,sha256=1DrXMF1THfnBjsdS5sZn-e7BKcmUn7jnMbShGeZomgc,84 + {name}-1.0.0.dist-info/METADATA,sha256=hRhZavK_Y6WqKurFFAABDnoVMjZFBH0NJRjwLOutnJI,236 + {name}-1.0.0.dist-info/RECORD,, + """.encode(), } with zipfile.ZipFile(path, "w") as archive: diff --git a/tests/test_main.py b/tests/test_main.py index d1e4eeec..96b37e01 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -35,6 +35,25 @@ def test_main(fancy_wheel, tmp_path): } +def test_main_multiple_wheels(fancy_wheel, another_fancy_wheel, tmp_path): + destdir = tmp_path / "dest" + + main( + [str(fancy_wheel), str(another_fancy_wheel), "-d", str(destdir)], + "python -m installer", + ) + + for wheel_name in ("fancy", "another_fancy"): + installed_py_files = destdir.rglob(f"**/{wheel_name}/**/*.py") + assert {f.stem for f in installed_py_files} == {"__init__", "__main__", "data"} + + installed_pyc_files = destdir.rglob(f"**/{wheel_name}/**/*.pyc") + assert {f.name.split(".")[0] for f in installed_pyc_files} == { + "__init__", + "__main__", + } + + def test_main_prefix(fancy_wheel, tmp_path): destdir = tmp_path / "dest"