Repository: pypa/pipx (python)

Issue:
Error with `pipx install` Command Using Relative Path for Pip Arguments
<!--
Thank you for filing a bug! Please feel free to answer as much or as little of this template as you can.

Please check pipx's Troubleshooting page to see if any of those solutions help solve your issue:
https://pipx.pypa.io/stable/troubleshooting/
-->

**Describe the bug**

When using the `pipx install` command with pip arguments `-c` or `--constraint`, using a relative path, we encounter an error stating that the file does not exist, even though it does.
<!-- Please be as detailed as possible! -->

**How to reproduce**

`pipx install --pip-args=--constraint=some_constraint_file.txt some_package_inside_constraint_file`
<!-- If possible, include output of `pipx --verbose ...` -->

**Expected behavior**

The package should be installed successfully.
<!-- What should have happened? -->

I have already identified that this issue occurs following the changes introduced in PR https://github.com/pypa/pipx/pull/1237.

Is it necessary to run the commands in a different directory?

If so, I have already begun drafting a solution for this issue.
My approach involves checking for `-c` and `--constraint` flags and replacing relative paths with absolute paths before calling the subprocess.

Do you think this proposed solution is appropriate? If there is a simpler way to resolve this issue, please advise.

Thank you very much for this package!

Pull request:
Fix passing constraints file path into `pipx install` operation via `--pip-args` 
closes #1389

<!-- add an 'x' in the brackets below -->

- [x] I have added a news fragment under `changelog.d/` (if the patch affects the end users)

## Summary of changes

Fix error with `pipx install` Command Using Relative Path for Pip Arguments

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
pipx install --pip-args=--constraint=some_constraint_file.txt some_package_inside_constraint_file
```


Patch:
diff --git a/changelog.d/1389.bugfix.md b/changelog.d/1389.bugfix.md
new file mode 100644
index 0000000..a290ff8
--- /dev/null
+++ b/changelog.d/1389.bugfix.md
@@ -0,0 +1,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 5f2d20c..79f5551 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 62fe053..066a6b3 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:


Test patch:
diff --git a/tests/test_install.py b/tests/test_install.py
index a4fc3b2..b83310e 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"