Esempio n. 1
0
def ensure_path(location: Path, *, force: bool) -> Tuple[bool, bool]:
    """Ensure location is in user's PATH or add it to PATH.
    Returns True if location was added to PATH
    """
    location_str = str(location)
    path_added = False
    need_shell_restart = userpath.need_shell_restart(location_str)
    in_current_path = userpath.in_current_path(location_str)

    if force or (not in_current_path and not need_shell_restart):
        userpath.append(location_str, "pipx")
        print(
            pipx_wrap(
                f"Success! Added {location_str} to the PATH environment variable.",
                subsequent_indent=" " * 4,
            ))
        path_added = True
        need_shell_restart = userpath.need_shell_restart(location_str)
    elif not in_current_path and need_shell_restart:
        print(
            pipx_wrap(
                f"""
                {location_str} has been been added to PATH, but you need to
                open a new terminal or re-login for this PATH change to take
                effect.
                """,
                subsequent_indent=" " * 4,
            ))
    else:
        print(
            pipx_wrap(f"{location_str} is already in PATH.",
                      subsequent_indent=" " * 4))

    return (path_added, need_shell_restart)
Esempio n. 2
0
def _symlink_package_apps(
    local_bin_dir: Path, app_paths: List[Path], *, force: bool, suffix: str = ""
) -> None:
    for app_path in app_paths:
        app_name = app_path.name
        app_name_suffixed = add_suffix(app_name, suffix)
        symlink_path = Path(local_bin_dir / app_name_suffixed)
        if not symlink_path.parent.is_dir():
            mkdir(symlink_path.parent)

        if force:
            logger.info(f"Force is true. Removing {str(symlink_path)}.")
            try:
                symlink_path.unlink()
            except FileNotFoundError:
                pass
            except IsADirectoryError:
                rmdir(symlink_path)

        exists = symlink_path.exists()
        is_symlink = symlink_path.is_symlink()
        if exists:
            if symlink_path.samefile(app_path):
                logger.info(f"Same path {str(symlink_path)} and {str(app_path)}")
            else:
                logger.warning(
                    pipx_wrap(
                        f"""
                        {hazard}  File exists at {str(symlink_path)} and points
                        to {symlink_path.resolve()}, not {str(app_path)}. Not
                        modifying.
                        """,
                        subsequent_indent=" " * 4,
                    )
                )
            continue
        if is_symlink and not exists:
            logger.info(
                f"Removing existing symlink {str(symlink_path)} since it "
                "pointed non-existent location"
            )
            symlink_path.unlink()

        existing_executable_on_path = which(app_name_suffixed)
        symlink_path.symlink_to(app_path)

        if existing_executable_on_path:
            logger.warning(
                pipx_wrap(
                    f"""
                    {hazard}  Note: {app_name_suffixed} was already on your
                    PATH at {existing_executable_on_path}
                    """,
                    subsequent_indent=" " * 4,
                )
            )
Esempio n. 3
0
def ensure_pipx_paths(force: bool) -> ExitCode:
    """Returns pipx exit code."""
    bin_paths = [constants.LOCAL_BIN_DIR]

    pipx_user_bin_path = get_pipx_user_bin_path()
    if pipx_user_bin_path is not None:
        bin_paths.append(pipx_user_bin_path)

    path_added = False
    need_shell_restart = False
    for bin_path in bin_paths:
        (path_added_current, need_shell_restart_current) = ensure_path(
            bin_path, force=force
        )
        path_added |= path_added_current
        need_shell_restart |= need_shell_restart_current

    print()

    if path_added:
        print(
            pipx_wrap(
                """
                Consider adding shell completions for pipx. Run 'pipx
                completions' for instructions.
                """
            )
            + "\n"
        )
    elif not need_shell_restart:
        sys.stdout.flush()
        logger.warning(
            pipx_wrap(
                f"""
                {hazard}  All pipx binary directories have been added to PATH. If you
                are sure you want to proceed, try again with the '--force'
                flag.
                """
            )
            + "\n"
        )

    if need_shell_restart:
        print(
            pipx_wrap(
                """
                You will need to open a new terminal or re-login for the PATH
                changes to take effect.
                """
            )
            + "\n"
        )

    print(f"Otherwise pipx is ready to go! {stars}")

    return EXIT_CODE_OK
Esempio n. 4
0
def setup(args: argparse.Namespace) -> None:
    if "version" in args and args.version:
        print_version()
        sys.exit(0)

    setup_logging("verbose" in args and args.verbose)

    logger.debug(f"{time.strftime('%Y-%m-%d %H:%M:%S')}")
    logger.debug(f"{' '.join(sys.argv)}")
    logger.info(f"pipx version is {__version__}")
    logger.info(f"Default python interpreter is {repr(DEFAULT_PYTHON)}")

    mkdir(constants.PIPX_LOCAL_VENVS)
    mkdir(constants.LOCAL_BIN_DIR)
    mkdir(constants.PIPX_VENV_CACHEDIR)

    old_pipx_venv_location = constants.PIPX_LOCAL_VENVS / "pipx-app"
    if old_pipx_venv_location.exists():
        logger.warning(
            pipx_wrap(
                f"""
                {hazard}  A virtual environment for pipx was detected at
                {str(old_pipx_venv_location)}. The 'pipx-app' package has been
                renamed back to 'pipx'
                (https://github.com/pipxproject/pipx/issues/82).
                """,
                subsequent_indent=" " * 4,
            ))
Esempio n. 5
0
File: venv.py Progetto: ionelmc/pipx
    def __init__(self,
                 path: Path,
                 *,
                 verbose: bool = False,
                 python: str = DEFAULT_PYTHON) -> None:
        self.root = path
        self.python = python
        self.bin_path, self.python_path = get_venv_paths(self.root)
        self.pipx_metadata = PipxMetadata(venv_dir=path)
        self.verbose = verbose
        self.do_animation = not verbose
        try:
            self._existing = self.root.exists() and next(self.root.iterdir())
        except StopIteration:
            self._existing = False

        if self._existing and self.uses_shared_libs:
            if shared_libs.is_valid:
                if shared_libs.needs_upgrade:
                    shared_libs.upgrade(verbose=verbose)
            else:
                shared_libs.create(verbose)

            if not shared_libs.is_valid:
                raise PipxError(
                    pipx_wrap(f"""
                        Error: pipx's shared venv {shared_libs.root} is invalid
                        and needs re-installation. To fix this, install or
                        reinstall a package. For example:
                        """) + f"\n  pipx install {self.root.name} --force",
                    wrap_message=False,
                )
Esempio n. 6
0
def parse_specifier_for_install(
    package_spec: str, pip_args: List[str]
) -> Tuple[str, List[str]]:
    """Return package_or_url and pip_args suitable for pip install

    Specifically:
    * Strip any markers (e.g. python_version > "3.4")
    * Ensure --editable is removed for any package_spec not a local path
    * Convert local paths to absolute paths
    """
    parsed_package = _parse_specifier(package_spec)
    package_or_url = _parsed_package_to_package_or_url(
        parsed_package, remove_version_specifiers=False
    )
    if "--editable" in pip_args and not parsed_package.valid_local_path:
        logger.warning(
            pipx_wrap(
                f"""
                {hazard}  Ignoring --editable install option. pipx disallows it
                for anything but a local path, to avoid having to create a new
                src/ directory.
                """,
                subsequent_indent=" " * 4,
            )
        )
        pip_args.remove("--editable")

    return (package_or_url, pip_args)
Esempio n. 7
0
def _parsed_package_to_package_or_url(
    parsed_package: ParsedPackage, remove_version_specifiers: bool
) -> str:
    if parsed_package.valid_pep508 is not None:
        if parsed_package.valid_pep508.marker is not None:
            logger.warning(
                pipx_wrap(
                    f"""
                    {hazard}  Ignoring environment markers
                    ({parsed_package.valid_pep508.marker}) in package
                    specification. Use pipx options to specify this type of
                    information.
                    """,
                    subsequent_indent=" " * 4,
                )
            )
        package_or_url = package_or_url_from_pep508(
            parsed_package.valid_pep508,
            remove_version_specifiers=remove_version_specifiers,
        )
    elif parsed_package.valid_url is not None:
        package_or_url = parsed_package.valid_url
    elif parsed_package.valid_local_path is not None:
        package_or_url = parsed_package.valid_local_path

    logger.info(f"cleaned package spec: {package_or_url}")
    return package_or_url
Esempio n. 8
0
File: venv.py Progetto: ionelmc/pipx
 def remove_venv(self) -> None:
     if self.safe_to_remove():
         rmdir(self.root)
     else:
         logger.warning(
             pipx_wrap(
                 f"""
                 {hazard}  Not removing existing venv {self.root} because it
                 was not created in this session
                 """,
                 subsequent_indent=" " * 4,
             ))
Esempio n. 9
0
def warn_if_not_on_path(local_bin_dir: Path) -> None:
    if not userpath.in_current_path(str(local_bin_dir)):
        logger.warning(
            pipx_wrap(
                f"""
                {hazard}  Note: {str(local_bin_dir)!r} is not on your PATH
                environment variable. These apps will not be globally
                accessible until your PATH is updated. Run `pipx ensurepath` to
                automatically add it, or manually modify your PATH in your
                shell's config file (i.e. ~/.bashrc).
                """,
                subsequent_indent=" " * 4,
            ))
Esempio n. 10
0
 def read(self, verbose: bool = False) -> None:
     try:
         with open(self.venv_dir / PIPX_INFO_FILENAME, "r") as pipx_metadata_fh:
             self.from_dict(
                 json.load(pipx_metadata_fh, object_hook=_json_decoder_object_hook)
             )
     except IOError:  # Reset self if problem reading
         if verbose:
             logger.warning(
                 pipx_wrap(
                     f"""
                     {hazard}  Unable to read {PIPX_INFO_FILENAME} in
                     {self.venv_dir}.  This may cause this or future pipx
                     operations involving {self.venv_dir.name} to fail or
                     behave incorrectly.
                     """,
                     subsequent_indent=" " * 4,
                 )
             )
         return
Esempio n. 11
0
def fix_package_name(package_or_url: str, package_name: str) -> str:
    try:
        package_req = Requirement(package_or_url)
    except InvalidRequirement:
        # not a valid PEP508 package specification
        return package_or_url

    if canonicalize_name(package_req.name) != canonicalize_name(package_name):
        logger.warning(
            pipx_wrap(
                f"""
                {hazard}  Name supplied in package specifier was
                {package_req.name!r} but package found has name {package_name!r}.
                Using {package_name!r}.
                """,
                subsequent_indent=" " * 4,
            ))
    package_req.name = package_name

    return str(package_req)
Esempio n. 12
0
 def write(self) -> None:
     self._validate_before_write()
     try:
         with open(self.venv_dir / PIPX_INFO_FILENAME, "w") as pipx_metadata_fh:
             json.dump(
                 self.to_dict(),
                 pipx_metadata_fh,
                 indent=4,
                 sort_keys=True,
                 cls=JsonEncoderHandlesPath,
             )
     except IOError:
         logger.warning(
             pipx_wrap(
                 f"""
                 {hazard}  Unable to write {PIPX_INFO_FILENAME} to
                 {self.venv_dir}.  This may cause future pipx operations
                 involving {self.venv_dir.name} to fail or behave
                 incorrectly.
                 """,
                 subsequent_indent=" " * 4,
             )
         )
Esempio n. 13
0
PIPX_DESCRIPTION = textwrap.dedent(f"""
    Install and execute apps from Python packages.

    Binaries can either be installed globally into isolated Virtual Environments
    or run directly in a temporary Virtual Environment.

    Virtual Environment location is {str(constants.PIPX_LOCAL_VENVS)}.
    Symlinks to apps are placed in {str(constants.LOCAL_BIN_DIR)}.

    """)
PIPX_DESCRIPTION += pipx_wrap(
    """
    optional environment variables:
      PIPX_HOME             Overrides default pipx location. Virtual Environments will be installed to $PIPX_HOME/venvs.
      PIPX_BIN_DIR          Overrides location of app installations. Apps are symlinked or copied here.
      USE_EMOJI             Overrides emoji behavior. Default value varies based on platform.
      PIPX_DEFAULT_PYTHON   Overrides default python used for commands.
    """,
    subsequent_indent=" " * 24,  # match the indent of argparse options
    keep_newlines=True,
)

DOC_DEFAULT_PYTHON = os.getenv("PIPX__DOC_DEFAULT_PYTHON", DEFAULT_PYTHON)

INSTALL_DESCRIPTION = textwrap.dedent(f"""
    The install command is the preferred way to globally install apps
    from python packages on your system. It creates an isolated virtual
    environment for the package, then ensures the package's apps are
    accessible on your $PATH.

    The result: apps you can run from anywhere, located in packages
Esempio n. 14
0
def _upgrade_package(
    venv: Venv,
    package_name: str,
    pip_args: List[str],
    is_main_package: bool,
    force: bool,
    upgrading_all: bool,
) -> int:
    """Returns 1 if package version changed, 0 if same version"""
    package_metadata = venv.package_metadata[package_name]

    if package_metadata.package_or_url is None:
        raise PipxError(
            f"Internal Error: package {package_name} has corrupt pipx metadata."
        )

    package_or_url = parse_specifier_for_upgrade(
        package_metadata.package_or_url)
    old_version = package_metadata.package_version

    venv.upgrade_package(
        package_name,
        package_or_url,
        pip_args,
        include_dependencies=package_metadata.include_dependencies,
        include_apps=package_metadata.include_apps,
        is_main_package=is_main_package,
        suffix=package_metadata.suffix,
    )

    package_metadata = venv.package_metadata[package_name]

    display_name = f"{package_metadata.package}{package_metadata.suffix}"
    new_version = package_metadata.package_version

    if package_metadata.include_apps:
        expose_apps_globally(
            constants.LOCAL_BIN_DIR,
            package_metadata.app_paths,
            force=force,
            suffix=package_metadata.suffix,
        )

    if package_metadata.include_dependencies:
        for _, app_paths in package_metadata.app_paths_of_dependencies.items():
            expose_apps_globally(
                constants.LOCAL_BIN_DIR,
                app_paths,
                force=force,
                suffix=package_metadata.suffix,
            )

    if old_version == new_version:
        if upgrading_all:
            pass
        else:
            print(
                pipx_wrap(f"""
                    {display_name} is already at latest version {old_version}
                    (location: {str(venv.root)})
                    """))
        return 0
    else:
        print(
            pipx_wrap(f"""
                upgraded package {display_name} from {old_version} to
                {new_version} (location: {str(venv.root)})
                """))
        return 1
Esempio n. 15
0
def install(
    venv_dir: Optional[Path],
    package_name: Optional[str],
    package_spec: str,
    local_bin_dir: Path,
    python: str,
    pip_args: List[str],
    venv_args: List[str],
    verbose: bool,
    *,
    force: bool,
    include_dependencies: bool,
    suffix: str = "",
) -> ExitCode:
    """Returns pipx exit code."""
    # package_spec is anything pip-installable, including package_name, vcs spec,
    #   zip file, or tar.gz file.

    if package_name is None:
        package_name = package_name_from_spec(package_spec,
                                              python,
                                              pip_args=pip_args,
                                              verbose=verbose)
    if venv_dir is None:
        venv_container = VenvContainer(constants.PIPX_LOCAL_VENVS)
        venv_dir = venv_container.get_venv_dir(f"{package_name}{suffix}")

    try:
        exists = venv_dir.exists() and bool(next(venv_dir.iterdir()))
    except StopIteration:
        exists = False

    venv = Venv(venv_dir, python=python, verbose=verbose)
    if exists:
        if force:
            print(f"Installing to existing venv {venv.name!r}")
        else:
            print(
                pipx_wrap(f"""
                    {venv.name!r} already seems to be installed. Not modifying
                    existing installation in {str(venv_dir)!r}. Pass '--force'
                    to force installation.
                    """))
            return EXIT_CODE_INSTALL_VENV_EXISTS

    try:
        venv.create_venv(venv_args, pip_args)
        venv.install_package(
            package_name=package_name,
            package_or_url=package_spec,
            pip_args=pip_args,
            include_dependencies=include_dependencies,
            include_apps=True,
            is_main_package=True,
            suffix=suffix,
        )
        run_post_install_actions(
            venv,
            package_name,
            local_bin_dir,
            venv_dir,
            include_dependencies,
            force=force,
        )
    except (Exception, KeyboardInterrupt):
        print()
        venv.remove_venv()
        raise

    # Any failure to install will raise PipxError, otherwise success
    return EXIT_CODE_OK
Esempio n. 16
0
def run(
    app: str,
    package_or_url: str,
    app_args: List[str],
    python: str,
    pip_args: List[str],
    venv_args: List[str],
    pypackages: bool,
    verbose: bool,
    use_cache: bool,
) -> NoReturn:
    """Installs venv to temporary dir (or reuses cache), then runs app from
    package
    """

    if urllib.parse.urlparse(app).scheme:
        if not app.endswith(".py"):
            raise PipxError("""
                pipx will only execute apps from the internet directly if they
                end with '.py'. To run from an SVN, try pipx --spec URL BINARY
                """)
        logger.info(
            "Detected url. Downloading and executing as a Python file.")

        content = _http_get_request(app)
        exec_app([str(python), "-c", content])

    elif which(app):
        logger.warning(
            pipx_wrap(
                f"""
                {hazard}  {app} is already on your PATH and installed at
                {which(app)}. Downloading and running anyway.
                """,
                subsequent_indent=" " * 4,
            ))

    if WINDOWS:
        app_filename = f"{app}.exe"
        logger.info(f"Assuming app is {app_filename!r} (Windows only)")
    else:
        app_filename = app

    pypackage_bin_path = get_pypackage_bin_path(app)
    if pypackage_bin_path.exists():
        logger.info(
            f"Using app in local __pypackages__ directory at {str(pypackage_bin_path)}"
        )
        run_pypackage_bin(pypackage_bin_path, app_args)
    if pypackages:
        raise PipxError(f"""
            '--pypackages' flag was passed, but {str(pypackage_bin_path)!r} was
            not found. See https://github.com/cs01/pythonloc to learn how to
            install here, or omit the flag.
            """)

    venv_dir = _get_temporary_venv_path(package_or_url, python, pip_args,
                                        venv_args)

    venv = Venv(venv_dir)
    bin_path = venv.bin_path / app_filename
    _prepare_venv_cache(venv, bin_path, use_cache)

    if venv.has_app(app, app_filename):
        logger.info(f"Reusing cached venv {venv_dir}")
        venv.run_app(app, app_filename, app_args)
    else:
        logger.info(f"venv location is {venv_dir}")
        _download_and_run(
            Path(venv_dir),
            package_or_url,
            app,
            app_filename,
            app_args,
            python,
            pip_args,
            venv_args,
            use_cache,
            verbose,
        )