From 583483c1496c02fee76e1bb3ff290a4805733e97 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Mon, 22 Jul 2024 18:11:27 +0000 Subject: [PATCH 01/25] Initial version --- pyproject.toml | 6 ++ src/pipx/backend.py | 22 ++++ src/pipx/commands/install.py | 6 +- src/pipx/commands/interpreter.py | 2 + src/pipx/commands/reinstall.py | 8 ++ src/pipx/commands/run.py | 20 +++- src/pipx/commands/upgrade.py | 5 +- src/pipx/constants.py | 2 + src/pipx/main.py | 20 ++++ src/pipx/pipx_metadata_file.py | 12 ++- src/pipx/venv.py | 177 +++++++++++++++++++++++-------- src/pipx/venv_inspect.py | 6 ++ 12 files changed, 232 insertions(+), 54 deletions(-) create mode 100644 src/pipx/backend.py diff --git a/pyproject.toml b/pyproject.toml index b78b5c2991..00a870ab8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,12 @@ dependencies = [ "tomli; python_version<'3.11'", "userpath!=1.9,>=1.6", ] +optional-dependencies.uv = [ + "uv>=0.2.27", +] +optional-dependencies.virtualenv = [ + "virtualenv>=20.26.3", +] urls."Bug Tracker" = "https://github.com/pypa/pipx/issues" urls.Documentation = "https://pipx.pypa.io" urls.Homepage = "https://pipx.pypa.io" diff --git a/src/pipx/backend.py b/src/pipx/backend.py new file mode 100644 index 0000000000..d11144342f --- /dev/null +++ b/src/pipx/backend.py @@ -0,0 +1,22 @@ +from pipx.constants import DEFAULT_BACKEND + +import logging +import shutil +from typing import Optional + +SUPPORTED_BACKEND = ("uv", "venv", "virtualenv") +SUPPORTED_INSTALLER = ("uv", "pip") + +logger = logging.getLogger(__name__) + + +def path_to_exec(executable: str, installer: bool = False) -> str: + path = shutil.which(executable) + if path: + return path + elif installer: + logger.warning(f"{executable} not found on PATH. Falling back to pip.") + return "pip" + else: + logger.warning(f"{executable} not found on PATH. Falling back to venv.") + return "venv" diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index dbf26ba28f..7e8eb8178a 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -34,6 +34,8 @@ def install( preinstall_packages: Optional[List[str]], suffix: str = "", python_flag_passed=False, + backend: str, + installer: str, ) -> ExitCode: """Returns pipx exit code.""" # package_spec is anything pip-installable, including package_name, vcs spec, @@ -58,7 +60,7 @@ def install( except StopIteration: exists = False - venv = Venv(venv_dir, python=python, verbose=verbose) + venv = Venv(venv_dir, python=python, verbose=verbose, backend=backend, installer=installer) venv.check_upgrade_shared_libs(pip_args=pip_args, verbose=verbose) if exists: if not reinstall and force and python_flag_passed: @@ -214,6 +216,8 @@ def install_all( include_dependencies=main_package.include_dependencies, preinstall_packages=[], suffix=main_package.suffix, + backend=venv_metadata.backend, + installer=venv_metadata.installer, ) # Install the injected packages diff --git a/src/pipx/commands/interpreter.py b/src/pipx/commands/interpreter.py index 6fea85b7b1..9f90ceaffd 100644 --- a/src/pipx/commands/interpreter.py +++ b/src/pipx/commands/interpreter.py @@ -134,6 +134,8 @@ def upgrade_interpreters(venv_container: VenvContainer, verbose: bool): local_man_dir=paths.ctx.man_dir, python=str(interpreter_python), verbose=verbose, + backend=venv.pipx_metadata.backend, + installer=venv.pipx_metadata.installer, ) upgraded.append((venv.name, interpreter_full_version, latest_micro_version)) diff --git a/src/pipx/commands/reinstall.py b/src/pipx/commands/reinstall.py index 72011c5deb..413a242576 100644 --- a/src/pipx/commands/reinstall.py +++ b/src/pipx/commands/reinstall.py @@ -25,6 +25,8 @@ def reinstall( local_man_dir: Path, python: str, verbose: bool, + backend: str, + installer: str, force_reinstall_shared_libs: bool = False, python_flag_passed: bool = False, ) -> ExitCode: @@ -76,6 +78,8 @@ def reinstall( preinstall_packages=[], suffix=venv.pipx_metadata.main_package.suffix, python_flag_passed=python_flag_passed, + backend=backend, + installer=installer, ) # now install injected packages @@ -105,6 +109,8 @@ def reinstall_all( local_man_dir: Path, python: str, verbose: bool, + backend: str, + installer: str, *, skip: Sequence[str], python_flag_passed: bool = False, @@ -127,6 +133,8 @@ def reinstall_all( local_man_dir=local_man_dir, python=python, verbose=verbose, + backend=backend, + installer=installer, force_reinstall_shared_libs=first_reinstall, python_flag_passed=python_flag_passed, ) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index c68dfcf8a2..d5f76874ad 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -78,6 +78,8 @@ def run_script( venv_args: List[str], verbose: bool, use_cache: bool, + backend: str, + installer: str, ) -> NoReturn: requirements = _get_requirements_from_script(content) if requirements is None: @@ -96,7 +98,7 @@ def run_script( if venv_dir.exists(): logger.info(f"Reusing cached venv {venv_dir}") else: - venv = Venv(venv_dir, python=python, verbose=verbose) + venv = Venv(venv_dir, python=python, verbose=verbose, backend=backend, installer=installer) venv.check_upgrade_shared_libs(pip_args=pip_args, verbose=verbose) venv.create_venv(venv_args, pip_args) venv.install_unmanaged_packages(requirements, pip_args) @@ -118,6 +120,8 @@ def run_package( pypackages: bool, verbose: bool, use_cache: bool, + backend: str, + installer: str, ) -> NoReturn: if which(app): logger.warning( @@ -151,7 +155,7 @@ def run_package( venv_dir = _get_temporary_venv_path([package_or_url], python, pip_args, venv_args) - venv = Venv(venv_dir) + venv = Venv(venv_dir, backend=backend, installer=installer) bin_path = venv.bin_path / app_filename _prepare_venv_cache(venv, bin_path, use_cache) @@ -171,6 +175,8 @@ def run_package( venv_args, use_cache, verbose, + backend, + installer, ) @@ -185,6 +191,8 @@ def run( pypackages: bool, verbose: bool, use_cache: bool, + backend: str, + installer: str, ) -> NoReturn: """Installs venv to temporary dir (or reuses cache), then runs app from package @@ -200,7 +208,7 @@ def run( content = None if spec is not None else maybe_script_content(app, is_path) if content is not None: - run_script(content, app_args, python, pip_args, venv_args, verbose, use_cache) + run_script(content, app_args, python, pip_args, venv_args, verbose, use_cache, backend, installer) else: package_or_url = spec if spec is not None else app run_package( @@ -213,6 +221,8 @@ def run( pypackages, verbose, use_cache, + backend, + installer, ) @@ -227,8 +237,10 @@ def _download_and_run( venv_args: List[str], use_cache: bool, verbose: bool, + backend: str, + installer: str, ) -> NoReturn: - venv = Venv(venv_dir, python=python, verbose=verbose) + venv = Venv(venv_dir, python=python, verbose=verbose, backend=backend, installer=installer) venv.check_upgrade_shared_libs(pip_args=pip_args, verbose=verbose) if venv.pipx_metadata.main_package.package is not None: diff --git a/src/pipx/commands/upgrade.py b/src/pipx/commands/upgrade.py index c5ea6f4d24..0a06fb84ea 100644 --- a/src/pipx/commands/upgrade.py +++ b/src/pipx/commands/upgrade.py @@ -7,7 +7,7 @@ from pipx import commands, paths from pipx.colors import bold, red from pipx.commands.common import expose_resources_globally -from pipx.constants import EXIT_CODE_OK, ExitCode +from pipx.constants import DEFAULT_BACKEND, DEFAULT_INSTALLER, EXIT_CODE_OK, ExitCode from pipx.emojis import sleep from pipx.package_specifier import parse_specifier_for_upgrade from pipx.util import PipxError, pipx_wrap @@ -137,6 +137,9 @@ def _upgrade_venv( include_dependencies=False, preinstall_packages=None, python_flag_passed=python_flag_passed, + # What to deal with this? Should we allow cli option? + backend=DEFAULT_BACKEND, + installer=DEFAULT_INSTALLER, ) return 0 else: diff --git a/src/pipx/constants.py b/src/pipx/constants.py index eec4f98dc8..f56f594857 100644 --- a/src/pipx/constants.py +++ b/src/pipx/constants.py @@ -9,6 +9,8 @@ MINIMUM_PYTHON_VERSION = "3.8" MAN_SECTIONS = ["man%d" % i for i in range(1, 10)] FETCH_MISSING_PYTHON = os.environ.get("PIPX_FETCH_MISSING_PYTHON", False) +DEFAULT_BACKEND = os.environ.get("PIPX_DEFAULT_BACKEND", "venv") +DEFAULT_INSTALLER = os.environ.get("PIPX_DEFAULT_INSTALLER", "pip") ExitCode = NewType("ExitCode", int) diff --git a/src/pipx/main.py b/src/pipx/main.py index 4800f64313..7bb9752ab7 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -21,9 +21,12 @@ from pipx import commands, constants, paths from pipx.animate import hide_cursor, show_cursor +from pipx.backend import SUPPORTED_BACKEND, SUPPORTED_INSTALLER from pipx.colors import bold, green from pipx.commands.environment import ENVIRONMENT_VARIABLES from pipx.constants import ( + DEFAULT_BACKEND, + DEFAULT_INSTALLER, EXIT_CODE_OK, EXIT_CODE_SPECIFIED_PYTHON_EXECUTABLE_NOT_FOUND, MINIMUM_PYTHON_VERSION, @@ -275,6 +278,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar args.pypackages, verbose, not args.no_cache, + args.backend, + args.installer, ) # We should never reach here because run() is NoReturn. return ExitCode(1) @@ -295,6 +300,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar preinstall_packages=args.preinstall, suffix=args.suffix, python_flag_passed=python_flag_passed, + backend=args.backend, + installer=args.installer, ) elif args.command == "install-all": return commands.install_all( @@ -396,6 +403,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar local_man_dir=paths.ctx.man_dir, python=args.python, verbose=verbose, + backend=args.backend, + installer=args.installer, python_flag_passed=python_flag_passed, ) elif args.command == "reinstall-all": @@ -405,6 +414,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar paths.ctx.man_dir, args.python, verbose, + args.backend, + args.installer, skip=skip_list, python_flag_passed=python_flag_passed, ) @@ -450,6 +461,13 @@ def add_include_dependencies(parser: argparse.ArgumentParser) -> None: parser.add_argument("--include-deps", help="Include apps of dependent packages", action="store_true") +def add_backend_and_installer(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--backend", help="Virtual environment backend to use", choices=SUPPORTED_BACKEND, default=DEFAULT_BACKEND + ) + parser.add_argument("--installer", help="Installer to use", choices=SUPPORTED_INSTALLER, default=DEFAULT_INSTALLER) + + def add_python_options(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--python", @@ -502,6 +520,7 @@ def _add_install(subparsers: argparse._SubParsersAction, shared_parser: argparse ), ) add_pip_venv_args(p) + add_backend_and_installer(p) def _add_install_all(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None: @@ -849,6 +868,7 @@ def _add_run(subparsers: argparse._SubParsersAction, shared_parser: argparse.Arg p.add_argument("--spec", help=SPEC_HELP) add_python_options(p) add_pip_venv_args(p) + add_backend_and_installer(p) p.set_defaults(subparser=p) # modify usage text to show required app argument diff --git a/src/pipx/pipx_metadata_file.py b/src/pipx/pipx_metadata_file.py index 3362698e9f..ee128bc61d 100644 --- a/src/pipx/pipx_metadata_file.py +++ b/src/pipx/pipx_metadata_file.py @@ -54,7 +54,8 @@ class PipxMetadata: # V0.3 -> Add man pages fields # V0.4 -> Add source interpreter # V0.5 -> Add pinned - __METADATA_VERSION__: str = "0.5" + # V0.6 -> Add installer, backend + __METADATA_VERSION__: str = "0.6" def __init__(self, venv_dir: Path, read: bool = True): self.venv_dir = venv_dir @@ -83,6 +84,8 @@ def __init__(self, venv_dir: Path, read: bool = True): self.source_interpreter: Optional[Path] = None self.venv_args: List[str] = [] self.injected_packages: Dict[str, PackageInfo] = {} + self.backend: str = "" + self.installer: str = "" if read: self.read() @@ -90,6 +93,8 @@ def __init__(self, venv_dir: Path, read: bool = True): def to_dict(self) -> Dict[str, Any]: return { "main_package": asdict(self.main_package), + "backend": self.backend, + "installer": self.installer, "python_version": self.python_version, "source_interpreter": self.source_interpreter, "venv_args": self.venv_args, @@ -100,6 +105,9 @@ def to_dict(self) -> Dict[str, Any]: def _convert_legacy_metadata(self, metadata_dict: Dict[str, Any]) -> Dict[str, Any]: if metadata_dict["pipx_metadata_version"] in (self.__METADATA_VERSION__): pass + elif metadata_dict["pipx_metadata_version"] == "0.5": + metadata_dict["backend"] = "venv" + metadata_dict["installer"] = "pip" elif metadata_dict["pipx_metadata_version"] == "0.4": metadata_dict["pinned"] = False elif metadata_dict["pipx_metadata_version"] in ("0.2", "0.3"): @@ -132,6 +140,8 @@ def from_dict(self, input_dict: Dict[str, Any]) -> None: f"{name}{data.get('suffix', '')}": PackageInfo(**data) for (name, data) in input_dict["injected_packages"].items() } + self.backend = input_dict["backend"] + self.installer = input_dict["installer"] def _validate_before_write(self) -> None: if ( diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 422de3da08..56b4e84d31 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -17,7 +17,8 @@ from packaging.utils import canonicalize_name from pipx.animate import animate -from pipx.constants import PIPX_SHARED_PTH, ExitCode +from pipx.backend import path_to_exec +from pipx.constants import DEFAULT_BACKEND, DEFAULT_INSTALLER, PIPX_SHARED_PTH, ExitCode from pipx.emojis import hazard from pipx.interpreter import DEFAULT_PYTHON from pipx.package_specifier import ( @@ -83,18 +84,32 @@ def get_venv_dir(self, package_name: str) -> Path: class Venv: """Abstraction for a virtual environment with various useful methods for pipx""" - def __init__(self, path: Path, *, verbose: bool = False, python: str = DEFAULT_PYTHON) -> None: + def __init__( + self, + path: Path, + *, + verbose: bool = False, + backend: str = DEFAULT_BACKEND, + installer: str = DEFAULT_INSTALLER, + python: str = DEFAULT_PYTHON, + ) -> None: self.root = path self.python = python self.bin_path, self.python_path, self.man_path = get_venv_paths(self.root) self.pipx_metadata = PipxMetadata(venv_dir=path) self.verbose = verbose self.do_animation = not verbose + self.backend = backend + self.installer = installer try: self._existing = self.root.exists() and bool(next(self.root.iterdir())) except StopIteration: self._existing = False + if self._existing: + self.backend = self.pipx_metadata.backend + self.installer = self.pipx_metadata.installer + def check_upgrade_shared_libs(self, verbose: bool, pip_args: List[str], force_upgrade: bool = False): """ If necessary, run maintenance tasks to keep the shared libs up-to-date. @@ -137,8 +152,11 @@ def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None + # elif self.backend == "uv": + # # No need to use shared lib for uv + # return False else: - # always use shared libs when creating a new venv + # always use shared libs when creating a new venv with venv and virtualenv return True @property @@ -162,13 +180,20 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared override_shared -- Override installing shared libraries to the pipx shared directory (default False) """ logger.info("Creating virtual environment") - with animate("creating virtual environment", self.do_animation): - cmd = [self.python, "-m", "venv"] - if not override_shared: - cmd.append("--without-pip") + with animate(f"creating virtual environment using {self.backend}", self.do_animation): + if self.backend == "venv": + cmd = [self.python, "-m", "venv"] + if not override_shared: + cmd.append("--without-pip") + elif self.backend == "uv": + cmd = [path_to_exec("uv"), "venv"] + elif self.backend == "virtualenv": + cmd = [path_to_exec("virtualenv")] + if not override_shared: + cmd.append("--no-pip") venv_process = run_subprocess(cmd + venv_args + [str(self.root)], run_dir=str(self.root)) subprocess_post_check(venv_process) - + # if self.backend != "uv": shared_libs.create(verbose=self.verbose, pip_args=pip_args) if not override_shared: pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH @@ -188,6 +213,8 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared source_interpreter = shutil.which(self.python) if source_interpreter: self.pipx_metadata.source_interpreter = Path(source_interpreter) + if not self._existing: + self.pipx_metadata.backend = self.backend def safe_to_remove(self) -> bool: return not self._existing @@ -212,14 +239,19 @@ def upgrade_packaging_libraries(self, pip_args: List[str]) -> None: else: # TODO: setuptools and wheel? Original code didn't bother # but shared libs code does. - self.upgrade_package_no_metadata("pip", pip_args) + if self.backend != "uv": + self.upgrade_package_no_metadata("pip", pip_args) def uninstall_package(self, package: str, was_injected: bool = False): try: logger.info("Uninstalling %s", package) with animate(f"uninstalling {package}", self.do_animation): - cmd = ["uninstall", "-y"] + [package] - self._run_pip(cmd) + if self.installer != "uv": + cmd = ["uninstall", "-y"] + [package] + self._run_pip(cmd) + else: + cmd = ["uninstall"] + [package] + self._run_uv(cmd) except PipxError as e: logger.info(e) raise PipxError(f"Error uninstalling {package}.") from None @@ -244,19 +276,30 @@ def install_package( # check syntax and clean up spec and pip_args (package_or_url, pip_args) = parse_specifier_for_install(package_or_url, pip_args) - logger.info("Installing %s", package_descr := full_package_description(package_name, package_or_url)) + logger.info( + "Installing %s", + package_descr := full_package_description(package_name, package_or_url), + ) with animate(f"installing {package_descr}", self.do_animation): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. - cmd = [ - str(self.python_path), - "-m", - "pip", - "--no-input", - "install", - *pip_args, - package_or_url, - ] + if self.installer != "uv": + cmd = [ + str(self.python_path), + "-m", + "pip", + "--no-input", + "install", + *pip_args, + package_or_url, + ] + else: + cmd = [ + path_to_exec("uv", installer=True), + "pip", + "install", + package_or_url, + ] # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False, run_dir=str(self.root)) @@ -264,6 +307,9 @@ def install_package( if pip_process.returncode: raise PipxError(f"Error installing {full_package_description(package_name, package_or_url)}.") + if not self._existing: + self.pipx_metadata.installer = self.installer + self.update_package_metadata( package_name=package_name, package_or_url=package_or_url, @@ -293,15 +339,23 @@ def install_unmanaged_packages(self, requirements: List[str], pip_args: List[str with animate(f"installing {package_descr}", self.do_animation): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. - cmd = [ - str(self.python_path), - "-m", - "pip", - "--no-input", - "install", - *pip_args, - *requirements, - ] + if self.installer != "uv": + cmd = [ + str(self.python_path), + "-m", + "pip", + "--no-input", + "install", + *pip_args, + *requirements, + ] + else: + cmd = [ + path_to_exec(self.installer, installer=True), + "pip", + "install", + *requirements, + ] # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False, run_dir=str(self.root)) @@ -312,16 +366,25 @@ def install_unmanaged_packages(self, requirements: List[str], pip_args: List[str def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> str: with animate(f"determining package name from {package_or_url!r}", self.do_animation): old_package_set = self.list_installed_packages() - cmd = [ - "--no-input", - "install", - "--no-dependencies", - *pip_args, - package_or_url, - ] - pip_process = self._run_pip(cmd) - subprocess_post_check(pip_process, raise_error=False) - if pip_process.returncode: + if self.installer != "uv": + cmd = [ + "--no-input", + "install", + "--no-dependencies", + *pip_args, + package_or_url, + ] + install_process = self._run_pip(cmd) + else: + cmd = [ + "install", + "--no-deps", + package_or_url, + ] + install_process = self._run_uv(cmd) + + subprocess_post_check(install_process, raise_error=False) + if install_process.returncode: raise PipxError( f""" Cannot determine package name from spec {package_or_url!r}. @@ -347,7 +410,9 @@ def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> s def get_venv_metadata_for_package(self, package_name: str, package_extras: Set[str]) -> VenvMetadata: data_start = time.time() - venv_metadata = inspect_venv(package_name, package_extras, self.bin_path, self.python_path, self.man_path) + venv_metadata = inspect_venv( + package_name, package_extras, self.bin_path, self.python_path, self.man_path, self.backend, self.installer + ) logger.info(f"get_venv_metadata_for_package: {1e3*(time.time()-data_start):.0f}ms") return venv_metadata @@ -439,10 +504,16 @@ def has_package(self, package_name: str) -> bool: return bool(list(Distribution.discover(name=package_name, path=[str(get_site_packages(self.python_path))]))) def upgrade_package_no_metadata(self, package_name: str, pip_args: List[str]) -> None: - logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_name)) + logger.info( + "Upgrading %s", + package_descr := full_package_description(package_name, package_name), + ) with animate(f"upgrading {package_descr}", self.do_animation): - pip_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_name]) - subprocess_post_check(pip_process) + if self.installer != "uv": + upgrade_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_name]) + else: + upgrade_process = self._run_uv(["install", "--upgrade", package_name]) + subprocess_post_check(upgrade_process) def upgrade_package( self, @@ -454,10 +525,16 @@ def upgrade_package( is_main_package: bool, suffix: str = "", ) -> None: - logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_or_url)) + logger.info( + "Upgrading %s", + package_descr := full_package_description(package_name, package_or_url), + ) with animate(f"upgrading {package_descr}", self.do_animation): - pip_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_or_url]) - subprocess_post_check(pip_process) + if self.installer != "uv": + upgrade_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_or_url]) + else: + upgrade_process = self._run_uv(["pip", "install", "--upgrade", package_or_url]) + subprocess_post_check(upgrade_process) self.update_package_metadata( package_name=package_name, @@ -469,6 +546,12 @@ def upgrade_package( suffix=suffix, ) + def _run_uv(self, cmd: List[str]) -> "CompletedProcess[str]": + cmd = [path_to_exec("uv")] + cmd + if not self.verbose: + cmd.append("-q") + return run_subprocess(cmd, run_dir=str(self.root)) + def _run_pip(self, cmd: List[str]) -> "CompletedProcess[str]": cmd = [str(self.python_path), "-m", "pip"] + cmd if not self.verbose: diff --git a/src/pipx/venv_inspect.py b/src/pipx/venv_inspect.py index 8d6b0ce400..86da1811e3 100644 --- a/src/pipx/venv_inspect.py +++ b/src/pipx/venv_inspect.py @@ -36,6 +36,8 @@ class VenvMetadata(NamedTuple): man_paths_of_dependencies: Dict[str, List[Path]] package_version: str python_version: str + backend: str + installer: str def get_dist(package: str, distributions: Collection[metadata.Distribution]) -> Optional[metadata.Distribution]: @@ -253,6 +255,8 @@ def inspect_venv( venv_bin_path: Path, venv_python_path: Path, venv_man_path: Path, + backend: str, + installer: str, ) -> VenvMetadata: app_paths_of_dependencies: Dict[str, List[Path]] = {} apps_of_dependencies: List[str] = [] @@ -316,4 +320,6 @@ def inspect_venv( man_paths_of_dependencies=man_paths_of_dependencies, package_version=root_dist.version, python_version=venv_python_version, + backend=backend, + installer=installer, ) From 89182f98f6ec34d5086613c9787a1d619ff23e94 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:19:25 +0000 Subject: [PATCH 02/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pipx/backend.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pipx/backend.py b/src/pipx/backend.py index d11144342f..4d66d78862 100644 --- a/src/pipx/backend.py +++ b/src/pipx/backend.py @@ -1,8 +1,5 @@ -from pipx.constants import DEFAULT_BACKEND - import logging import shutil -from typing import Optional SUPPORTED_BACKEND = ("uv", "venv", "virtualenv") SUPPORTED_INSTALLER = ("uv", "pip") From 865d10d1f1bcd278c1ef46c11e699dba9a41233a Mon Sep 17 00:00:00 2001 From: meowmeow Date: Tue, 23 Jul 2024 05:03:54 +0000 Subject: [PATCH 03/25] Update names of constants --- src/pipx/backend.py | 4 ++-- src/pipx/main.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pipx/backend.py b/src/pipx/backend.py index 4d66d78862..10910b0baa 100644 --- a/src/pipx/backend.py +++ b/src/pipx/backend.py @@ -1,8 +1,8 @@ import logging import shutil -SUPPORTED_BACKEND = ("uv", "venv", "virtualenv") -SUPPORTED_INSTALLER = ("uv", "pip") +SUPPORTED_VENV_BACKENDS = ("uv", "venv", "virtualenv") +SUPPORTED_INSTALLERS = ("uv", "pip") logger = logging.getLogger(__name__) diff --git a/src/pipx/main.py b/src/pipx/main.py index 7bb9752ab7..7077579d67 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -21,7 +21,7 @@ from pipx import commands, constants, paths from pipx.animate import hide_cursor, show_cursor -from pipx.backend import SUPPORTED_BACKEND, SUPPORTED_INSTALLER +from pipx.backend import SUPPORTED_VENV_BACKENDS, SUPPORTED_INSTALLERS from pipx.colors import bold, green from pipx.commands.environment import ENVIRONMENT_VARIABLES from pipx.constants import ( @@ -463,9 +463,9 @@ def add_include_dependencies(parser: argparse.ArgumentParser) -> None: def add_backend_and_installer(parser: argparse.ArgumentParser) -> None: parser.add_argument( - "--backend", help="Virtual environment backend to use", choices=SUPPORTED_BACKEND, default=DEFAULT_BACKEND + "--backend", help="Virtual environment backend to use", choices=SUPPORTED_VENV_BACKENDS, default=DEFAULT_BACKEND ) - parser.add_argument("--installer", help="Installer to use", choices=SUPPORTED_INSTALLER, default=DEFAULT_INSTALLER) + parser.add_argument("--installer", help="Installer to use", choices=SUPPORTED_INSTALLERS, default=DEFAULT_INSTALLER) def add_python_options(parser: argparse.ArgumentParser) -> None: From ef9abf47bc114b8fa1f9b99a80872a2c32334ab9 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Tue, 23 Jul 2024 05:13:04 +0000 Subject: [PATCH 04/25] Revert style changes --- src/pipx/main.py | 2 +- src/pipx/venv.py | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/pipx/main.py b/src/pipx/main.py index 7077579d67..a76813b753 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -21,7 +21,7 @@ from pipx import commands, constants, paths from pipx.animate import hide_cursor, show_cursor -from pipx.backend import SUPPORTED_VENV_BACKENDS, SUPPORTED_INSTALLERS +from pipx.backend import SUPPORTED_INSTALLERS, SUPPORTED_VENV_BACKENDS from pipx.colors import bold, green from pipx.commands.environment import ENVIRONMENT_VARIABLES from pipx.constants import ( diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 56b4e84d31..66bdfef8f6 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -276,10 +276,7 @@ def install_package( # check syntax and clean up spec and pip_args (package_or_url, pip_args) = parse_specifier_for_install(package_or_url, pip_args) - logger.info( - "Installing %s", - package_descr := full_package_description(package_name, package_or_url), - ) + logger.info("Installing %s", package_descr := full_package_description(package_name, package_or_url)) with animate(f"installing {package_descr}", self.do_animation): # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. @@ -504,10 +501,7 @@ def has_package(self, package_name: str) -> bool: return bool(list(Distribution.discover(name=package_name, path=[str(get_site_packages(self.python_path))]))) def upgrade_package_no_metadata(self, package_name: str, pip_args: List[str]) -> None: - logger.info( - "Upgrading %s", - package_descr := full_package_description(package_name, package_name), - ) + logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_name)) with animate(f"upgrading {package_descr}", self.do_animation): if self.installer != "uv": upgrade_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_name]) @@ -525,10 +519,7 @@ def upgrade_package( is_main_package: bool, suffix: str = "", ) -> None: - logger.info( - "Upgrading %s", - package_descr := full_package_description(package_name, package_or_url), - ) + logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_or_url)) with animate(f"upgrading {package_descr}", self.do_animation): if self.installer != "uv": upgrade_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_or_url]) From f3c142fc417915923763c791340116d94eb83e17 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Tue, 23 Jul 2024 05:30:01 +0000 Subject: [PATCH 05/25] Add backend and installer options to reinstall and reinstall-all command --- src/pipx/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pipx/main.py b/src/pipx/main.py index a76813b753..310c685411 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -751,6 +751,7 @@ def _add_reinstall(subparsers, venv_completer: VenvCompleter, shared_parser: arg ) p.add_argument("package").completer = venv_completer add_python_options(p) + add_backend_and_installer(p) def _add_reinstall_all(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None: @@ -772,6 +773,7 @@ def _add_reinstall_all(subparsers: argparse._SubParsersAction, shared_parser: ar parents=[shared_parser], ) add_python_options(p) + add_backend_and_installer(p) p.add_argument("--skip", nargs="+", default=[], help="skip these packages") From 095c83c8baff929efb7ea07faecbe17389c8c225 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Tue, 23 Jul 2024 14:15:36 +0000 Subject: [PATCH 06/25] Improve logic --- src/pipx/backend.py | 6 +- src/pipx/pipx_metadata_file.py | 4 +- src/pipx/venv.py | 139 ++++++++------------ testdata/pipx_metadata_multiple_errors.json | 4 + 4 files changed, 66 insertions(+), 87 deletions(-) diff --git a/src/pipx/backend.py b/src/pipx/backend.py index 10910b0baa..1b32df0ddd 100644 --- a/src/pipx/backend.py +++ b/src/pipx/backend.py @@ -8,12 +8,14 @@ def path_to_exec(executable: str, installer: bool = False) -> str: + if executable == "venv" or "pip": + return executable path = shutil.which(executable) if path: return path elif installer: logger.warning(f"{executable} not found on PATH. Falling back to pip.") - return "pip" + return "" else: logger.warning(f"{executable} not found on PATH. Falling back to venv.") - return "venv" + return "" diff --git a/src/pipx/pipx_metadata_file.py b/src/pipx/pipx_metadata_file.py index ee128bc61d..f99b7f4d56 100644 --- a/src/pipx/pipx_metadata_file.py +++ b/src/pipx/pipx_metadata_file.py @@ -140,8 +140,8 @@ def from_dict(self, input_dict: Dict[str, Any]) -> None: f"{name}{data.get('suffix', '')}": PackageInfo(**data) for (name, data) in input_dict["injected_packages"].items() } - self.backend = input_dict["backend"] - self.installer = input_dict["installer"] + self.backend = input_dict["backend"] if input_dict.get("backend") else "venv" + self.installer = input_dict["installer"] if input_dict.get("installer") else "pip" def _validate_before_write(self) -> None: if ( diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 66bdfef8f6..d72534d3af 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -109,6 +109,11 @@ def __init__( if self._existing: self.backend = self.pipx_metadata.backend self.installer = self.pipx_metadata.installer + + if not path_to_exec(self.backend): + self.backend = "venv" + if not path_to_exec(self.installer): + self.installer = "pip" def check_upgrade_shared_libs(self, verbose: bool, pip_args: List[str], force_upgrade: bool = False): """ @@ -152,9 +157,9 @@ def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None - # elif self.backend == "uv": - # # No need to use shared lib for uv - # return False + elif self.backend == "uv": + # No need to use shared lib for uv + return False else: # always use shared libs when creating a new venv with venv and virtualenv return True @@ -175,6 +180,12 @@ def main_package_name(self) -> str: else: return self.pipx_metadata.main_package.package + def default_create_venv_cmd(self, override_shared: bool = False) -> List[str]: + cmd = [self.python, "-m", "venv"] + if not override_shared: + cmd.append("--without-pip") + return cmd + def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared: bool = False) -> None: """ override_shared -- Override installing shared libraries to the pipx shared directory (default False) @@ -182,9 +193,7 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared logger.info("Creating virtual environment") with animate(f"creating virtual environment using {self.backend}", self.do_animation): if self.backend == "venv": - cmd = [self.python, "-m", "venv"] - if not override_shared: - cmd.append("--without-pip") + cmd = self.default_create_venv_cmd(override_shared=override_shared) elif self.backend == "uv": cmd = [path_to_exec("uv"), "venv"] elif self.backend == "virtualenv": @@ -193,9 +202,9 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared cmd.append("--no-pip") venv_process = run_subprocess(cmd + venv_args + [str(self.root)], run_dir=str(self.root)) subprocess_post_check(venv_process) - # if self.backend != "uv": - shared_libs.create(verbose=self.verbose, pip_args=pip_args) - if not override_shared: + if self.backend != "uv": + shared_libs.create(verbose=self.verbose, pip_args=pip_args) + if not override_shared and self.backend != "uv": pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH # write path pointing to the shared libs site-packages directory # example pipx_pth location: @@ -278,30 +287,13 @@ def install_package( logger.info("Installing %s", package_descr := full_package_description(package_name, package_or_url)) with animate(f"installing {package_descr}", self.do_animation): - # do not use -q with `pip install` so subprocess_post_check_pip_errors - # has more information to analyze in case of failure. - if self.installer != "uv": - cmd = [ - str(self.python_path), - "-m", - "pip", - "--no-input", - "install", - *pip_args, - package_or_url, - ] - else: - cmd = [ - path_to_exec("uv", installer=True), - "pip", - "install", - package_or_url, - ] # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() - pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False, run_dir=str(self.root)) - subprocess_post_check_handle_pip_error(pip_process) - if pip_process.returncode: + install_process = self._run_installer( + self.installer, [*pip_args, package_or_url], quiet=True, log_stdout=False, log_stderr=False + ) + subprocess_post_check_handle_pip_error(install_process) + if install_process.returncode: raise PipxError(f"Error installing {full_package_description(package_name, package_or_url)}.") if not self._existing: @@ -334,51 +326,20 @@ def install_unmanaged_packages(self, requirements: List[str], pip_args: List[str # pip resolve conflicts correctly. logger.info("Installing %s", package_descr := ", ".join(requirements)) with animate(f"installing {package_descr}", self.do_animation): - # do not use -q with `pip install` so subprocess_post_check_pip_errors - # has more information to analyze in case of failure. - if self.installer != "uv": - cmd = [ - str(self.python_path), - "-m", - "pip", - "--no-input", - "install", - *pip_args, - *requirements, - ] - else: - cmd = [ - path_to_exec(self.installer, installer=True), - "pip", - "install", - *requirements, - ] # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() - pip_process = run_subprocess(cmd, log_stdout=False, log_stderr=False, run_dir=str(self.root)) - subprocess_post_check_handle_pip_error(pip_process) - if pip_process.returncode: + install_process = self._run_installer( + self.installer, [*pip_args, *requirements], quiet=True, log_stdout=False, log_stderr=False + ) + subprocess_post_check_handle_pip_error(install_process) + if install_process.returncode: raise PipxError(f"Error installing {', '.join(requirements)}.") def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> str: with animate(f"determining package name from {package_or_url!r}", self.do_animation): old_package_set = self.list_installed_packages() - if self.installer != "uv": - cmd = [ - "--no-input", - "install", - "--no-dependencies", - *pip_args, - package_or_url, - ] - install_process = self._run_pip(cmd) - else: - cmd = [ - "install", - "--no-deps", - package_or_url, - ] - install_process = self._run_uv(cmd) + # TODO: rename pip_args to installer_args? But we can keep pip_args as implicit one + install_process = self._run_installer(self.installer, [*pip_args, package_or_url], no_deps=True) subprocess_post_check(install_process, raise_error=False) if install_process.returncode: @@ -503,10 +464,7 @@ def has_package(self, package_name: str) -> bool: def upgrade_package_no_metadata(self, package_name: str, pip_args: List[str]) -> None: logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_name)) with animate(f"upgrading {package_descr}", self.do_animation): - if self.installer != "uv": - upgrade_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_name]) - else: - upgrade_process = self._run_uv(["install", "--upgrade", package_name]) + upgrade_process = self._run_installer(self.installer, [*pip_args, "--upgrade", package_name]) subprocess_post_check(upgrade_process) def upgrade_package( @@ -521,10 +479,7 @@ def upgrade_package( ) -> None: logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_or_url)) with animate(f"upgrading {package_descr}", self.do_animation): - if self.installer != "uv": - upgrade_process = self._run_pip(["--no-input", "install"] + pip_args + ["--upgrade", package_or_url]) - else: - upgrade_process = self._run_uv(["pip", "install", "--upgrade", package_or_url]) + upgrade_process = self._run_installer(self.installer, [*pip_args, "--upgrade", package_or_url]) subprocess_post_check(upgrade_process) self.update_package_metadata( @@ -537,17 +492,35 @@ def upgrade_package( suffix=suffix, ) - def _run_uv(self, cmd: List[str]) -> "CompletedProcess[str]": - cmd = [path_to_exec("uv")] + cmd - if not self.verbose: + def _run_installer( + self, + installer: str, + cmd: List[str], + quiet: bool = True, + no_deps: bool = False, + log_stdout: bool = True, + log_stderr: bool = True, + ) -> "CompletedProcess[str]": + # do not use -q with `pip install` so subprocess_post_check_pip_errors + # has more information to analyze in case of failure. + if installer != "uv": + return self._run_pip(["--no-input", "install"] + (["--no-dependencies"] if no_deps else []) + cmd, quiet=quiet) + else: + return self._run_uv(["pip", "install"] + (["--no-deps"] if no_deps else []) + cmd, quiet=quiet) + + def _run_uv(self, cmd: List[str], quiet: bool = True) -> "CompletedProcess[str]": + cmd = [path_to_exec("uv", installer=True)] + cmd + ["--python", str(self.python_path)] + if not self.verbose and quiet: cmd.append("-q") return run_subprocess(cmd, run_dir=str(self.root)) - def _run_pip(self, cmd: List[str]) -> "CompletedProcess[str]": + def _run_pip( + self, cmd: List[str], quiet: bool = True, log_stdout: bool = True, log_stderr: bool = True + ) -> "CompletedProcess[str]": cmd = [str(self.python_path), "-m", "pip"] + cmd - if not self.verbose: + if not self.verbose and quiet: cmd.append("-q") - return run_subprocess(cmd, run_dir=str(self.root)) + return run_subprocess(cmd, log_stdout=log_stdout, log_stderr=log_stderr, run_dir=str(self.root)) def run_pip_get_exit_code(self, cmd: List[str]) -> ExitCode: cmd = [str(self.python_path), "-m", "pip"] + cmd diff --git a/testdata/pipx_metadata_multiple_errors.json b/testdata/pipx_metadata_multiple_errors.json index 7c00f184b1..ff1756839e 100644 --- a/testdata/pipx_metadata_multiple_errors.json +++ b/testdata/pipx_metadata_multiple_errors.json @@ -3,7 +3,9 @@ "venvs": { "dotenv": { "metadata": { + "backend": "venv", "injected_packages": {}, + "installer": "pip", "main_package": { "app_paths": [ ], @@ -32,7 +34,9 @@ }, "weblate": { "metadata": { + "backend": "venv", "injected_packages": {}, + "installer": "pip", "main_package": { "app_paths": [ ], From 4155cefd00a378b56e4b29f93e7a3b9096593448 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:21:33 +0000 Subject: [PATCH 07/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pipx/venv.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index d72534d3af..863a66caaf 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -109,7 +109,7 @@ def __init__( if self._existing: self.backend = self.pipx_metadata.backend self.installer = self.pipx_metadata.installer - + if not path_to_exec(self.backend): self.backend = "venv" if not path_to_exec(self.installer): @@ -504,7 +504,9 @@ def _run_installer( # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. if installer != "uv": - return self._run_pip(["--no-input", "install"] + (["--no-dependencies"] if no_deps else []) + cmd, quiet=quiet) + return self._run_pip( + ["--no-input", "install"] + (["--no-dependencies"] if no_deps else []) + cmd, quiet=quiet + ) else: return self._run_uv(["pip", "install"] + (["--no-deps"] if no_deps else []) + cmd, quiet=quiet) From 6ebe524fd2384af8dc3311635d572fca0c8953be Mon Sep 17 00:00:00 2001 From: meowmeow Date: Wed, 24 Jul 2024 06:22:21 +0000 Subject: [PATCH 08/25] Apply suggestions from code review --- src/pipx/backend.py | 6 +++--- src/pipx/venv.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pipx/backend.py b/src/pipx/backend.py index 1b32df0ddd..4a19f757d7 100644 --- a/src/pipx/backend.py +++ b/src/pipx/backend.py @@ -8,14 +8,14 @@ def path_to_exec(executable: str, installer: bool = False) -> str: - if executable == "venv" or "pip": + if executable in ("venv", "pip"): return executable path = shutil.which(executable) if path: return path elif installer: - logger.warning(f"{executable} not found on PATH. Falling back to pip.") + logger.warning(f"'{executable}' not found on PATH. Falling back to 'pip'.") return "" else: - logger.warning(f"{executable} not found on PATH. Falling back to venv.") + logger.warning(f"'{executable}' not found on PATH. Falling back to 'venv'.") return "" diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 863a66caaf..c39c9f6470 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -112,7 +112,7 @@ def __init__( if not path_to_exec(self.backend): self.backend = "venv" - if not path_to_exec(self.installer): + if not path_to_exec(self.installer, installer=True): self.installer = "pip" def check_upgrade_shared_libs(self, verbose: bool, pip_args: List[str], force_upgrade: bool = False): @@ -290,7 +290,7 @@ def install_package( # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() install_process = self._run_installer( - self.installer, [*pip_args, package_or_url], quiet=True, log_stdout=False, log_stderr=False + [*pip_args, package_or_url], quiet=True, log_stdout=False, log_stderr=False ) subprocess_post_check_handle_pip_error(install_process) if install_process.returncode: @@ -329,7 +329,7 @@ def install_unmanaged_packages(self, requirements: List[str], pip_args: List[str # no logging because any errors will be specially logged by # subprocess_post_check_handle_pip_error() install_process = self._run_installer( - self.installer, [*pip_args, *requirements], quiet=True, log_stdout=False, log_stderr=False + [*pip_args, *requirements], quiet=True, log_stdout=False, log_stderr=False ) subprocess_post_check_handle_pip_error(install_process) if install_process.returncode: @@ -339,7 +339,7 @@ def install_package_no_deps(self, package_or_url: str, pip_args: List[str]) -> s with animate(f"determining package name from {package_or_url!r}", self.do_animation): old_package_set = self.list_installed_packages() # TODO: rename pip_args to installer_args? But we can keep pip_args as implicit one - install_process = self._run_installer(self.installer, [*pip_args, package_or_url], no_deps=True) + install_process = self._run_installer([*pip_args, package_or_url], no_deps=True) subprocess_post_check(install_process, raise_error=False) if install_process.returncode: @@ -464,7 +464,7 @@ def has_package(self, package_name: str) -> bool: def upgrade_package_no_metadata(self, package_name: str, pip_args: List[str]) -> None: logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_name)) with animate(f"upgrading {package_descr}", self.do_animation): - upgrade_process = self._run_installer(self.installer, [*pip_args, "--upgrade", package_name]) + upgrade_process = self._run_installer([*pip_args, "--upgrade", package_name]) subprocess_post_check(upgrade_process) def upgrade_package( @@ -479,7 +479,7 @@ def upgrade_package( ) -> None: logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_or_url)) with animate(f"upgrading {package_descr}", self.do_animation): - upgrade_process = self._run_installer(self.installer, [*pip_args, "--upgrade", package_or_url]) + upgrade_process = self._run_installer([*pip_args, "--upgrade", package_or_url]) subprocess_post_check(upgrade_process) self.update_package_metadata( @@ -494,7 +494,6 @@ def upgrade_package( def _run_installer( self, - installer: str, cmd: List[str], quiet: bool = True, no_deps: bool = False, @@ -503,7 +502,7 @@ def _run_installer( ) -> "CompletedProcess[str]": # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. - if installer != "uv": + if self.installer != "uv": return self._run_pip( ["--no-input", "install"] + (["--no-dependencies"] if no_deps else []) + cmd, quiet=quiet ) From ca024fd694c2da002876cb086298adeec963e2e5 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Wed, 24 Jul 2024 09:43:48 +0000 Subject: [PATCH 09/25] Apply suggestions from code review --- src/pipx/util.py | 24 ++++++++++++------------ src/pipx/venv.py | 19 +++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/pipx/util.py b/src/pipx/util.py index 0951232373..a650b34f2c 100644 --- a/src/pipx/util.py +++ b/src/pipx/util.py @@ -325,27 +325,27 @@ def analyze_pip_output(pip_stdout: str, pip_stderr: str) -> None: print(f" {relevant_saved[0]}", file=sys.stderr) -def subprocess_post_check_handle_pip_error( - completed_process: "subprocess.CompletedProcess[str]", +def subprocess_post_check_handle_installer_error( + installer: str, completed_process: "subprocess.CompletedProcess[str]", ) -> None: if completed_process.returncode: logger.info(f"{' '.join(completed_process.args)!r} failed") # Save STDOUT and STDERR to file in pipx/logs/ if paths.ctx.log_file is None: raise PipxError("Pipx internal error: No log_file present.") - pip_error_file = paths.ctx.log_file.parent / (paths.ctx.log_file.stem + "_pip_errors.log") - with pip_error_file.open("a", encoding="utf-8") as pip_error_fh: - print("PIP STDOUT", file=pip_error_fh) - print("----------", file=pip_error_fh) + installer_error_file = paths.ctx.log_file.parent / (paths.ctx.log_file.stem + f"_{installer}_errors.log") + with installer_error_file.open("a", encoding="utf-8") as installer_error_fh: + print(f"{installer.upper()} STDOUT", file=installer_error_fh) + print("----------", file=installer_error_fh) if completed_process.stdout is not None: - print(completed_process.stdout, file=pip_error_fh, end="") - print("\nPIP STDERR", file=pip_error_fh) - print("----------", file=pip_error_fh) + print(completed_process.stdout, file=installer_error_fh, end="") + print(f"\n{installer.upper()} STDERR", file=installer_error_fh) + print("----------", file=installer_error_fh) if completed_process.stderr is not None: - print(completed_process.stderr, file=pip_error_fh, end="") - - logger.error(f"Fatal error from pip prevented installation. Full pip output in file:\n {pip_error_file}") + print(completed_process.stderr, file=installer_error_fh, end="") + logger.error(f"Fatal error from {installer} prevented installation. Full {installer} output in file:\n {installer_error_file}") + # TODO: we need to check whether uv's output is similar to the one in pip, if yes, we can keep this function analyze_pip_output(completed_process.stdout, completed_process.stderr) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index c39c9f6470..d1f079ca7f 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -39,7 +39,7 @@ rmdir, run_subprocess, subprocess_post_check, - subprocess_post_check_handle_pip_error, + subprocess_post_check_handle_installer_error, ) from pipx.venv_inspect import VenvMetadata, inspect_venv @@ -288,11 +288,11 @@ def install_package( logger.info("Installing %s", package_descr := full_package_description(package_name, package_or_url)) with animate(f"installing {package_descr}", self.do_animation): # no logging because any errors will be specially logged by - # subprocess_post_check_handle_pip_error() + # subprocess_post_check_handle_installer_error() install_process = self._run_installer( [*pip_args, package_or_url], quiet=True, log_stdout=False, log_stderr=False ) - subprocess_post_check_handle_pip_error(install_process) + subprocess_post_check_handle_installer_error(self.installer, install_process) if install_process.returncode: raise PipxError(f"Error installing {full_package_description(package_name, package_or_url)}.") @@ -327,11 +327,11 @@ def install_unmanaged_packages(self, requirements: List[str], pip_args: List[str logger.info("Installing %s", package_descr := ", ".join(requirements)) with animate(f"installing {package_descr}", self.do_animation): # no logging because any errors will be specially logged by - # subprocess_post_check_handle_pip_error() + # subprocess_post_check_handle_installer_error() install_process = self._run_installer( [*pip_args, *requirements], quiet=True, log_stdout=False, log_stderr=False ) - subprocess_post_check_handle_pip_error(install_process) + subprocess_post_check_handle_installer_error(self.installer, install_process) if install_process.returncode: raise PipxError(f"Error installing {', '.join(requirements)}.") @@ -502,12 +502,11 @@ def _run_installer( ) -> "CompletedProcess[str]": # do not use -q with `pip install` so subprocess_post_check_pip_errors # has more information to analyze in case of failure. - if self.installer != "uv": - return self._run_pip( - ["--no-input", "install"] + (["--no-dependencies"] if no_deps else []) + cmd, quiet=quiet - ) + install_cmd = ["install"] + (["--no-deps"] if no_deps else []) + cmd + if self.installer == "pip": + return self._run_pip(["--no-input"] + install_cmd, quiet=quiet) else: - return self._run_uv(["pip", "install"] + (["--no-deps"] if no_deps else []) + cmd, quiet=quiet) + return self._run_uv(["pip"] + install_cmd, quiet=quiet) def _run_uv(self, cmd: List[str], quiet: bool = True) -> "CompletedProcess[str]": cmd = [path_to_exec("uv", installer=True)] + cmd + ["--python", str(self.python_path)] From bdd18f4fa5c8c484d67638bde2bbed677833062c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:44:52 +0000 Subject: [PATCH 10/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pipx/util.py | 7 +++++-- src/pipx/venv.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pipx/util.py b/src/pipx/util.py index a650b34f2c..e5edbdedf5 100644 --- a/src/pipx/util.py +++ b/src/pipx/util.py @@ -326,7 +326,8 @@ def analyze_pip_output(pip_stdout: str, pip_stderr: str) -> None: def subprocess_post_check_handle_installer_error( - installer: str, completed_process: "subprocess.CompletedProcess[str]", + installer: str, + completed_process: "subprocess.CompletedProcess[str]", ) -> None: if completed_process.returncode: logger.info(f"{' '.join(completed_process.args)!r} failed") @@ -344,7 +345,9 @@ def subprocess_post_check_handle_installer_error( if completed_process.stderr is not None: print(completed_process.stderr, file=installer_error_fh, end="") - logger.error(f"Fatal error from {installer} prevented installation. Full {installer} output in file:\n {installer_error_file}") + logger.error( + f"Fatal error from {installer} prevented installation. Full {installer} output in file:\n {installer_error_file}" + ) # TODO: we need to check whether uv's output is similar to the one in pip, if yes, we can keep this function analyze_pip_output(completed_process.stdout, completed_process.stderr) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index d1f079ca7f..5e9daf4d21 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -506,7 +506,7 @@ def _run_installer( if self.installer == "pip": return self._run_pip(["--no-input"] + install_cmd, quiet=quiet) else: - return self._run_uv(["pip"] + install_cmd, quiet=quiet) + return self._run_uv(["pip"] + install_cmd, quiet=quiet) def _run_uv(self, cmd: List[str], quiet: bool = True) -> "CompletedProcess[str]": cmd = [path_to_exec("uv", installer=True)] + cmd + ["--python", str(self.python_path)] From 87c1821716c604b299aebe33fa424d4a53543b06 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Fri, 26 Jul 2024 03:50:07 +0000 Subject: [PATCH 11/25] Apply suggestions from code review --- src/pipx/venv.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 5e9daf4d21..fa26665bdc 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -180,12 +180,6 @@ def main_package_name(self) -> str: else: return self.pipx_metadata.main_package.package - def default_create_venv_cmd(self, override_shared: bool = False) -> List[str]: - cmd = [self.python, "-m", "venv"] - if not override_shared: - cmd.append("--without-pip") - return cmd - def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared: bool = False) -> None: """ override_shared -- Override installing shared libraries to the pipx shared directory (default False) @@ -193,7 +187,9 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared logger.info("Creating virtual environment") with animate(f"creating virtual environment using {self.backend}", self.do_animation): if self.backend == "venv": - cmd = self.default_create_venv_cmd(override_shared=override_shared) + cmd = [self.python, "-m", "venv"] + if not override_shared: + cmd.append("--without-pip") elif self.backend == "uv": cmd = [path_to_exec("uv"), "venv"] elif self.backend == "virtualenv": @@ -255,7 +251,7 @@ def uninstall_package(self, package: str, was_injected: bool = False): try: logger.info("Uninstalling %s", package) with animate(f"uninstalling {package}", self.do_animation): - if self.installer != "uv": + if self.installer == "pip": cmd = ["uninstall", "-y"] + [package] self._run_pip(cmd) else: @@ -509,7 +505,10 @@ def _run_installer( return self._run_uv(["pip"] + install_cmd, quiet=quiet) def _run_uv(self, cmd: List[str], quiet: bool = True) -> "CompletedProcess[str]": - cmd = [path_to_exec("uv", installer=True)] + cmd + ["--python", str(self.python_path)] + if not (uv_path := path_to_exec("uv", installer=True)): + self._run_pip(cmd=cmd, quiet=quiet) + else: + cmd = [uv_path] + cmd + ["--python", str(self.python_path)] if not self.verbose and quiet: cmd.append("-q") return run_subprocess(cmd, run_dir=str(self.root)) From 18c7e7c76100c6536b868de10778ec469623df52 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Tue, 6 Aug 2024 14:39:16 +0000 Subject: [PATCH 12/25] Add tests --- noxfile.py | 2 +- src/pipx/commands/upgrade.py | 16 ++++-- src/pipx/main.py | 3 ++ src/pipx/venv.py | 17 +++---- tests/conftest.py | 2 +- tests/test_backend.py | 96 ++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 tests/test_backend.py diff --git a/noxfile.py b/noxfile.py index d16eac0584..d55177d454 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,7 +17,7 @@ "markdown-gfm-admonition", ] MAN_DEPENDENCIES = ["argparse-manpage[setuptools]"] -TEST_DEPENDENCIES = ["pytest", "pypiserver[passlib]", 'setuptools; python_version>="3.12"', "pytest-cov"] +TEST_DEPENDENCIES = ["pytest", "pypiserver[passlib]", 'setuptools; python_version>="3.12"', "pytest-cov", "uv", "virtualenv"] # Packages whose dependencies need an intact system PATH to compile # pytest setup clears PATH. So pre-build some wheels to the pip cache. PREBUILD_PACKAGES = {"all": ["jupyter==1.0.0"], "macos": [], "unix": [], "win": []} diff --git a/src/pipx/commands/upgrade.py b/src/pipx/commands/upgrade.py index 0a06fb84ea..e17c02125f 100644 --- a/src/pipx/commands/upgrade.py +++ b/src/pipx/commands/upgrade.py @@ -108,6 +108,8 @@ def _upgrade_venv( venv_dir: Path, pip_args: List[str], verbose: bool, + backend: str, + installer: str, *, include_injected: bool, upgrading_all: bool, @@ -137,9 +139,8 @@ def _upgrade_venv( include_dependencies=False, preinstall_packages=None, python_flag_passed=python_flag_passed, - # What to deal with this? Should we allow cli option? - backend=DEFAULT_BACKEND, - installer=DEFAULT_INSTALLER, + backend=backend, + installer=installer, ) return 0 else: @@ -155,6 +156,9 @@ def _upgrade_venv( if python and not install: logger.info("Ignoring --python as not combined with --install") + + if (backend or installer) and not install: + logger.info("Ignoring --backend or --installer as not combined with --install") venv = Venv(venv_dir, verbose=verbose) venv.check_upgrade_shared_libs(pip_args=pip_args, verbose=verbose) @@ -204,6 +208,8 @@ def upgrade( pip_args: List[str], venv_args: List[str], verbose: bool, + backend: str, + installer: str, *, include_injected: bool, force: bool, @@ -217,6 +223,8 @@ def upgrade( venv_dir, pip_args, verbose, + backend, + installer, include_injected=include_injected, upgrading_all=False, force=force, @@ -254,6 +262,8 @@ def upgrade_all( venv_dir, venv.pipx_metadata.main_package.pip_args, verbose=verbose, + backend=venv.pipx_metadata.backend, + installer=venv.pipx_metadata.installer, include_injected=include_injected, upgrading_all=True, force=force, diff --git a/src/pipx/main.py b/src/pipx/main.py index 310c685411..34435e7994 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -343,6 +343,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar pip_args, venv_args, verbose, + args.backend, + args.installer, include_injected=args.include_injected, force=args.force, install=args.install, @@ -678,6 +680,7 @@ def _add_upgrade(subparsers, venv_completer: VenvCompleter, shared_parser: argpa help="Install package spec if missing", ) add_python_options(p) + add_backend_and_installer(p) def _add_upgrade_all(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None: diff --git a/src/pipx/venv.py b/src/pipx/venv.py index fa26665bdc..a5f00c447c 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -157,8 +157,8 @@ def uses_shared_libs(self) -> bool: if self._existing: pth_files = self.root.glob("**/" + PIPX_SHARED_PTH) return next(pth_files, None) is not None - elif self.backend == "uv": - # No need to use shared lib for uv + elif self.backend == "uv" and self.installer == "uv": + # No need to use shared lib for uv if both backend and installer are uv return False else: # always use shared libs when creating a new venv with venv and virtualenv @@ -191,16 +191,16 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared if not override_shared: cmd.append("--without-pip") elif self.backend == "uv": - cmd = [path_to_exec("uv"), "venv"] + cmd = [path_to_exec("uv"), "venv", "--python", self.python] elif self.backend == "virtualenv": - cmd = [path_to_exec("virtualenv")] + cmd = [path_to_exec("virtualenv"), "--python", self.python] if not override_shared: cmd.append("--no-pip") venv_process = run_subprocess(cmd + venv_args + [str(self.root)], run_dir=str(self.root)) subprocess_post_check(venv_process) - if self.backend != "uv": + if self.backend != "uv" or self.installer != "uv": shared_libs.create(verbose=self.verbose, pip_args=pip_args) - if not override_shared and self.backend != "uv": + if not override_shared and (self.backend != "uv" or self.installer != "uv"): pipx_pth = get_site_packages(self.python_path) / PIPX_SHARED_PTH # write path pointing to the shared libs site-packages directory # example pipx_pth location: @@ -505,10 +505,7 @@ def _run_installer( return self._run_uv(["pip"] + install_cmd, quiet=quiet) def _run_uv(self, cmd: List[str], quiet: bool = True) -> "CompletedProcess[str]": - if not (uv_path := path_to_exec("uv", installer=True)): - self._run_pip(cmd=cmd, quiet=quiet) - else: - cmd = [uv_path] + cmd + ["--python", str(self.python_path)] + cmd = [path_to_exec("uv", installer=True)] + cmd + ["--python", str(self.python_path)] if not self.verbose and quiet: cmd.append("-q") return run_subprocess(cmd, run_dir=str(self.root)) diff --git a/tests/conftest.py b/tests/conftest.py index ea1b561996..3bb5d4b7da 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -181,7 +181,7 @@ def pipx_session_shared_dir(tmp_path_factory): @pytest.fixture(scope="session") def utils_temp_dir(tmp_path_factory): tmp_path = tmp_path_factory.mktemp("session_utilstempdir") - utils = ["git"] + utils = ["git", "uv", "virtualenv"] for util in utils: at_path = shutil.which(util) assert at_path is not None diff --git a/tests/test_backend.py b/tests/test_backend.py new file mode 100644 index 0000000000..5d7996b805 --- /dev/null +++ b/tests/test_backend.py @@ -0,0 +1,96 @@ +import os + +from helpers import run_pipx_cli + +def test_custom_backend_venv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--backend", "venv", "black"]) + captured = capsys.readouterr() + assert "-m venv --without-pip" in caplog.text + assert "installed package" in captured.out + + +def test_custom_backend_virtualenv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--backend", "virtualenv", "nox"]) + captured = capsys.readouterr() + assert "virtualenv --python" in caplog.text + assert "installed package" in captured.out + + +def test_custom_backend_uv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--backend", "uv", "pylint"]) + captured = capsys.readouterr() + assert "uv venv" in caplog.text + assert "installed package" in captured.out + + +def test_custom_installer_pip(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--installer", "pip", "pycowsay"]) + captured = capsys.readouterr() + assert "pip --no-input" in caplog.text + assert "installed package" in captured.out + + +def test_custom_installer_uv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--installer", "uv", "sphinx"]) + captured = capsys.readouterr() + assert "uv pip install" in caplog.text + assert "installed package" in captured.out + + +def test_custom_installer_backend_uv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "uv", "black"]) + captured = capsys.readouterr() + assert "uv venv" in caplog.text + assert "uv pip install" in caplog.text + assert "installed package" in captured.out + + +def test_custom_installer_uv_backend_venv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "venv", "nox"]) + captured = capsys.readouterr() + assert "-m venv --without-pip" in caplog.text + assert "uv pip install" in caplog.text + assert "installed package" in captured.out + + +def test_custom_installer_uv_backend_virtualenv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "virtualenv", "pylint"]) + captured = capsys.readouterr() + assert "virtualenv --python" in caplog.text + assert "uv pip install" in caplog.text + assert "installed package" in captured.out + + +def test_custom_installer_pip_backend_uv(pipx_temp_env, capsys, caplog): + assert not run_pipx_cli(["install", "--installer", "pip", "--backend", "uv", "nox"]) + captured = capsys.readouterr() + assert "uv venv" in caplog.text + assert "-m pip" in caplog.text + assert "installed package" in captured.out + + +def test_derive_installer_backend_from_env_variable(monkeypatch, pipx_temp_env, capsys, caplog): + monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "uv") + monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "virtualenv") + assert not run_pipx_cli(["install", "black"]) + captured = capsys.readouterr() + assert "virtualenv" in caplog.text + assert "uv pip" in caplog.text + assert "installed package" in captured.out + + +def test_fallback_to_default(monkeypatch, pipx_temp_env, capsys, caplog): + monkeypatch.setenv("PATH", os.getenv("PATH_TEST")) + monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "uv") + monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "virtualenv") + assert not run_pipx_cli(["install", "black"]) + captured = capsys.readouterr() + assert "'uv' not found on PATH" in caplog.text + assert "'virtualenv' not found on PATH" in caplog.text + assert "-m venv" in caplog.text + assert "-m pip" in caplog.text + assert "installed package" in captured.out + + monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "") + monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "") + monkeypatch.setenv("PATH", os.getenv("PATH")) \ No newline at end of file From 460e707d2fb10035f85fa19c4decea266b0755e4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:39:37 +0000 Subject: [PATCH 13/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- noxfile.py | 9 ++++++++- src/pipx/commands/upgrade.py | 4 ++-- tests/test_backend.py | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index d55177d454..56111d4c4c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,7 +17,14 @@ "markdown-gfm-admonition", ] MAN_DEPENDENCIES = ["argparse-manpage[setuptools]"] -TEST_DEPENDENCIES = ["pytest", "pypiserver[passlib]", 'setuptools; python_version>="3.12"', "pytest-cov", "uv", "virtualenv"] +TEST_DEPENDENCIES = [ + "pytest", + "pypiserver[passlib]", + 'setuptools; python_version>="3.12"', + "pytest-cov", + "uv", + "virtualenv", +] # Packages whose dependencies need an intact system PATH to compile # pytest setup clears PATH. So pre-build some wheels to the pip cache. PREBUILD_PACKAGES = {"all": ["jupyter==1.0.0"], "macos": [], "unix": [], "win": []} diff --git a/src/pipx/commands/upgrade.py b/src/pipx/commands/upgrade.py index e17c02125f..bbc5490dcf 100644 --- a/src/pipx/commands/upgrade.py +++ b/src/pipx/commands/upgrade.py @@ -7,7 +7,7 @@ from pipx import commands, paths from pipx.colors import bold, red from pipx.commands.common import expose_resources_globally -from pipx.constants import DEFAULT_BACKEND, DEFAULT_INSTALLER, EXIT_CODE_OK, ExitCode +from pipx.constants import EXIT_CODE_OK, ExitCode from pipx.emojis import sleep from pipx.package_specifier import parse_specifier_for_upgrade from pipx.util import PipxError, pipx_wrap @@ -156,7 +156,7 @@ def _upgrade_venv( if python and not install: logger.info("Ignoring --python as not combined with --install") - + if (backend or installer) and not install: logger.info("Ignoring --backend or --installer as not combined with --install") diff --git a/tests/test_backend.py b/tests/test_backend.py index 5d7996b805..ec848da6b6 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -2,6 +2,7 @@ from helpers import run_pipx_cli + def test_custom_backend_venv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--backend", "venv", "black"]) captured = capsys.readouterr() @@ -93,4 +94,4 @@ def test_fallback_to_default(monkeypatch, pipx_temp_env, capsys, caplog): monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "") monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "") - monkeypatch.setenv("PATH", os.getenv("PATH")) \ No newline at end of file + monkeypatch.setenv("PATH", os.getenv("PATH")) From 684570e997b174084c32a28fa4d58815b4136277 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Fri, 23 Aug 2024 08:44:39 +0000 Subject: [PATCH 14/25] Update tests --- src/pipx/backend.py | 3 +++ src/pipx/constants.py | 2 -- src/pipx/main.py | 4 +--- src/pipx/venv.py | 4 ++-- tests/conftest.py | 6 ++++++ tests/test_backend.py | 18 +----------------- 6 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/pipx/backend.py b/src/pipx/backend.py index 4a19f757d7..66d7502c46 100644 --- a/src/pipx/backend.py +++ b/src/pipx/backend.py @@ -1,6 +1,9 @@ import logging +import os import shutil +DEFAULT_BACKEND = os.getenv("PIPX_DEFAULT_BACKEND", "venv") +DEFAULT_INSTALLER = os.getenv("PIPX_DEFAULT_INSTALLER", "pip") SUPPORTED_VENV_BACKENDS = ("uv", "venv", "virtualenv") SUPPORTED_INSTALLERS = ("uv", "pip") diff --git a/src/pipx/constants.py b/src/pipx/constants.py index f56f594857..eec4f98dc8 100644 --- a/src/pipx/constants.py +++ b/src/pipx/constants.py @@ -9,8 +9,6 @@ MINIMUM_PYTHON_VERSION = "3.8" MAN_SECTIONS = ["man%d" % i for i in range(1, 10)] FETCH_MISSING_PYTHON = os.environ.get("PIPX_FETCH_MISSING_PYTHON", False) -DEFAULT_BACKEND = os.environ.get("PIPX_DEFAULT_BACKEND", "venv") -DEFAULT_INSTALLER = os.environ.get("PIPX_DEFAULT_INSTALLER", "pip") ExitCode = NewType("ExitCode", int) diff --git a/src/pipx/main.py b/src/pipx/main.py index 34435e7994..e17a5f5679 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -21,12 +21,10 @@ from pipx import commands, constants, paths from pipx.animate import hide_cursor, show_cursor -from pipx.backend import SUPPORTED_INSTALLERS, SUPPORTED_VENV_BACKENDS +from pipx.backend import DEFAULT_BACKEND, DEFAULT_INSTALLER, SUPPORTED_INSTALLERS, SUPPORTED_VENV_BACKENDS from pipx.colors import bold, green from pipx.commands.environment import ENVIRONMENT_VARIABLES from pipx.constants import ( - DEFAULT_BACKEND, - DEFAULT_INSTALLER, EXIT_CODE_OK, EXIT_CODE_SPECIFIED_PYTHON_EXECUTABLE_NOT_FOUND, MINIMUM_PYTHON_VERSION, diff --git a/src/pipx/venv.py b/src/pipx/venv.py index a5f00c447c..49c4e64d86 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -17,8 +17,8 @@ from packaging.utils import canonicalize_name from pipx.animate import animate -from pipx.backend import path_to_exec -from pipx.constants import DEFAULT_BACKEND, DEFAULT_INSTALLER, PIPX_SHARED_PTH, ExitCode +from pipx.backend import DEFAULT_BACKEND, DEFAULT_INSTALLER, path_to_exec +from pipx.constants import PIPX_SHARED_PTH, ExitCode from pipx.emojis import hazard from pipx.interpreter import DEFAULT_PYTHON from pipx.package_specifier import ( diff --git a/tests/conftest.py b/tests/conftest.py index 3bb5d4b7da..207b359937 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -93,6 +93,12 @@ def pipx_temp_env_helper(pipx_shared_dir, tmp_path, monkeypatch, request, utils_ if "PIPX_DEFAULT_PYTHON" in os.environ: monkeypatch.delenv("PIPX_DEFAULT_PYTHON") + if "PIPX_DEFAULT_BACKEND" in os.environ: + monkeypatch.delenv("PIPX_DEFAULT_BACKEND") + + if "PIPX_DEFAULT_INSTALLER" in os.environ: + monkeypatch.delenv("PIPX_DEFAULT_INSTALLER") + # macOS needs /usr/bin in PATH to compile certain packages, but # applications in /usr/bin cause test_install.py tests to raise warnings # which make tests fail (e.g. on Github ansible apps exist in /usr/bin) diff --git a/tests/test_backend.py b/tests/test_backend.py index ec848da6b6..5afbd1674c 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -70,28 +70,12 @@ def test_custom_installer_pip_backend_uv(pipx_temp_env, capsys, caplog): assert "installed package" in captured.out -def test_derive_installer_backend_from_env_variable(monkeypatch, pipx_temp_env, capsys, caplog): - monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "uv") - monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "virtualenv") - assert not run_pipx_cli(["install", "black"]) - captured = capsys.readouterr() - assert "virtualenv" in caplog.text - assert "uv pip" in caplog.text - assert "installed package" in captured.out - - def test_fallback_to_default(monkeypatch, pipx_temp_env, capsys, caplog): monkeypatch.setenv("PATH", os.getenv("PATH_TEST")) - monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "uv") - monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "virtualenv") - assert not run_pipx_cli(["install", "black"]) + assert not run_pipx_cli(["install", "black", "--backend", "virtualenv", "--installer", "uv"]) captured = capsys.readouterr() assert "'uv' not found on PATH" in caplog.text assert "'virtualenv' not found on PATH" in caplog.text assert "-m venv" in caplog.text assert "-m pip" in caplog.text assert "installed package" in captured.out - - monkeypatch.setenv("PIPX_DEFAULT_INSTALLER", "") - monkeypatch.setenv("PIPX_DEFAULT_BACKEND", "") - monkeypatch.setenv("PATH", os.getenv("PATH")) From 54d6ca93d09ce4fd7868f0ddb5037a6b00603c6c Mon Sep 17 00:00:00 2001 From: meowmeow Date: Sat, 24 Aug 2024 08:52:41 +0000 Subject: [PATCH 15/25] Update tests --- tests/test_backend.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 5afbd1674c..7ee18fbfce 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,6 +1,7 @@ import os from helpers import run_pipx_cli +from package_info import _exe_if_win def test_custom_backend_venv(pipx_temp_env, capsys, caplog): @@ -13,14 +14,14 @@ def test_custom_backend_venv(pipx_temp_env, capsys, caplog): def test_custom_backend_virtualenv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--backend", "virtualenv", "nox"]) captured = capsys.readouterr() - assert "virtualenv --python" in caplog.text + assert f"{_exe_if_win("virtualenv")} --python" in caplog.text assert "installed package" in captured.out def test_custom_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--backend", "uv", "pylint"]) captured = capsys.readouterr() - assert "uv venv" in caplog.text + assert f"{_exe_if_win("uv")} venv" in caplog.text assert "installed package" in captured.out @@ -34,15 +35,15 @@ def test_custom_installer_pip(pipx_temp_env, capsys, caplog): def test_custom_installer_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "sphinx"]) captured = capsys.readouterr() - assert "uv pip install" in caplog.text + assert f"{_exe_if_win("uv")} pip install" in caplog.text assert "installed package" in captured.out def test_custom_installer_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "uv", "black"]) captured = capsys.readouterr() - assert "uv venv" in caplog.text - assert "uv pip install" in caplog.text + assert f"{_exe_if_win("uv")} venv" in caplog.text + assert f"{_exe_if_win("uv")} pip install" in caplog.text assert "installed package" in captured.out @@ -50,22 +51,22 @@ def test_custom_installer_uv_backend_venv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "venv", "nox"]) captured = capsys.readouterr() assert "-m venv --without-pip" in caplog.text - assert "uv pip install" in caplog.text + assert f"{_exe_if_win("uv")} pip install" in caplog.text assert "installed package" in captured.out def test_custom_installer_uv_backend_virtualenv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "virtualenv", "pylint"]) captured = capsys.readouterr() - assert "virtualenv --python" in caplog.text - assert "uv pip install" in caplog.text + assert f"{_exe_if_win("virtualenv")} --python" in caplog.text + assert f"{_exe_if_win("uv")} pip install" in caplog.text assert "installed package" in captured.out def test_custom_installer_pip_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "pip", "--backend", "uv", "nox"]) captured = capsys.readouterr() - assert "uv venv" in caplog.text + assert f"{_exe_if_win("uv")} venv" in caplog.text assert "-m pip" in caplog.text assert "installed package" in captured.out From 18d060d6d57e76d7f0af5d224fb67b22eeae6b09 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Sat, 24 Aug 2024 10:57:55 +0000 Subject: [PATCH 16/25] Add docs --- docs/backend.md | 34 ++++++++++++++++++++++++++++++++++ docs/examples.md | 2 ++ mkdocs.yml | 1 + tests/test_backend.py | 21 ++++++++++----------- 4 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 docs/backend.md diff --git a/docs/backend.md b/docs/backend.md new file mode 100644 index 0000000000..2f0667e96f --- /dev/null +++ b/docs/backend.md @@ -0,0 +1,34 @@ +Starting from version 1.8.0, pipx supports different backends and installers. + +### Backends supported: +- `venv` (Default) +- `uv` (via `uv venv`) +- `virtualenv` + +### Installers supported: +- `pip` (Default) +- `uv` (via `uv pip`) + +If you wish to use a different backend or installer, you can either: + +- Pass command line arguments (`--backend`, `--installer`) +- Set envirionment variables (`PIPX_DEFAULT_BACKEND`, `PIPX_DEFAULT_INSTALLER`) + +> [!NOTE] +> Command line arguments always have higher precedence than environment variables. + +### Examples +```bash +# Use uv as backend and installer +pipx install --backend uv --installer uv black + +# Use virtualenv as backend and uv as installer +pipx install --backend virtualenv --installer uv black +``` + +Use environment variables to set backend and installer: +```bash +export PIPX_DEFAULT_BACKEND=uv +export PIPX_DEFAULT_INSTALLER=uv +pipx install black +``` \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index 2c79c54830..7dfd63f2a3 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -21,6 +21,7 @@ pipx install --index-url https://test.pypi.org/simple/ --pip-args='--extra-index pipx --global install pycowsay pipx install . pipx install path/to/some-project +pipx install --backend uv --installer uv black ``` ## `pipx run` examples @@ -41,6 +42,7 @@ pipx run pycowsay --version # prints pycowsay version pipx run --python pythonX pycowsay pipx run pycowsay==2.0 --version pipx run pycowsay[dev] --version +pipx run --backend uv --installer uv pycowsay pipx run --spec git+https://github.com/psf/black.git black pipx run --spec git+https://github.com/psf/black.git@branch-name black pipx run --spec git+https://github.com/psf/black.git@git-hash black diff --git a/mkdocs.yml b/mkdocs.yml index 3bc9212a67..35c575aef8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,6 +31,7 @@ nav: - Getting Started: "getting-started.md" - Docs: "docs.md" - Troubleshooting: "troubleshooting.md" + - Backend: "backend.md" - Examples: "examples.md" - Comparison to Other Tools: "comparisons.md" - How pipx works: "how-pipx-works.md" diff --git a/tests/test_backend.py b/tests/test_backend.py index 7ee18fbfce..21d1cb1deb 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,7 +1,6 @@ import os -from helpers import run_pipx_cli -from package_info import _exe_if_win +from helpers import run_pipx_cli, WIN def test_custom_backend_venv(pipx_temp_env, capsys, caplog): @@ -14,14 +13,14 @@ def test_custom_backend_venv(pipx_temp_env, capsys, caplog): def test_custom_backend_virtualenv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--backend", "virtualenv", "nox"]) captured = capsys.readouterr() - assert f"{_exe_if_win("virtualenv")} --python" in caplog.text + assert "virtualenv" in caplog.text assert "installed package" in captured.out def test_custom_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--backend", "uv", "pylint"]) captured = capsys.readouterr() - assert f"{_exe_if_win("uv")} venv" in caplog.text + assert "uv" in caplog.text assert "installed package" in captured.out @@ -35,15 +34,15 @@ def test_custom_installer_pip(pipx_temp_env, capsys, caplog): def test_custom_installer_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "sphinx"]) captured = capsys.readouterr() - assert f"{_exe_if_win("uv")} pip install" in caplog.text + assert "uv" in caplog.text assert "installed package" in captured.out def test_custom_installer_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "uv", "black"]) captured = capsys.readouterr() - assert f"{_exe_if_win("uv")} venv" in caplog.text - assert f"{_exe_if_win("uv")} pip install" in caplog.text + assert f"{'uv.EXE' if WIN else 'uv'} venv" in caplog.text + assert "uv pip install" in caplog.text assert "installed package" in captured.out @@ -51,22 +50,22 @@ def test_custom_installer_uv_backend_venv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "venv", "nox"]) captured = capsys.readouterr() assert "-m venv --without-pip" in caplog.text - assert f"{_exe_if_win("uv")} pip install" in caplog.text + assert f"{'uv.EXE' if WIN else 'uv'} pip install" in caplog.text assert "installed package" in captured.out def test_custom_installer_uv_backend_virtualenv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "virtualenv", "pylint"]) captured = capsys.readouterr() - assert f"{_exe_if_win("virtualenv")} --python" in caplog.text - assert f"{_exe_if_win("uv")} pip install" in caplog.text + assert "virtualenv" in caplog.text + assert f"{'uv.EXE' if WIN else 'uv'} pip install" in caplog.text assert "installed package" in captured.out def test_custom_installer_pip_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "pip", "--backend", "uv", "nox"]) captured = capsys.readouterr() - assert f"{_exe_if_win("uv")} venv" in caplog.text + assert f"{'uv.EXE' if WIN else 'uv'} venv" in caplog.text assert "-m pip" in caplog.text assert "installed package" in captured.out From 809074e53188bbdc6aaab974cfaf0462ca61064f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:58:11 +0000 Subject: [PATCH 17/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/backend.md | 2 +- tests/test_backend.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/backend.md b/docs/backend.md index 2f0667e96f..adf9d22ead 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -31,4 +31,4 @@ Use environment variables to set backend and installer: export PIPX_DEFAULT_BACKEND=uv export PIPX_DEFAULT_INSTALLER=uv pipx install black -``` \ No newline at end of file +``` diff --git a/tests/test_backend.py b/tests/test_backend.py index 21d1cb1deb..501f7e7baa 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -1,6 +1,6 @@ import os -from helpers import run_pipx_cli, WIN +from helpers import WIN, run_pipx_cli def test_custom_backend_venv(pipx_temp_env, capsys, caplog): From 9e52db0ecaaa0333c4b807089cc4040934849815 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Sat, 24 Aug 2024 11:45:43 +0000 Subject: [PATCH 18/25] Update tests --- tests/test_backend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_backend.py b/tests/test_backend.py index 501f7e7baa..1b8c055765 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -42,7 +42,7 @@ def test_custom_installer_backend_uv(pipx_temp_env, capsys, caplog): assert not run_pipx_cli(["install", "--installer", "uv", "--backend", "uv", "black"]) captured = capsys.readouterr() assert f"{'uv.EXE' if WIN else 'uv'} venv" in caplog.text - assert "uv pip install" in caplog.text + assert f"{'uv.EXE' if WIN else 'uv'} pip install" in caplog.text assert "installed package" in captured.out From 0795cee04741964eef1e95aef047bc537323a97c Mon Sep 17 00:00:00 2001 From: meowmeow Date: Sat, 24 Aug 2024 11:49:07 +0000 Subject: [PATCH 19/25] Add changelog --- changelog.d/1392.feature.md | 1 + docs/backend.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/1392.feature.md diff --git a/changelog.d/1392.feature.md b/changelog.d/1392.feature.md new file mode 100644 index 0000000000..f329e531dc --- /dev/null +++ b/changelog.d/1392.feature.md @@ -0,0 +1 @@ +Support different backends (uv, virtualenv, venv) and installers (uv, pip) \ No newline at end of file diff --git a/docs/backend.md b/docs/backend.md index adf9d22ead..af1c93f81a 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -12,7 +12,7 @@ Starting from version 1.8.0, pipx supports different backends and installers. If you wish to use a different backend or installer, you can either: - Pass command line arguments (`--backend`, `--installer`) -- Set envirionment variables (`PIPX_DEFAULT_BACKEND`, `PIPX_DEFAULT_INSTALLER`) +- Set environment variables (`PIPX_DEFAULT_BACKEND`, `PIPX_DEFAULT_INSTALLER`) > [!NOTE] > Command line arguments always have higher precedence than environment variables. From 21c7de0eda90d4fb97470d2f49dad3bf38e81b8d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:49:21 +0000 Subject: [PATCH 20/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- changelog.d/1392.feature.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/1392.feature.md b/changelog.d/1392.feature.md index f329e531dc..53080ae933 100644 --- a/changelog.d/1392.feature.md +++ b/changelog.d/1392.feature.md @@ -1 +1 @@ -Support different backends (uv, virtualenv, venv) and installers (uv, pip) \ No newline at end of file +Support different backends (uv, virtualenv, venv) and installers (uv, pip) From bca02e2f6394caab6d69b4bc551eed80cffbea83 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Thu, 26 Sep 2024 13:32:01 +0000 Subject: [PATCH 21/25] Remove optional dependencies --- pyproject.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 00a870ab8d..b78b5c2991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,12 +43,6 @@ dependencies = [ "tomli; python_version<'3.11'", "userpath!=1.9,>=1.6", ] -optional-dependencies.uv = [ - "uv>=0.2.27", -] -optional-dependencies.virtualenv = [ - "virtualenv>=20.26.3", -] urls."Bug Tracker" = "https://github.com/pypa/pipx/issues" urls.Documentation = "https://pipx.pypa.io" urls.Homepage = "https://pipx.pypa.io" From c334237b814f0fbb68a8ed4baf74f318949eaa71 Mon Sep 17 00:00:00 2001 From: meowmeow Date: Thu, 26 Sep 2024 13:33:31 +0000 Subject: [PATCH 22/25] Rename `installer` to `is_installer` --- src/pipx/backend.py | 4 ++-- src/pipx/venv.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pipx/backend.py b/src/pipx/backend.py index 66d7502c46..683fdb085c 100644 --- a/src/pipx/backend.py +++ b/src/pipx/backend.py @@ -10,13 +10,13 @@ logger = logging.getLogger(__name__) -def path_to_exec(executable: str, installer: bool = False) -> str: +def path_to_exec(executable: str, is_installer: bool = False) -> str: if executable in ("venv", "pip"): return executable path = shutil.which(executable) if path: return path - elif installer: + elif is_installer: logger.warning(f"'{executable}' not found on PATH. Falling back to 'pip'.") return "" else: diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 49c4e64d86..24ff2ca936 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -112,7 +112,7 @@ def __init__( if not path_to_exec(self.backend): self.backend = "venv" - if not path_to_exec(self.installer, installer=True): + if not path_to_exec(self.installer, is_installer=True): self.installer = "pip" def check_upgrade_shared_libs(self, verbose: bool, pip_args: List[str], force_upgrade: bool = False): @@ -505,7 +505,7 @@ def _run_installer( return self._run_uv(["pip"] + install_cmd, quiet=quiet) def _run_uv(self, cmd: List[str], quiet: bool = True) -> "CompletedProcess[str]": - cmd = [path_to_exec("uv", installer=True)] + cmd + ["--python", str(self.python_path)] + cmd = [path_to_exec("uv", is_installer=True)] + cmd + ["--python", str(self.python_path)] if not self.verbose and quiet: cmd.append("-q") return run_subprocess(cmd, run_dir=str(self.root)) From 638480114a4f8c85d4a257bad7ec3b99f11f100c Mon Sep 17 00:00:00 2001 From: meowmeow Date: Thu, 26 Sep 2024 13:37:42 +0000 Subject: [PATCH 23/25] Change `--no-pip` to `--without-pip` --- src/pipx/venv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 24ff2ca936..a157181081 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -195,7 +195,7 @@ def create_venv(self, venv_args: List[str], pip_args: List[str], override_shared elif self.backend == "virtualenv": cmd = [path_to_exec("virtualenv"), "--python", self.python] if not override_shared: - cmd.append("--no-pip") + cmd.append("--without-pip") venv_process = run_subprocess(cmd + venv_args + [str(self.root)], run_dir=str(self.root)) subprocess_post_check(venv_process) if self.backend != "uv" or self.installer != "uv": From 42ce54d02012231a963e13ca0a307a73e18b66d0 Mon Sep 17 00:00:00 2001 From: Jason Lam Date: Thu, 26 Sep 2024 21:43:07 +0800 Subject: [PATCH 24/25] Add install backends/installer instructions --- docs/backend.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/backend.md b/docs/backend.md index af1c93f81a..a72eafa9d2 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -9,6 +9,9 @@ Starting from version 1.8.0, pipx supports different backends and installers. - `pip` (Default) - `uv` (via `uv pip`) +> [!NOTE] +> If `uv` or `virtualenv` is not present in PATH, you should install them with `pipx install uv` or `pipx install virtualenv` in advance. + If you wish to use a different backend or installer, you can either: - Pass command line arguments (`--backend`, `--installer`) From 22a99a790c12d7149485e721754d4e0d0a9e9c14 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 04:29:37 +0000 Subject: [PATCH 25/25] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/pipx/venv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pipx/venv.py b/src/pipx/venv.py index a157181081..84df5a0376 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -367,7 +367,7 @@ def get_venv_metadata_for_package(self, package_name: str, package_extras: Set[s venv_metadata = inspect_venv( package_name, package_extras, self.bin_path, self.python_path, self.man_path, self.backend, self.installer ) - logger.info(f"get_venv_metadata_for_package: {1e3*(time.time()-data_start):.0f}ms") + logger.info(f"get_venv_metadata_for_package: {1e3 * (time.time() - data_start):.0f}ms") return venv_metadata def update_package_metadata(