diff --git a/src/pipx/commands/inject.py b/src/pipx/commands/inject.py index 4f028a3e4..9e50b7e1c 100644 --- a/src/pipx/commands/inject.py +++ b/src/pipx/commands/inject.py @@ -29,6 +29,8 @@ def inject_dep( include_apps: bool, include_dependencies: bool, force: bool, + upgrade: bool, + upgrade_strategy: Optional[str], suffix: bool = False, ) -> bool: logger.debug("Injecting package %s", package_spec) @@ -65,7 +67,11 @@ def inject_dep( verbose=verbose, ) - if not force and venv.has_package(package_name): + if upgrade: + pip_args = ["--upgrade"] + pip_args + if upgrade_strategy is not None: + pip_args += ["--upgrade-strategy", upgrade_strategy] + elif not force and venv.has_package(package_name): logger.info("Package %s has already been injected", package_name) print( pipx_wrap( @@ -119,6 +125,8 @@ def inject( include_apps: bool, include_dependencies: bool, force: bool, + upgrade: bool, + upgrade_strategy: Optional[str], suffix: bool = False, ) -> ExitCode: """Returns pipx exit code.""" @@ -148,6 +156,8 @@ def inject( include_apps=include_apps, include_dependencies=include_dependencies, force=force, + upgrade=upgrade, + upgrade_strategy=upgrade_strategy, suffix=suffix, ) diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index 7870b8a12..1c8913f27 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -30,6 +30,8 @@ def install( verbose: bool, *, force: bool, + upgrade: bool, + upgrade_strategy: Optional[str], reinstall: bool, include_dependencies: bool, preinstall_packages: Optional[list[str]], @@ -61,7 +63,11 @@ def install( venv = Venv(venv_dir, python=python, verbose=verbose) venv.check_upgrade_shared_libs(pip_args=pip_args, verbose=verbose) - if exists: + if upgrade: + pip_args = ["--upgrade"] + pip_args + if upgrade_strategy is not None: + pip_args += ["--upgrade-strategy", upgrade_strategy] + elif exists: if not reinstall and force and python_flag_passed: print( pipx_wrap( @@ -104,6 +110,8 @@ def install( include_dependencies=include_dependencies, include_apps=True, is_main_package=True, + upgrade=upgrade, + upgrade_strategy=upgrade_strategy, suffix=suffix, ) run_post_install_actions( @@ -214,6 +222,8 @@ def install_all( reinstall=False, include_dependencies=main_package.include_dependencies, preinstall_packages=[], + upgrade=False, + upgrade_strategy=None, suffix=main_package.suffix, ) @@ -228,6 +238,8 @@ def install_all( include_apps=inject_package.include_apps, include_dependencies=inject_package.include_dependencies, force=force, + upgrade=False, + upgrade_strategy=None, suffix=inject_package.suffix == main_package.suffix, ) except PipxError as e: diff --git a/src/pipx/commands/reinstall.py b/src/pipx/commands/reinstall.py index f051d7ee0..a8dd66124 100644 --- a/src/pipx/commands/reinstall.py +++ b/src/pipx/commands/reinstall.py @@ -95,6 +95,8 @@ def reinstall( verbose=verbose, include_apps=injected_package.include_apps, include_dependencies=injected_package.include_dependencies, + upgrade=False, + upgrade_strategy=None, force=True, ) diff --git a/src/pipx/commands/run.py b/src/pipx/commands/run.py index 62a67d311..35c4b3fb7 100644 --- a/src/pipx/commands/run.py +++ b/src/pipx/commands/run.py @@ -266,6 +266,8 @@ def _prepare_venv( pip_args=pip_args, include_dependencies=False, include_apps=True, + upgrade=False, + upgrade_strategy=None, is_main_package=True, ) diff --git a/src/pipx/commands/upgrade.py b/src/pipx/commands/upgrade.py index 9193de24e..e7f52d796 100644 --- a/src/pipx/commands/upgrade.py +++ b/src/pipx/commands/upgrade.py @@ -23,6 +23,7 @@ def _upgrade_package( pip_args: list[str], is_main_package: bool, force: bool, + upgrade_strategy: Optional[str], upgrading_all: bool, ) -> int: """Returns 1 if package version changed, 0 if same version""" @@ -50,6 +51,7 @@ def _upgrade_package( include_dependencies=package_metadata.include_dependencies, include_apps=package_metadata.include_apps, is_main_package=is_main_package, + upgrade_strategy=upgrade_strategy, suffix=package_metadata.suffix, ) @@ -112,6 +114,7 @@ def _upgrade_venv( *, include_injected: bool, upgrading_all: bool, + upgrade_strategy: Optional[str], force: bool, install: bool = False, venv_args: Optional[list[str]] = None, @@ -178,6 +181,7 @@ def _upgrade_venv( is_main_package=True, force=force, upgrading_all=upgrading_all, + upgrade_strategy=upgrade_strategy, ) if include_injected: @@ -191,6 +195,7 @@ def _upgrade_venv( is_main_package=False, force=force, upgrading_all=upgrading_all, + upgrade_strategy=upgrade_strategy, ) return versions_updated @@ -206,6 +211,7 @@ def upgrade( include_injected: bool, force: bool, install: bool, + upgrade_strategy: Optional[str], python_flag_passed: bool = False, ) -> ExitCode: """Return pipx exit code.""" @@ -219,6 +225,7 @@ def upgrade( upgrading_all=False, force=force, install=install, + upgrade_strategy=upgrade_strategy, venv_args=venv_args, python=python, python_flag_passed=python_flag_passed, @@ -237,6 +244,7 @@ def upgrade_all( skip: Sequence[str], force: bool, python_flag_passed: bool = False, + upgrade_strategy: Optional[str], ) -> ExitCode: """Return pipx exit code.""" failed: list[str] = [] @@ -254,6 +262,7 @@ def upgrade_all( verbose=verbose, include_injected=include_injected, upgrading_all=True, + upgrade_strategy=upgrade_strategy, force=force, python_flag_passed=python_flag_passed, ) diff --git a/src/pipx/main.py b/src/pipx/main.py index 7548b71d3..0c6df2416 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -304,6 +304,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: dict[str, argparse.Ar venv_args, verbose, force=args.force, + upgrade=args.upgrade, + upgrade_strategy=args.upgrade_strategy, reinstall=False, include_dependencies=args.include_deps, preinstall_packages=args.preinstall, @@ -331,6 +333,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: dict[str, argparse.Ar include_apps=args.include_apps, include_dependencies=args.include_deps, force=args.force, + upgrade=args.upgrade, + upgrade_strategy=args.upgrade_strategy, suffix=args.with_suffix, ) elif args.command == "uninject": @@ -353,6 +357,7 @@ def run_pipx_command(args: argparse.Namespace, subparsers: dict[str, argparse.Ar force=args.force, install=args.install, python_flag_passed=python_flag_passed, + upgrade_strategy=args.upgrade_strategy, ) elif args.command == "upgrade-all": return commands.upgrade_all( @@ -363,6 +368,7 @@ def run_pipx_command(args: argparse.Namespace, subparsers: dict[str, argparse.Ar force=args.force, pip_args=pip_args, python_flag_passed=python_flag_passed, + upgrade_strategy=args.upgrade_strategy, ) elif args.command == "upgrade-shared": return commands.upgrade_shared( @@ -515,6 +521,22 @@ def _add_install(subparsers: argparse._SubParsersAction, shared_parser: argparse "installing the main package. Use this flag multiple times if you want to preinstall multiple packages." ), ) + p.add_argument( + "--upgrade", + action="store_true", + help="Upgrade packages if already installed with `pip install --upgrade`", + ) + p.add_argument( + "--upgrade-strategy", + nargs=1, + choices=["eager", "only-if-needed"], + help=( + "Determines how dependency upgrading is handled. " + '"eager" upgrades all dependencies regardless of whether version requirements are satisfied by the main package. ' + '"only-if-needed" upgrades dependencies only when they do not satisfy requirements. ' + 'Default "only-if-needed" if --upgrade is provided. Ignored if --upgrade is not specified.' + ), + ) add_pip_venv_args(p) @@ -583,6 +605,22 @@ def _add_inject(subparsers, venv_completer: VenvCompleter, shared_parser: argpar action="store_true", help="Modify existing virtual environment and files in PIPX_BIN_DIR and PIPX_MAN_DIR", ) + p.add_argument( + "--upgrade", + action="store_true", + help="Upgrade packages if already installed with `pip install --upgrade`", + ) + p.add_argument( + "--upgrade-strategy", + nargs=1, + choices=["eager", "only-if-needed"], + help=( + "Determines how dependency upgrading is handled. " + '"eager" upgrades all dependencies regardless of whether version requirements are satisfied by the main package. ' + '"only-if-needed" upgrades dependencies only when they do not satisfy requirements. ' + 'Default "only-if-needed" if --upgrade is provided. Ignored if --upgrade is not specified.' + ), + ) p.add_argument( "--with-suffix", action="store_true", @@ -672,6 +710,17 @@ def _add_upgrade(subparsers, venv_completer: VenvCompleter, shared_parser: argpa action="store_true", help="Install package spec if missing", ) + p.add_argument( + "--upgrade-strategy", + nargs=1, + choices=["eager", "only-if-needed"], + help=( + "Determines how dependency upgrading is handled. " + '"eager" upgrades all dependencies regardless of whether version requirements are satisfied by the main package. ' + '"only-if-needed" upgrades dependencies only when they do not satisfy requirements. ' + 'Default "only-if-needed" if --upgrade is provided. Ignored if --upgrade is not specified.' + ), + ) add_python_options(p) @@ -694,6 +743,17 @@ def _add_upgrade_all(subparsers: argparse._SubParsersAction, shared_parser: argp action="store_true", help="Modify existing virtual environment and files in PIPX_BIN_DIR and PIPX_MAN_DIR", ) + p.add_argument( + "--upgrade-strategy", + nargs=1, + choices=["eager", "only-if-needed"], + help=( + "Determines how dependency upgrading is handled. " + '"eager" upgrades all dependencies regardless of whether version requirements are satisfied by the main package. ' + '"only-if-needed" upgrades dependencies only when they do not satisfy requirements. ' + 'Default "only-if-needed" if --upgrade is provided. Ignored if --upgrade is not specified.' + ), + ) def _add_upgrade_shared(subparsers: argparse._SubParsersAction, shared_parser: argparse.ArgumentParser) -> None: diff --git a/src/pipx/venv.py b/src/pipx/venv.py index 1c32e0306..2625b9ab2 100644 --- a/src/pipx/venv.py +++ b/src/pipx/venv.py @@ -453,9 +453,12 @@ def upgrade_package( include_dependencies: bool, include_apps: bool, is_main_package: bool, + upgrade_strategy: Optional[str], suffix: str = "", ) -> None: logger.info("Upgrading %s", package_descr := full_package_description(package_name, package_or_url)) + if upgrade_strategy is not None: + pip_args += ["--upgrade-strategy", upgrade_strategy] with animate(f"upgrading {package_descr}", self.do_animation): pip_process = self._run_pip(["--no-input", "install", "--upgrade"] + pip_args + [package_or_url]) subprocess_post_check(pip_process)