Repository: pipxproject/pipx (python)

Issue:
Add `reinstall` command


**How would this feature be useful?**
<!-- Describe any use cases this solves or frustrations it alleviates.   -->
per @sahensley 's comment in https://github.com/pipxproject/pipx/pull/246

> Also, a bit of a side trek but would it be worth introducing reinstall for an individual package? I could possible see a need if only certain venvs were made with a Python interpreter that is no longer present but it may be a bit of a niche case. The instance I've run into this when I've had multiple Python versions compiled via Pyenv.

**Describe the solution you'd like**
<!-- If you have an idea of how to do this, write it here! -->
Add top-level `reinstall` command, as an alternative to the `reinstall-all` command.

**Describe alternatives you've considered**
<!-- If there's some workaround or alternative solutions, let us know. -->
Uninstall then reinstall the package, but that is not super very friendly.

Pull request:
Reinstall command
<!-- add an 'x' in the brackets below -->
* [x] I have added an entry to `docs/changelog.md`

## Summary of changes
Adds a `reinstall` command to only reinstall one venv.

Closes #256 

## Test plan
<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->
Tested by running
```
pipx install pycowsay
pipx reinstall pycowsay
```


Patch:
diff --git a/.gitignore b/.gitignore
index d117c05..92395ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,4 @@ venv
 .tox
 __pycache__
 site
+docs/docs.md
diff --git a/docs/changelog.md b/docs/changelog.md
index 6c6ba43..e39429a 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,10 +1,11 @@
 dev
 
-
+- Added reinstall command for reinstalling a single venv.
 
 0.15.6.0
 
 - [docs] Update license
+- [docs] Display a more idomatic command for registering completions on fish.
 - [bugfix] Fixed regression in list, inject, upgrade, reinstall-all commands when suffixed packages are used.
 - [bugfix] Do not reset package url during upgrade when main package is `pipx`
 - Updated help text to show description for `ensurepath` and `completions` help
diff --git a/docs/docs.md b/docs/docs.md
deleted file mode 100644
index aed6126..0000000
--- a/docs/docs.md
+++ /dev/null
@@ -1,356 +0,0 @@
-<!---
-Do not edit this file. This file was rendered from the
-templates/ directory.
-See Contributing for how to update this file.
---->
-
-```
-pipx --help
-usage: pipx [-h] [--version]
-            {install,inject,upgrade,upgrade-all,uninstall,uninstall-all,reinstall-all,list,run,runpip,ensurepath,completions}
-            ...
-
-Install and execute apps from Python packages.
-
-Binaries can either be installed globally into isolated Virtual Environments
-or run directly in an temporary Virtual Environment.
-
-Virtual Environment location is ~/.local/pipx/venvs.
-Symlinks to apps are placed in ~/.local/bin.
-
-Optional Environment Variables:
-PIPX_HOME: Overrides default pipx location. Virtual Environments
-will be installed to $PIPX_HOME/venvs.
-PIPX_BIN_DIR: Overrides location of app installations. Apps are symlinked
-or copied here.
-USE_EMOJI: Override emoji behavior. Default value varies based on platform.
-PIPX_DEFAULT_PYTHON: Overrides default python used for commands.
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --version             Print version and exit
-
-subcommands:
-  Get help for commands with pipx COMMAND --help
-
-  {install,inject,upgrade,upgrade-all,uninstall,uninstall-all,reinstall-all,list,run,runpip,ensurepath,completions}
-    install             Install a package
-    inject              Install packages into an existing Virtual Environment
-    upgrade             Upgrade a package
-    upgrade-all         Upgrade all packages. Runs `pip install -U <pkgname>`
-                        for each package.
-    uninstall           Uninstall a package
-    uninstall-all       Uninstall all packages
-    reinstall-all       Reinstall all packages
-    list                List installed packages
-    run                 Download the latest version of a package to a
-                        temporary virtual environment, then run an app from
-                        it. Also compatible with local `__pypackages__`
-                        directory (experimental).
-    runpip              Run pip in an existing pipx-managed Virtual
-                        Environment
-    ensurepath          Ensure directories necessary for pipx operation are in
-                        your PATH environment variable.
-    completions         Print instructions on enabling shell completions for
-                        pipx
-
-```
-
-
-### pipx install
-
-```
-pipx install --help
-usage: pipx install [-h] [--include-deps] [--verbose] [--force]
-                    [--suffix SUFFIX] [--python PYTHON]
-                    [--system-site-packages] [--index-url INDEX_URL]
-                    [--editable] [--pip-args PIP_ARGS]
-                    package_spec
-
-The install command is the preferred way to globally install apps
-from python packages on your system. It creates an isolated virtual
-environment for the package, then ensures the package's apps are
-accessible on your $PATH.
-
-The result: apps you can run from anywhere, located in packages
-you can cleanly upgrade or uninstall. Guaranteed to not have
-dependency version conflicts or interfere with your OS's python
-packages. 'sudo' is not required to do this.
-
-pipx install PACKAGE_NAME
-pipx install --python PYTHON PACKAGE_NAME
-pipx install VCS_URL
-pipx install ./LOCAL_PATH
-pipx install ZIP_FILE
-pipx install TAR_GZ_FILE
-
-The PACKAGE_SPEC argument is passed directly to `pip install`.
-
-The default virtual environment location is ~/.local/pipx
-and can be overridden by setting the environment variable `PIPX_HOME`
- (Virtual Environments will be installed to `$PIPX_HOME/venvs`).
-
-The default app location is ~/.local/bin and can be
-overridden by setting the environment variable `PIPX_BIN_DIR`.
-
-The default python executable used to install a package is
-~/git/pipx-pipxproject/.nox/docs/bin/python and can be overridden by setting the environment
-variable `PIPX_DEFAULT_PYTHON`.
-
-positional arguments:
-  package_spec          package name or pip installation spec
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --include-deps        Include apps of dependent packages
-  --verbose
-  --force, -f           Modify existing virtual environment and files in
-                        PIPX_BIN_DIR
-  --suffix SUFFIX       Optional suffix for virtual environment and executable
-                        names. NOTE: The suffix feature is experimental and
-                        subject to change.
-  --python PYTHON       The Python executable used to create the Virtual
-                        Environment and run the associated app/apps. Must be
-                        v3.5+.
-  --system-site-packages
-                        Give the virtual environment access to the system
-                        site-packages dir.
-  --index-url INDEX_URL, -i INDEX_URL
-                        Base URL of Python Package Index
-  --editable, -e        Install a project in editable mode
-  --pip-args PIP_ARGS   Arbitrary pip arguments to pass directly to pip
-                        install/upgrade commands
-
-```
-
-
-### pipx run
-
-```
-pipx run --help
-usage: pipx run [-h] [--no-cache] [--pypackages] [--spec SPEC] [--verbose]
-                [--python PYTHON] [--system-site-packages]
-                [--index-url INDEX_URL] [--editable] [--pip-args PIP_ARGS]
-                app ...
-
-Download the latest version of a package to a temporary virtual environment,
-then run an app from it. The environment will be cached
-and re-used for up to 14 days. This
-means subsequent calls to 'run' for the same package will be faster
-since they can re-use the cached Virtual Environment.
-
-In support of PEP 582 'run' will use apps found in a local __pypackages__
- directory, if present. Please note that this behavior is experimental,
- and is a acts as a companion tool to pythonloc. It may be modified or
- removed in the future. See https://github.com/cs01/pythonloc.
-
-positional arguments:
-  app ...               app/package name and any arguments to be passed to it
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --no-cache            Do not re-use cached virtual environment if it exists
-  --pypackages          Require app to be run from local __pypackages__
-                        directory
-  --spec SPEC           The package name or specific installation source
-                        passed to pip. Runs `pip install -U SPEC`. For example
-                        `--spec mypackage==2.0.0` or `--spec
-                        git+https://github.com/user/repo.git@branch`
-  --verbose
-  --python PYTHON       The Python version to run package's CLI app with. Must
-                        be v3.5+.
-  --system-site-packages
-                        Give the virtual environment access to the system
-                        site-packages dir.
-  --index-url INDEX_URL, -i INDEX_URL
-                        Base URL of Python Package Index
-  --editable, -e        Install a project in editable mode
-  --pip-args PIP_ARGS   Arbitrary pip arguments to pass directly to pip
-                        install/upgrade commands
-
-```
-
-
-### pipx upgrade
-
-```
-pipx upgrade --help
-usage: pipx upgrade [-h] [--force] [--system-site-packages]
-                    [--index-url INDEX_URL] [--editable] [--pip-args PIP_ARGS]
-                    [--verbose]
-                    package
-
-Upgrade a package in a pipx-managed Virtual Environment by running 'pip
-install --upgrade PACKAGE'
-
-positional arguments:
-  package
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --force, -f           Modify existing virtual environment and files in
-                        PIPX_BIN_DIR
-  --system-site-packages
-                        Give the virtual environment access to the system
-                        site-packages dir.
-  --index-url INDEX_URL, -i INDEX_URL
-                        Base URL of Python Package Index
-  --editable, -e        Install a project in editable mode
-  --pip-args PIP_ARGS   Arbitrary pip arguments to pass directly to pip
-                        install/upgrade commands
-  --verbose
-
-```
-
-
-### pipx upgrade-all
-
-```
-pipx upgrade-all --help
-usage: pipx upgrade-all [-h] [--skip SKIP [SKIP ...]] [--force] [--verbose]
-
-Upgrades all packages within their virtual environments by running 'pip
-install --upgrade PACKAGE'
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --skip SKIP [SKIP ...]
-                        skip these packages
-  --force, -f           Modify existing virtual environment and files in
-                        PIPX_BIN_DIR
-  --verbose
-
-```
-
-
-### pipx inject
-
-```
-pipx inject --help
-usage: pipx inject [-h] [--include-apps] [--include-deps]
-                   [--system-site-packages] [--index-url INDEX_URL]
-                   [--editable] [--pip-args PIP_ARGS] [--force] [--verbose]
-                   package dependencies [dependencies ...]
-
-Installs packages to an existing pipx-managed virtual environment.
-
-positional arguments:
-  package               Name of the existing pipx-managed Virtual Environment
-                        to inject into
-  dependencies          the packages to inject into the Virtual Environment--
-                        either package name or pip package spec
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --include-apps        Add apps from the injected packages onto your PATH
-  --include-deps        Include apps of dependent packages
-  --system-site-packages
-                        Give the virtual environment access to the system
-                        site-packages dir.
-  --index-url INDEX_URL, -i INDEX_URL
-                        Base URL of Python Package Index
-  --editable, -e        Install a project in editable mode
-  --pip-args PIP_ARGS   Arbitrary pip arguments to pass directly to pip
-                        install/upgrade commands
-  --force, -f           Modify existing virtual environment and files in
-                        PIPX_BIN_DIR
-  --verbose
-
-```
-
-
-### pipx uninstall
-
-```
-pipx uninstall --help
-usage: pipx uninstall [-h] [--verbose] package
-
-Uninstalls a pipx-managed Virtual Environment by deleting it and any files
-that point to its apps.
-
-positional arguments:
-  package
-
-optional arguments:
-  -h, --help  show this help message and exit
-  --verbose
-
-```
-
-
-### pipx uninstall-all
-
-```
-pipx uninstall-all --help
-usage: pipx uninstall-all [-h] [--verbose]
-
-Uninstall all pipx-managed packages
-
-optional arguments:
-  -h, --help  show this help message and exit
-  --verbose
-
-```
-
-
-### pipx reinstall-all
-
-```
-pipx reinstall-all --help
-usage: pipx reinstall-all [-h] [--python PYTHON] [--skip SKIP [SKIP ...]]
-                          [--verbose]
-
-Reinstalls all packages.
-
-Packages are uninstalled, then installed with pipx install PACKAGE
-with the same options used in the original install of PACKAGE.
-This is useful if you upgraded to a new version of Python and want
-all your packages to use the latest as well.
-
-optional arguments:
-  -h, --help            show this help message and exit
-  --python PYTHON       The Python executable used to recreate the Virtual
-                        Environment and run the associated app/apps. Must be
-                        v3.5+.
-  --skip SKIP [SKIP ...]
-                        skip these packages
-  --verbose
-
-```
-
-
-### pipx list
-
-```
-pipx list --help
-usage: pipx list [-h] [--include-injected] [--verbose]
-
-List packages and apps installed with pipx
-
-optional arguments:
-  -h, --help          show this help message and exit
-  --include-injected  Show packages injected into the main app's environment
-  --verbose
-
-```
-
-
-### pipx runpip
-
-```
-pipx runpip --help
-usage: pipx runpip [-h] [--verbose] package ...
-
-Run pip in an existing pipx-managed Virtual Environment
-
-positional arguments:
-  package     Name of the existing pipx-managed Virtual Environment to run pip
-              in
-  pipargs     Arguments to forward to pip command
-
-optional arguments:
-  -h, --help  show this help message and exit
-  --verbose
-
-```
-
diff --git a/noxfile.py b/noxfile.py
index 95c0814..9fd76f3 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -132,7 +132,6 @@ def publish(session):
     build(session)
     print("REMINDER: Has the changelog been updated?")
     session.run("python", "-m", "twine", "upload", "dist/*")
-    publish_docs(session)
 
 
 @nox.session(python="3.8")
diff --git a/src/pipx/commands/__init__.py b/src/pipx/commands/__init__.py
index bfa3fa6..e8b245d 100644
--- a/src/pipx/commands/__init__.py
+++ b/src/pipx/commands/__init__.py
@@ -2,13 +2,12 @@ from .ensure_path import ensure_pipx_paths
 from .inject import inject
 from .install import install
 from .list_packages import list_packages
-from .reinstall import reinstall_all
+from .reinstall import reinstall, reinstall_all
 from .run import run
 from .run_pip import run_pip
 from .uninstall import uninstall, uninstall_all
 from .upgrade import upgrade, upgrade_all
 
-
 __all__ = [
     "upgrade",
     "upgrade_all",
@@ -17,6 +16,7 @@ __all__ = [
     "inject",
     "uninstall",
     "uninstall_all",
+    "reinstall",
     "reinstall_all",
     "list_packages",
     "run_pip",
diff --git a/src/pipx/commands/reinstall.py b/src/pipx/commands/reinstall.py
index c4064f0..d4842ec 100644
--- a/src/pipx/commands/reinstall.py
+++ b/src/pipx/commands/reinstall.py
@@ -5,13 +5,19 @@ from typing import List
 from pipx.commands.inject import inject
 from pipx.commands.install import install
 from pipx.commands.uninstall import uninstall
+from pipx.emojies import sleep
 from pipx.util import PipxError
 from pipx.venv import Venv, VenvContainer
 
 
 def reinstall(
     *, venv_dir: Path, local_bin_dir: Path, python: str, verbose: bool,
-):
+) -> int:
+    """Returns pipx shell exit code"""
+    if not venv_dir.exists():
+        print(f"Nothing to reinstall for {venv_dir.name} {sleep}")
+        return 1
+
     venv = Venv(venv_dir, verbose=verbose)
 
     if venv.pipx_metadata.main_package.package_or_url is not None:
@@ -58,6 +64,8 @@ def reinstall(
             force=True,
         )
 
+    return 0
+
 
 def reinstall_all(
     venv_container: VenvContainer,
@@ -66,7 +74,8 @@ def reinstall_all(
     verbose: bool,
     *,
     skip: List[str],
-):
+) -> int:
+    """Returns pipx shell exit code"""
     failed: List[str] = []
     for venv_dir in venv_container.iter_venv_dirs():
         if venv_dir.name in skip:
@@ -85,3 +94,4 @@ def reinstall_all(
         raise PipxError(
             f"The following package(s) failed to reinstall: {', '.join(failed)}"
         )
+    return 0
diff --git a/src/pipx/constants.py b/src/pipx/constants.py
index e8a3870..e5cf9ed 100644
--- a/src/pipx/constants.py
+++ b/src/pipx/constants.py
@@ -70,7 +70,8 @@ tcsh:
     eval `register-python-argcomplete --shell tcsh pipx`
 
 fish:
-    register-python-argcomplete --shell fish pipx | source
+    # Not required to be in the config file, only run once
+    register-python-argcomplete --shell fish pipx >~/.config/fish/completions/pipx.fish
 
 """
 )
diff --git a/src/pipx/main.py b/src/pipx/main.py
index 89ac57f..a53081c 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -114,7 +114,7 @@ def get_venv_args(parsed_args: Dict) -> List[str]:
     return venv_args
 
 
-def run_pipx_command(args: argparse.Namespace):  # noqa: C901
+def run_pipx_command(args: argparse.Namespace) -> int:  # noqa: C901
     verbose = args.verbose if "verbose" in args else False
     pip_args = get_pip_args(vars(args))
     venv_args = get_venv_args(vars(args))
@@ -182,12 +182,16 @@ def run_pipx_command(args: argparse.Namespace):  # noqa: C901
                 include_dependencies=args.include_deps,
                 force=args.force,
             )
+        # TODO: Issue #503 make pipx commands have proper exit codes
+        return 0
     elif args.command == "upgrade":
         return commands.upgrade(
             venv_dir, pip_args, verbose, upgrading_all=False, force=args.force
         )
     elif args.command == "list":
-        return commands.list_packages(venv_container, args.include_injected)
+        commands.list_packages(venv_container, args.include_injected)
+        # TODO: Issue #503 make pipx commands have proper exit codes
+        return 0
     elif args.command == "uninstall":
         return commands.uninstall(venv_dir, constants.LOCAL_BIN_DIR, verbose)
     elif args.command == "uninstall-all":
@@ -196,6 +200,13 @@ def run_pipx_command(args: argparse.Namespace):  # noqa: C901
         return commands.upgrade_all(
             venv_container, verbose, skip=args.skip, force=args.force
         )
+    elif args.command == "reinstall":
+        return commands.reinstall(
+            venv_dir=venv_dir,
+            local_bin_dir=constants.LOCAL_BIN_DIR,
+            python=args.python,
+            verbose=verbose,
+        )
     elif args.command == "reinstall-all":
         return commands.reinstall_all(
             venv_container,
@@ -370,6 +381,33 @@ def _add_uninstall_all(subparsers):
     p.add_argument("--verbose", action="store_true")
 
 
+def _add_reinstall(subparsers, autocomplete_list_of_installed_packages):
+    p = subparsers.add_parser(
+        "reinstall",
+        formatter_class=LineWrapRawTextHelpFormatter,
+        help="Reinstall a package",
+        description=textwrap.dedent(
+            """
+        Reinstalls a package.
+
+        Package is uninstalled, then installed with pipx install PACKAGE
+        with the same options used in the original install of PACKAGE.
+
+        """
+        ),
+    )
+    p.add_argument("package").completer = autocomplete_list_of_installed_packages
+    p.add_argument(
+        "--python",
+        default=DEFAULT_PYTHON,
+        help=(
+            "The Python executable used to recreate the Virtual Environment "
+            "and run the associated app/apps. Must be v3.5+."
+        ),
+    )
+    p.add_argument("--verbose", action="store_true")
+
+
 def _add_reinstall_all(subparsers):
     p = subparsers.add_parser(
         "reinstall-all",
@@ -536,6 +574,7 @@ def get_command_parser():
     _add_upgrade_all(subparsers)
     _add_uninstall(subparsers, autocomplete_list_of_installed_packages)
     _add_uninstall_all(subparsers)
+    _add_reinstall(subparsers, autocomplete_list_of_installed_packages)
     _add_reinstall_all(subparsers)
     _add_list(subparsers)
     _add_run(subparsers)


Test patch:
diff --git a/tests/test_reinstall.py b/tests/test_reinstall.py
new file mode 100644
index 0000000..04bfd35
--- /dev/null
+++ b/tests/test_reinstall.py
@@ -0,0 +1,43 @@
+import sys
+
+import pytest  # type: ignore
+
+from helpers import mock_legacy_venv, run_pipx_cli
+
+
+def test_reinstall(pipx_temp_env, capsys):
+    assert not run_pipx_cli(["install", "pycowsay"])
+    assert not run_pipx_cli(["reinstall", "--python", sys.executable, "pycowsay"])
+
+
+def test_reinstall_nonexistent(pipx_temp_env, capsys):
+    assert run_pipx_cli(["reinstall", "--python", sys.executable, "nonexistent"])
+    assert "Nothing to reinstall for nonexistent" in capsys.readouterr().out
+
+
+@pytest.mark.parametrize("metadata_version", [None, "0.1"])
+def test_reinstall_legacy_venv(pipx_temp_env, capsys, metadata_version):
+    assert not run_pipx_cli(["install", "pycowsay"])
+    mock_legacy_venv("pycowsay", metadata_version=metadata_version)
+
+    assert not run_pipx_cli(["reinstall", "--python", sys.executable, "pycowsay"])
+
+
+def test_reinstall_suffix(pipx_temp_env, capsys):
+    suffix = "_x"
+    assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"])
+
+    assert not run_pipx_cli(
+        ["reinstall", "--python", sys.executable, f"pycowsay{suffix}"]
+    )
+
+
+@pytest.mark.parametrize("metadata_version", ["0.1"])
+def test_reinstall_suffix_legacy_venv(pipx_temp_env, capsys, metadata_version):
+    suffix = "_x"
+    assert not run_pipx_cli(["install", "pycowsay", f"--suffix={suffix}"])
+    mock_legacy_venv(f"pycowsay{suffix}", metadata_version=metadata_version)
+
+    assert not run_pipx_cli(
+        ["reinstall", "--python", sys.executable, f"pycowsay{suffix}"]
+    )