From 9267c96139c4088efec56cf2c95932145a0c99e4 Mon Sep 17 00:00:00 2001 From: Amund Hov Date: Fri, 20 Oct 2023 13:29:11 +0200 Subject: [PATCH] Factor out business logic of `shiv`. Collects CLI scripts into commands.py for click setup. I've added a motivating example to the docs for using shiv main() routine direcly from build scripts written in python. --- docs/cli-reference.rst | 4 +- setup.cfg | 4 +- src/shiv/__main__.py | 4 +- src/shiv/cli.py | 115 +++++++++++------------------------------ src/shiv/commands.py | 114 ++++++++++++++++++++++++++++++++++++++++ src/shiv/info.py | 33 ------------ test/test_cli.py | 8 +-- 7 files changed, 153 insertions(+), 129 deletions(-) create mode 100644 src/shiv/commands.py delete mode 100644 src/shiv/info.py diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index c80236a..30eb953 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -13,11 +13,11 @@ Available Commands .. contents:: :local: -.. click:: shiv.cli:main +.. click:: shiv.commands:shiv :prog: shiv :show-nested: -.. click:: shiv.info:main +.. click:: shiv.commands:shiv_info :prog: shiv-info :show-nested: diff --git a/setup.cfg b/setup.cfg index c8a4397..b55daa0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,8 +59,8 @@ where=src [options.entry_points] console_scripts = - shiv = shiv.cli:main - shiv-info = shiv.info:main + shiv = shiv.commands:shiv + shiv-info = shiv.commands:shiv_info [bdist_wheel] universal = True diff --git a/src/shiv/__main__.py b/src/shiv/__main__.py index 3ae58da..c6ca40b 100644 --- a/src/shiv/__main__.py +++ b/src/shiv/__main__.py @@ -1,7 +1,7 @@ """ Shim for package execution (python3 -m shiv ...). """ -from .cli import main +from .commands import shiv if __name__ == "__main__": # pragma: no cover - main() + shiv() diff --git a/src/shiv/cli.py b/src/shiv/cli.py index f361091..22f6b3e 100644 --- a/src/shiv/cli.py +++ b/src/shiv/cli.py @@ -88,95 +88,37 @@ def copytree(src: Path, dst: Path) -> None: shutil.copy2(str(path), str(dst / path.relative_to(src))) -@click.command(context_settings=dict(help_option_names=["-h", "--help", "--halp"], ignore_unknown_options=True)) -@click.version_option(version=__version__, prog_name="shiv") -@click.option( - "--entry-point", "-e", default=None, help="The entry point to invoke (takes precedence over --console-script)." -) -@click.option("--console-script", "-c", default=None, help="The console_script to invoke.") -@click.option("--output-file", "-o", help="The path to the output file for shiv to create.") -@click.option( - "--python", - "-p", - help=( - "The python interpreter to set as the shebang, a.k.a. whatever you want after '#!' " - "(default is '/usr/bin/env python3')" - ), -) -@click.option( - "--site-packages", - help="The path to an existing site-packages directory to copy into the zipapp.", - type=click.Path(exists=True), - multiple=True, -) -@click.option( - "--build-id", - default=None, - help=( - "Use a custom build id instead of the default (a SHA256 hash of the contents of the build). " - "Warning: must be unique per build!" - ), -) -@click.option("--compressed/--uncompressed", default=True, help="Whether or not to compress your zip.") -@click.option( - "--compile-pyc", - is_flag=True, - help="Whether or not to compile pyc files during initial bootstrap.", -) -@click.option( - "--extend-pythonpath", - "-E", - is_flag=True, - help="Add the contents of the zipapp to PYTHONPATH (for subprocesses).", -) -@click.option( - "--reproducible", - is_flag=True, - help=( - "Generate a reproducible zipapp by overwriting all files timestamps to a default value. " - "Timestamp can be overwritten by SOURCE_DATE_EPOCH env variable. " - "Note: If SOURCE_DATE_EPOCH is set, this option will be implicitly set to true." - ), -) -@click.option( - "--no-modify", - is_flag=True, - help=( - "If specified, this modifies the runtime of the zipapp to raise " - "a RuntimeException if the source files (in ~/.shiv or SHIV_ROOT) have been modified. " - """It's recommended to use Python's "--check-hash-based-pycs always" option with this feature.""" - ), -) -@click.option( - "--preamble", - type=click.Path(exists=True), - help=( - "Provide a path to a preamble script that is invoked by shiv's runtime after bootstrapping the environment, " - "but before invoking your entry point." - ), -) -@click.option("--root", type=click.Path(), help="Override the 'root' path (default is ~/.shiv).") -@click.argument("pip_args", nargs=-1, type=click.UNPROCESSED) def main( + *, output_file: str, - entry_point: Optional[str], - console_script: Optional[str], - python: Optional[str], - site_packages: Optional[str], - build_id: Optional[str], - compressed: bool, - compile_pyc: bool, - extend_pythonpath: bool, - reproducible: bool, - no_modify: bool, - preamble: Optional[str], - root: Optional[str], - pip_args: List[str], + entry_point: Optional[str] = None, + console_script: Optional[str] = None, + python: Optional[str] = None, + site_packages: Optional[str] = None, + build_id: Optional[str] = None, + # Optional switches and flags + compressed: bool = True, + # Default inactive flags + compile_pyc: bool = False, + extend_pythonpath: bool = False, + reproducible: bool = False, + no_modify: bool = False, + preamble: Optional[str] = None, + root: Optional[str] = None, + # Unprocessed args passed to pip + pip_args: Optional[List[str]] = None ) -> None: + """ Main routine wrapped by `shiv` command. + + Enables direct use from python scripts: + + .. code-block:: py + + >>> main(output_file='numpy.pyz', compile_pyc=True, pip_args=['numpy']) """ - Shiv is a command line utility for building fully self-contained Python zipapps - as outlined in PEP 441, but with all their dependencies included! - """ + + if pip_args is None: + pip_args = [] if not pip_args and not site_packages: sys.exit(NO_PIP_ARGS_OR_SITE_PACKAGES) @@ -277,4 +219,5 @@ def main( if __name__ == "__main__": # pragma: no cover - main() + from shiv.commands import shiv + shiv() diff --git a/src/shiv/commands.py b/src/shiv/commands.py new file mode 100644 index 0000000..a1ba7e8 --- /dev/null +++ b/src/shiv/commands.py @@ -0,0 +1,114 @@ +import json +import click +import zipfile + +from shiv.cli import __version__ +from shiv.cli import main as shiv_main + + +# FIXME these options' required and default values need to be kept in sync with +# shiv.cli.main, but could be inferred from its kwarg annotations on Python >3.10 +@click.command(context_settings=dict(help_option_names=["-h", "--help", "--halp"], ignore_unknown_options=True)) +@click.version_option(version=__version__, prog_name="shiv") +@click.option( + "--entry-point", "-e", default=None, help="The entry point to invoke (takes precedence over --console-script)." +) +@click.option("--console-script", "-c", default=None, help="The console_script to invoke.") +@click.option("--output-file", "-o", help="The path to the output file for shiv to create.") +@click.option( + "--python", + "-p", + help=( + "The python interpreter to set as the shebang, a.k.a. whatever you want after '#!' " + "(default is '/usr/bin/env python3')" + ), +) +@click.option( + "--site-packages", + help="The path to an existing site-packages directory to copy into the zipapp.", + type=click.Path(exists=True), + multiple=True, +) +@click.option( + "--build-id", + default=None, + help=( + "Use a custom build id instead of the default (a SHA256 hash of the contents of the build). " + "Warning: must be unique per build!" + ), +) +@click.option("--compressed/--uncompressed", default=True, help="Whether or not to compress your zip.") +@click.option( + "--compile-pyc", + is_flag=True, + help="Whether or not to compile pyc files during initial bootstrap.", +) +@click.option( + "--extend-pythonpath", + "-E", + is_flag=True, + help="Add the contents of the zipapp to PYTHONPATH (for subprocesses).", +) +@click.option( + "--reproducible", + is_flag=True, + help=( + "Generate a reproducible zipapp by overwriting all files timestamps to a default value. " + "Timestamp can be overwritten by SOURCE_DATE_EPOCH env variable. " + "Note: If SOURCE_DATE_EPOCH is set, this option will be implicitly set to true." + ), +) +@click.option( + "--no-modify", + is_flag=True, + help=( + "If specified, this modifies the runtime of the zipapp to raise " + "a RuntimeException if the source files (in ~/.shiv or SHIV_ROOT) have been modified. " + """It's recommended to use Python's "--check-hash-based-pycs always" option with this feature.""" + ), +) +@click.option( + "--preamble", + type=click.Path(exists=True), + help=( + "Provide a path to a preamble script that is invoked by shiv's runtime after bootstrapping the environment, " + "but before invoking your entry point." + ), +) +@click.option("--root", type=click.Path(), help="Override the 'root' path (default is ~/.shiv).") +@click.argument("pip_args", nargs=-1, type=click.UNPROCESSED) +def shiv(**kwargs): + """ + Shiv is a command line utility for building fully self-contained Python zipapps + as outlined in PEP 441, but with all their dependencies included! + """ + shiv_main(**kwargs) + + +@click.command(context_settings=dict(help_option_names=["-h", "--help", "--halp"])) +@click.option("--json", "-j", "print_as_json", is_flag=True, help="output as plain json") +@click.argument("pyz") +def shiv_info(print_as_json, pyz): + """A simple utility to print debugging information about PYZ files created with ``shiv``""" + + zip_file = zipfile.ZipFile(pyz) + data = json.loads(zip_file.read("environment.json")) + + if print_as_json: + click.echo(json.dumps(data, indent=4, sort_keys=True)) + + else: + click.echo() + click.secho("pyz file: ", fg="green", bold=True, nl=False) + click.secho(pyz, fg="white") + click.echo() + + for key, value in data.items(): + click.secho(f"{key}: ", fg="blue", bold=True, nl=False) + + if key == "hashes": + click.secho(json.dumps(value, sort_keys=True, indent=2)) + else: + click.secho(f"{value}", fg="white") + + click.echo() diff --git a/src/shiv/info.py b/src/shiv/info.py deleted file mode 100644 index 5b5eb5c..0000000 --- a/src/shiv/info.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import zipfile - -import click - - -@click.command(context_settings=dict(help_option_names=["-h", "--help", "--halp"])) -@click.option("--json", "-j", "print_as_json", is_flag=True, help="output as plain json") -@click.argument("pyz") -def main(print_as_json, pyz): - """A simple utility to print debugging information about PYZ files created with ``shiv``""" - - zip_file = zipfile.ZipFile(pyz) - data = json.loads(zip_file.read("environment.json")) - - if print_as_json: - click.echo(json.dumps(data, indent=4, sort_keys=True)) - - else: - click.echo() - click.secho("pyz file: ", fg="green", bold=True, nl=False) - click.secho(pyz, fg="white") - click.echo() - - for key, value in data.items(): - click.secho(f"{key}: ", fg="blue", bold=True, nl=False) - - if key == "hashes": - click.secho(json.dumps(value, sort_keys=True, indent=2)) - else: - click.secho(f"{value}", fg="white") - - click.echo() diff --git a/test/test_cli.py b/test/test_cli.py index bd1af23..e6bfb9f 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -11,9 +11,9 @@ import pytest from click.testing import CliRunner -from shiv.cli import console_script_exists, find_entry_point, main +from shiv import commands +from shiv.cli import console_script_exists, find_entry_point from shiv.constants import DISALLOWED_ARGS, DISALLOWED_PIP_ARGS, NO_OUTFILE, NO_PIP_ARGS_OR_SITE_PACKAGES -from shiv.info import main as info_main from shiv.pip import install UGOX = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH @@ -41,7 +41,7 @@ def runner(self): def invoke(args, env=None): args.extend(["-p", "/usr/bin/env python3"]) - return CliRunner().invoke(main, args, env=env) + return CliRunner().invoke(commands.shiv, args, env=env) return invoke @@ -49,7 +49,7 @@ def invoke(args, env=None): def info_runner(self): """Returns a click test runner (for shiv-info).""" - return lambda args: CliRunner().invoke(info_main, args) + return lambda args: CliRunner().invoke(commands.shiv_info, args) def test_find_entry_point(self, tmpdir, package_location): """Test that we can find console_script metadata."""