diff --git a/CHANGELOG.md b/CHANGELOG.md index 1288033a40..e26252f048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## dev +- `pipx install`: emit a warning when `--force` and `--python` were passed at the same time - Drop support for Python 3.7 - Make usage message in `pipx run` show `package_or_url`, so extra will be printed out as well - Add `--force-reinstall` to pip arguments when `--force` was passed diff --git a/src/pipx/commands/install.py b/src/pipx/commands/install.py index 8d6ae2b94a..218bf7d39e 100644 --- a/src/pipx/commands/install.py +++ b/src/pipx/commands/install.py @@ -4,6 +4,7 @@ from pipx import constants from pipx.commands.common import package_name_from_spec, run_post_install_actions from pipx.constants import EXIT_CODE_INSTALL_VENV_EXISTS, EXIT_CODE_OK, ExitCode +from pipx.interpreter import DEFAULT_PYTHON from pipx.util import pipx_wrap from pipx.venv import Venv, VenvContainer @@ -13,12 +14,13 @@ def install( package_name: Optional[str], package_spec: str, local_bin_dir: Path, - python: str, + python: Optional[str], pip_args: List[str], venv_args: List[str], verbose: bool, *, force: bool, + reinstall: bool, include_dependencies: bool, preinstall_packages: Optional[List[str]], suffix: str = "", @@ -26,6 +28,9 @@ def install( """Returns pipx exit code.""" # package_spec is anything pip-installable, including package_name, vcs spec, # zip file, or tar.gz file. + python_flag_was_passed = python is not None + + python = python or DEFAULT_PYTHON if package_name is None: package_name = package_name_from_spec( @@ -42,6 +47,16 @@ def install( venv = Venv(venv_dir, python=python, verbose=verbose) if exists: + if not reinstall and force and python_flag_was_passed: + print( + pipx_wrap( + f""" + --python is ignored when --force is passed. + If you want to reinstall {package_name} with {python}, + run `pipx reinstall {package_spec} --python {python}` instead. + """ + ) + ) if force: print(f"Installing to existing venv {venv.name!r}") pip_args += ["--force-reinstall"] diff --git a/src/pipx/commands/reinstall.py b/src/pipx/commands/reinstall.py index b8ab23fc97..a15bb292f5 100644 --- a/src/pipx/commands/reinstall.py +++ b/src/pipx/commands/reinstall.py @@ -69,6 +69,7 @@ def reinstall( venv.pipx_metadata.venv_args, verbose, force=True, + reinstall=True, include_dependencies=venv.pipx_metadata.main_package.include_dependencies, preinstall_packages=[], suffix=venv.pipx_metadata.main_package.suffix, diff --git a/src/pipx/main.py b/src/pipx/main.py index bb5750d164..fb59a8db8a 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -212,6 +212,7 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901 venv_args, verbose, force=args.force, + reinstall=False, include_dependencies=args.include_deps, preinstall_packages=args.preinstall, suffix=args.suffix, @@ -344,7 +345,7 @@ def _add_install(subparsers: argparse._SubParsersAction) -> None: ) p.add_argument( "--python", - default=DEFAULT_PYTHON, + # Don't pass a default Python here so we know whether --python flag was passed help=( "Python to install with. Possible values can be the executable name (python3.11), " "the version to pass to py launcher (3.11), or the full path to the executable." diff --git a/tests/test_install.py b/tests/test_install.py index 75702de02d..87fa2a8d1c 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -284,3 +284,14 @@ def test_preinstall(pipx_temp_env, caplog): def test_do_not_wait_for_input(pipx_temp_env, pipx_session_shared_dir, monkeypatch): monkeypatch.setenv("PIP_INDEX_URL", "http://127.0.0.1:8080/simple") run_pipx_cli(["install", "pycowsay"]) + + +def test_passed_python_and_force_flag_warning(pipx_temp_env, capsys): + assert not run_pipx_cli(["install", "black"]) + assert not run_pipx_cli(["install", "--python", sys.executable, "--force", "black"]) + captured = capsys.readouterr() + assert "--python is ignored when --force is passed." in captured.out + + assert not run_pipx_cli(["install", "pycowsay", "--force"]) + captured = capsys.readouterr() + assert "--python is ignored when --force is passed." not in captured.out