Repository: pypa/pipx (python)

Issue:
`reinstall` is destructive when misused with local source directory
<!--
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/
-->

pipx version 1.5.0

**Describe the bug**

I have a package installed with pipx, and I wanted to reinstall it from a local source repository. I ran 

```bash
pipx reinstall {absolute-path-to-repo}
```

thinking that it would do something equivalent to 

```bash
pipx install {absolute-path-to-repo} --force
```

Instead, it deleted the file tree at `{absolute-path-to-repo}`. 

From this experience, I have inferred that I have misunderstood the intended functionality and arguments of the `reinstall` command. (I'm inferring that reinstall accepts a path to a package's pipx virtual environment, rather than a path to package source.) However, I feel like this is an easy mistake to make, and the destructive side effect of this performing this action seems really bad. I did in fact lose some work that was only in my local git repository. 

**How to reproduce**

1. Create any directory
    ```bash
    mkdir testdir
    ```
2. Run `pipx reinstall` with an absolute path to that directory
    ```bash
    pipx reinstall $(pwd)/testdir
    ```
    This produces output
    ```
    uninstalled testdir! ✨ 🌟 ✨
    Fatal error from pip prevented installation. Full pip output in file:
        /Users/jqi/.local/pipx/logs/cmd_2024-04-05_19.03.13_pip_errors.log
 
    Some possibly relevant errors from pip install:
        ERROR: Could not find a version that satisfies the requirement testdir (from versions: none)
        ERROR: No matching distribution found for testdir
    
    Error installing testdir.
    ```
    with the side effect of deleting `testdir/`.

**Expected behavior**

`pipx reinstall` should not accept a local file path as an argument to prevent misuse. Or if this is actually desired functionality, there should more checks in place to ensure that it is intentional and actually a path to a pipx virtual environment. 

Pull request description:
Fix `pipx reinstall` crash with absolute path
<!-- 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

Closes #1324. This checks whether the argument given as package name inside `VenvContainer.get_venv_dir()` is an absolute path or existing path, raising `PipxError` if so. This is due to the behaviour that an absolute path passed into the `pathlib.PurePath.joinpath()` function to join with another path will return the same. 

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
pipx reinstall /some/absolute/path
mkdir existing_path ; pipx reinstall existing_path
```


Patch:
diff --git a/changelog.d/1324.bugfix.md b/changelog.d/1324.bugfix.md
new file mode 100644
index 0000000..3312909
--- /dev/null
+++ b/changelog.d/1324.bugfix.md
@@ -0,0 +1,1 @@
+Don't allow paths to be passed into `pipx reinstall`, as this might wreak havoc.
diff --git a/src/pipx/main.py b/src/pipx/main.py
index cee9df3..c49f2d3 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -185,7 +185,8 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar
 
     if "package" in args:
         package = args.package
-        if urllib.parse.urlparse(package).scheme:
+        url_parse_package = urllib.parse.urlparse(package)
+        if url_parse_package.scheme and url_parse_package.netloc:
             raise PipxError("Package cannot be a url")
 
         if "spec" in args and args.spec is not None:
@@ -193,6 +194,20 @@ def run_pipx_command(args: argparse.Namespace, subparsers: Dict[str, argparse.Ar
                 if "#egg=" not in args.spec:
                     args.spec = args.spec + f"#egg={package}"
 
+        if args.command == "reinstall":
+            # Passing paths into `reinstall` might have unintended
+            # side effects.
+            if Path(package).is_absolute() or Path(package).exists():
+                raise PipxError(
+                    pipx_wrap(
+                        f"""
+                        Error: Path '{package}' given as
+                        package. Expected the name of
+                        an installed package.
+                        """
+                    )
+                )
+
         venv_dir = venv_container.get_venv_dir(package)
         logger.info(f"Virtual Environment location is {venv_dir}")
 


Test patch:
diff --git a/tests/test_reinstall.py b/tests/test_reinstall.py
index 38f013f..b049c2f 100644
--- a/tests/test_reinstall.py
+++ b/tests/test_reinstall.py
@@ -54,3 +54,17 @@ def test_reinstall_specifier(pipx_temp_env, capsys):
     assert not run_pipx_cli(["reinstall", "--python", sys.executable, "pylint"])
     captured = capsys.readouterr()
     assert "installed package pylint 2.3.1" in captured.out
+
+
+def test_reinstall_with_path(pipx_temp_env, capsys, tmp_path):
+    path = tmp_path / "some" / "path"
+
+    assert run_pipx_cli(["reinstall", str(path)])
+    captured = capsys.readouterr()
+
+    assert "Expected the name of an installed package" in captured.err.replace("\n", " ")
+
+    assert run_pipx_cli(["reinstall", str(path.resolve())])
+    captured = capsys.readouterr()
+
+    assert "Expected the name of an installed package" in captured.err.replace("\n", " ")