Source code for shiv.cli

import importlib_resources  # type: ignore
import shutil
import sys
import uuid

from configparser import ConfigParser
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Optional, List

import click

from . import pip
from . import builder
from . import bootstrap
from .bootstrap.environment import Environment
from .constants import (
    BLACKLISTED_ARGS,
    DISALLOWED_PIP_ARGS,
    NO_PIP_ARGS,
    NO_OUTFILE,
    NO_ENTRY_POINT,
    INVALID_PYTHON,
)

# This is the 'knife' emoji
SHIV = u"\U0001F52A"


[docs]def find_entry_point(site_packages: Path, console_script: str) -> str: """Find a console_script in a site-packages directory. Console script metadata is stored in entry_points.txt per setuptools convention. This function searches all entry_points.txt files and returns the import string for a given console_script argument. :param site_packages: A path to a site-packages directory on disk. :param console_script: A console_script string. """ config_parser = ConfigParser() config_parser.read(site_packages.rglob("entry_points.txt")) return config_parser["console_scripts"][console_script]
[docs]def validate_interpreter(interpreter_path: Optional[str] = None) -> Path: """Ensure that the interpreter is a real path, not a symlink. If no interpreter is given, default to `sys.exectuable` :param interpreter_path: A path to a Python interpreter. """ real_path = Path(sys.executable) if interpreter_path is None else Path( interpreter_path ) if real_path.exists(): return real_path else: sys.exit(INVALID_PYTHON.format(path=real_path))
[docs]def copy_bootstrap(bootstrap_target: Path) -> None: """Copy bootstrap code from shiv into the pyz. :param bootstrap_target: The temporary directory where we are staging pyz contents. """ for bootstrap_file in importlib_resources.contents(bootstrap): if importlib_resources.is_resource(bootstrap, bootstrap_file): with importlib_resources.path(bootstrap, bootstrap_file) as f: shutil.copyfile(f.absolute(), bootstrap_target / f.name)
@click.command( context_settings=dict( help_option_names=["-h", "--help", "--halp"], ignore_unknown_options=True ) ) @click.option("--entry-point", "-e", default=None, help="The entry point to invoke.") @click.option( "--console-script", "-c", default=None, help="The console_script to invoke." ) @click.option("--output-file", "-o", help="The file for shiv to create.") @click.option("--python", "-p", help="The path to a python interpreter to use.") @click.option( "--compressed/--uncompressed", default=True, help="Whether or not to compress your zip.", ) @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], compressed: bool, pip_args: List[str], ) -> None: """ Shiv is a command line utility for building fully self-contained Python zipapps as outlined in PEP 441, but with all their dependencies included! """ quiet = "-q" in pip_args or '--quiet' in pip_args if not quiet: click.secho(" shiv! " + SHIV, bold=True) if not pip_args: sys.exit(NO_PIP_ARGS) if output_file is None: sys.exit(NO_OUTFILE) # check for disallowed pip arguments for blacklisted_arg in BLACKLISTED_ARGS: for supplied_arg in pip_args: if supplied_arg in blacklisted_arg: sys.exit( DISALLOWED_PIP_ARGS.format( arg=supplied_arg, reason=BLACKLISTED_ARGS[blacklisted_arg] ) ) # validate supplied python (if any) interpreter = validate_interpreter(python) with TemporaryDirectory() as working_path: site_packages = Path(working_path, "site-packages") site_packages.mkdir(parents=True, exist_ok=True) # install deps into staged site-packages pip.install( python or sys.executable, ["--target", site_packages.as_posix()] + list(pip_args), ) # if entry_point is a console script, get the callable if entry_point is None and console_script is not None: try: entry_point = find_entry_point(site_packages, console_script) except KeyError: sys.exit(NO_ENTRY_POINT.format(entry_point=console_script)) # create runtime environment metadata env = Environment( build_id=str(uuid.uuid4()), entry_point=entry_point, ) Path(working_path, "environment.json").write_text(env.to_json()) # create bootstrapping directory in working path bootstrap_target = Path(working_path, "_bootstrap") bootstrap_target.mkdir(parents=True, exist_ok=True) # copy bootstrap code copy_bootstrap(bootstrap_target) # create the zip builder.create_archive( Path(working_path), target=Path(output_file), interpreter=interpreter, main="_bootstrap:bootstrap", compressed=compressed, ) if not quiet: click.secho(" done ", bold=True)