diff --git a/changelog.d/1389.bugfix.md b/changelog.d/1389.bugfix.md new file mode 100644 index 0000000000..a290ff8370 --- /dev/null +++ b/changelog.d/1389.bugfix.md @@ -0,0 +1 @@ +Fix passing constraints file path into `pipx install` operation via `pip` args diff --git a/src/pipx/main.py b/src/pipx/main.py index 5f2d20c09c..79f55516f5 100644 --- a/src/pipx/main.py +++ b/src/pipx/main.py @@ -159,7 +159,9 @@ def get_pip_args(parsed_args: Dict[str, str]) -> List[str]: pip_args += ["--index-url", parsed_args["index_url"]] if parsed_args.get("pip_args"): - pip_args += shlex.split(parsed_args.get("pip_args", ""), posix=not WINDOWS) + # Stripping the single quote that can be parsed from several shells + pip_args_striped = parsed_args["pip_args"].strip("'") + pip_args += shlex.split(pip_args_striped, posix=not WINDOWS) # make sure --editable is last because it needs to be right before # package specification diff --git a/src/pipx/package_specifier.py b/src/pipx/package_specifier.py index 62fe053195..066a6b31fc 100644 --- a/src/pipx/package_specifier.py +++ b/src/pipx/package_specifier.py @@ -163,7 +163,27 @@ def parse_specifier_for_install(package_spec: str, pip_args: List[str]) -> Tuple ) pip_args.remove("--editable") - return (package_or_url, pip_args) + for index, option in enumerate(pip_args): + if not option.startswith(("-c", "--constraint")): + continue + + if option in ("-c", "--constraint"): + argument_index = index + 1 + if argument_index < len(pip_args): + constraints_file = pip_args[argument_index] + pip_args[argument_index] = str(Path(constraints_file).expanduser().resolve()) + + else: # option == "--constraint=some_path" + option_list = option.split("=") + + if len(option_list) == 2: + key, value = option_list + value_path = Path(value).expanduser().resolve() + pip_args[index] = f"{key}={value_path}" + + break + + return package_or_url, pip_args def parse_specifier_for_metadata(package_spec: str) -> str: diff --git a/tests/test_install.py b/tests/test_install.py index a4fc3b29f7..b83310e988 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -264,6 +264,39 @@ def test_pip_args_with_windows_path(pipx_temp_env, capsys): assert r"D:\\TEST\\DIR" in captured.err +@pytest.mark.parametrize("constraint_flag", ["-c ", "--constraint ", "--constraint="]) +def test_pip_args_with_constraint_relative_path(constraint_flag, pipx_temp_env, tmp_path, caplog): + constraint_file_name = "constraints.txt" + package_name = "ipython" + package_version = "8.23.0" + + os.chdir(tmp_path) + constraints_file = tmp_path / constraint_file_name + constraints_file.write_text(f"{package_name}!={package_version}") + constraints_file.touch() + + assert not run_pipx_cli(["install", f"--pip-args='{constraint_flag}{constraint_file_name}'", package_name]) + + assert f"{constraint_flag}{constraints_file}" in caplog.text + + subprocess_package_version = subprocess.run([package_name, "--version"], capture_output=True, text=True, check=False) + subprocess_package_version_output = subprocess_package_version.stdout.strip() + assert subprocess_package_version_output != package_version + + +@pytest.mark.parametrize("constraint_flag", ["-c ", "--constraint ", "--constraint="]) +def test_pip_args_with_wrong_constraint_fail(constraint_flag, pipx_ultra_temp_env, tmp_path, capsys): + constraint_file_name = "constraints.txt" + os.chdir(tmp_path) + + assert run_pipx_cli(["install", f"--pip-args='{constraint_flag}{constraint_file_name}'", "pycowsay"]) + + assert ( + f"ERROR: Could not open requirements file: [Errno 2] No such file or directory: '{constraint_file_name}'" + in capsys.readouterr().err + ) + + def test_install_suffix(pipx_temp_env, capsys): name = "pbr"