def upgrade( venv_dir: Path, package: str, package_or_url: str, pip_args: List[str], verbose: bool, *, upgrading_all: bool, include_dependencies: bool, force: bool, ) -> int: """Returns nonzero if package was upgraded, 0 if version did not change""" if not venv_dir.is_dir(): raise PipxError( f"Package is not installed. Expected to find {str(venv_dir)}, " "but it does not exist.") venv = Venv(venv_dir, verbose=verbose) old_version = venv.get_venv_metadata_for_package(package).package_version # Upgrade shared libraries (pip, setuptools and wheel) venv.upgrade_packaging_libraries(pip_args) venv.upgrade_package(package_or_url, pip_args) new_version = venv.get_venv_metadata_for_package(package).package_version metadata = venv.get_venv_metadata_for_package(package) _expose_apps_globally(constants.LOCAL_BIN_DIR, metadata.app_paths, package, force=force) if include_dependencies: for _, app_paths in metadata.app_paths_of_dependencies.items(): _expose_apps_globally(constants.LOCAL_BIN_DIR, app_paths, package, force=force) if old_version == new_version: if upgrading_all: pass else: print( f"{package} is already at latest version {old_version} (location: {str(venv_dir)})" ) return 0 else: print( f"upgraded package {package} from {old_version} to {new_version} (location: {str(venv_dir)})" ) return 1
def _get_package_summary(path: Path, *, package: str = None, new_install: bool = False) -> str: venv = Venv(path) python_path = venv.python_path.resolve() if package is None: package = path.name metadata = venv.get_venv_metadata_for_package(package) if metadata.package_version is None: not_installed = red("is not installed") return f" package {bold(package)} {not_installed} in the venv {str(path)}" apps = metadata.apps + metadata.apps_of_dependencies exposed_app_paths = _get_exposed_app_paths_for_package( venv.bin_path, apps, constants.LOCAL_BIN_DIR) exposed_binary_names = sorted(p.name for p in exposed_app_paths) unavailable_binary_names = sorted( set(metadata.apps) - set(exposed_binary_names)) return _get_list_output( metadata.python_version, python_path, metadata.package_version, package, new_install, exposed_binary_names, unavailable_binary_names, )
def uninstall(venv_dir: Path, package: str, local_bin_dir: Path, verbose: bool): if not venv_dir.exists(): print(f"Nothing to uninstall for {package} 😴") app = which(package) if app: print( f"{hazard} Note: '{app}' still exists on your system and is on your PATH" ) return venv = Venv(venv_dir, verbose=verbose) metadata = venv.get_venv_metadata_for_package(package) app_paths = metadata.app_paths for dep_paths in metadata.app_paths_of_dependencies.values(): app_paths += dep_paths for file in local_bin_dir.iterdir(): if WINDOWS: for b in app_paths: if file.name == b.name: file.unlink() else: symlink = file for b in app_paths: if symlink.exists() and b.exists() and symlink.samefile(b): logging.info(f"removing symlink {str(symlink)}") symlink.unlink() rmdir(venv_dir) print(f"uninstalled {package}! {stars}")
def _get_venv_bin_dir_app_paths(venv: Venv, local_bin_dir: Path) -> Set[Path]: bin_dir_app_paths = set() if venv.pipx_metadata.main_package.package is not None: # Valid metadata for venv for package_info in venv.package_metadata.values(): bin_dir_app_paths |= _get_package_bin_dir_app_paths( venv, package_info, local_bin_dir) elif venv.python_path.is_file(): # No metadata from pipx_metadata.json, but valid python interpreter. # In pre-metadata-pipx venv.root.name is name of main package # In pre-metadata-pipx there is no suffix # We make the conservative assumptions: no injected packages, # not include_dependencies. Other PackageInfo fields are irrelevant # here. venv_metadata = venv.get_venv_metadata_for_package( venv.root.name, set()) main_package_info = _venv_metadata_to_package_info( venv_metadata, venv.root.name) bin_dir_app_paths = _get_package_bin_dir_app_paths( venv, main_package_info, local_bin_dir) else: # No metadata and no valid python interpreter. # We'll take our best guess on what to uninstall here based on symlink # location for symlink-capable systems. # The heuristic here is any symlink in ~/.local/bin pointing to # .local/pipx/venvs/VENV_NAME/{bin,Scripts} should be uninstalled. # For non-symlink systems we give up and return and empty set. if not can_symlink(local_bin_dir): return set() bin_dir_app_paths = get_exposed_app_paths_for_package( venv.bin_path, local_bin_dir) return bin_dir_app_paths
def uninstall(venv_dir: Path, package: str, local_bin_dir: Path, verbose: bool): """Uninstall entire venv_dir, including main package and all injected packages. """ if not venv_dir.exists(): print(f"Nothing to uninstall for {package} {sleep}") app = which(package) if app: print( f"{hazard} Note: '{app}' still exists on your system and is on your PATH" ) return venv = Venv(venv_dir, verbose=verbose) if venv.pipx_metadata.main_package is not None: app_paths: List[Path] = [] for viewed_package in venv.package_metadata.values(): app_paths += viewed_package.app_paths for dep_paths in viewed_package.app_paths_of_dependencies.values(): app_paths += dep_paths else: # fallback if not metadata from pipx_metadata.json if venv.python_path.is_file(): # has a valid python interpreter and can get metadata about the package metadata = venv.get_venv_metadata_for_package(package) app_paths = metadata.app_paths for dep_paths in metadata.app_paths_of_dependencies.values(): app_paths += dep_paths else: # Doesn't have a valid python interpreter. We'll take our best guess on what to uninstall # here based on symlink location. pipx doesn't use symlinks on windows, so this is for # non-windows only. # The heuristic here is any symlink in ~/.local/bin pointing to .local/pipx/venvs/PACKAGE/bin # should be uninstalled. if WINDOWS: app_paths = [] else: apps_linking_to_venv_bin_dir = [ f for f in constants.LOCAL_BIN_DIR.iterdir() if str(f.resolve()).startswith(str(venv.bin_path)) ] app_paths = apps_linking_to_venv_bin_dir for file in local_bin_dir.iterdir(): if WINDOWS: for b in app_paths: if file.name == b.name: file.unlink() else: symlink = file for b in app_paths: if symlink.exists() and b.exists() and symlink.samefile(b): logging.info(f"removing symlink {str(symlink)}") symlink.unlink() rmdir(venv_dir) print(f"uninstalled {package}! {stars}")
def install( venv_dir: Path, package: str, package_or_url: str, local_bin_dir: Path, python: str, pip_args: List[str], venv_args: List[str], verbose: bool, *, force: bool, include_dependencies: bool, ): try: exists = venv_dir.exists() and next(venv_dir.iterdir()) except StopIteration: exists = False if exists: if force: print(f"Installing to existing directory {str(venv_dir)!r}") else: print(f"{package!r} already seems to be installed. " f"Not modifying existing installation in {str(venv_dir)!r}. " "Pass '--force' to force installation.") return venv = Venv(venv_dir, python=python, verbose=verbose) try: venv.create_venv(venv_args, pip_args) venv.install_package(package_or_url, pip_args) if venv.get_venv_metadata_for_package(package).package_version is None: venv.remove_venv() raise PipxError( f"Could not find package {package}. Is the name correct?") _run_post_install_actions(venv, package, local_bin_dir, venv_dir, include_dependencies, force=force) except (Exception, KeyboardInterrupt): print("") venv.remove_venv() raise
def _download_and_run( venv_dir: Path, package: str, app: str, binary_args: List[str], python: str, pip_args: List[str], venv_args: List[str], verbose: bool, ): venv = Venv(venv_dir, python=python, verbose=verbose) venv.create_venv(venv_args, pip_args) venv.install_package(package, pip_args) if not (venv.bin_path / app).exists(): apps = venv.get_venv_metadata_for_package(package).apps raise PipxError( f"'{app}' executable script not found in package '{package}'. " "Available executable scripts: " f"{', '.join(b for b in apps)}") return venv.run_app(app, binary_args)
def _run_post_install_actions( venv: Venv, package: str, local_bin_dir: Path, venv_dir: Path, include_dependencies: bool, *, force: bool, ): metadata = venv.get_venv_metadata_for_package(package) if not metadata.app_paths and not include_dependencies: # No apps associated with this package and we aren't including dependencies. # This package has nothing for pipx to use, so this is an error. for dep, dependent_apps in metadata.app_paths_of_dependencies.items(): print( f"Note: Dependent package '{dep}' contains {len(dependent_apps)} apps" ) for app in dependent_apps: print(f" - {app.name}") if venv.safe_to_remove(): venv.remove_venv() if len(metadata.app_paths_of_dependencies.keys()): raise PipxError( f"No apps associated with package {package}. " "Try again with '--include-deps' to include apps of dependent packages, " "which are listed above. " "If you are attempting to install a library, pipx should not be used. " "Consider using pip or a similar tool instead.") else: raise PipxError( f"No apps associated with package {package}. " "If you are attempting to install a library, pipx should not be used. " "Consider using pip or a similar tool instead.") if metadata.apps: pass elif metadata.apps_of_dependencies and include_dependencies: pass else: # No apps associated with this package and we aren't including dependencies. # This package has nothing for pipx to use, so this is an error. if venv.safe_to_remove(): venv.remove_venv() raise PipxError( f"No apps associated with package {package} or its dependencies." "If you are attempting to install a library, pipx should not be used. " "Consider using pip or a similar tool instead.") _expose_apps_globally(local_bin_dir, metadata.app_paths, package, force=force) if include_dependencies: for _, app_paths in metadata.app_paths_of_dependencies.items(): _expose_apps_globally(local_bin_dir, app_paths, package, force=force) print(_get_package_summary(venv_dir, package=package, new_install=True)) _warn_if_not_on_path(local_bin_dir) print(f"done! {stars}", file=sys.stderr)