Repository: pypa/pipx (python)

Issue:
Improve docs for resolving `space in home path` warning
**How would this feature be useful?**

<!-- Describe any use cases this solves or frustrations it alleviates.   -->

The existing warning and docs are not enough for users to resolve it.

**Describe the solution you'd like**

<!-- If you have an idea of how to do this, write it here! -->
Dedicated docs on how to move the home path and a clear link to it in the warning, as well as specifying it's the `Pipx Home` that has the space in it.

**Describe alternatives you've considered**

<!-- If there's some workaround or alternative solutions, let us know. -->

Hope people that are confused just find the existing discussions.


### Discussed in https://github.com/pypa/pipx/discussions/1315

<div type='discussions-op-text'>

<sup>Originally posted by **homocomputeris** April  1, 2024</sup>
I get the message every time I run pipx.

- What is home path here?
- Where are the docs on this issue? Nothing can be found here: https://www.google.com/search?hl=en&q=%22home%20path%22%20site%3Ahttps%3A%2F%2Fpipx.pypa.io%2Flatest%2F
- How to fix or disable this warning?</div>

Pull request:
Introduce a new env variable to allow spaces in home 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
Introduces an environment variable to silence the warning for having a space in the pipx home path.

Improves the docs on how to troubleshoot the warning and move the pipx installation location in general. This makes [the discussion](https://github.com/pypa/pipx/discussions/1330) I created a while ago redundant.

Resolves #1320

## Test plan

<!-- provide evidence of testing, preferably with command(s) that can be copy+pasted by others -->

Tested by running

```
nox -s tests-3.8 -- -k test_environment
```


Patch:
diff --git a/changelog.d/1320.bugfix b/changelog.d/1320.bugfix
new file mode 100644
index 0000000..28c2e01
--- /dev/null
+++ b/changelog.d/1320.bugfix
@@ -0,0 +1,1 @@
+Introduce `PIPX_HOME_ALLOW_SPACE` environment variable, to silence the spaces in pipx home path warning
diff --git a/docs/installation.md b/docs/installation.md
index 3ad7cbd..90c9a24 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -156,8 +156,8 @@ and `~\pipx` on Windows. For compatibility reasons, if `~/.local/pipx` on Linux,
 This can be overridden with the `PIPX_HOME` environment variable.
 
 In case one of these fallback locations exist, we recommend either manually moving the pipx files to the new default location
-(see the `Troubleshooting` section of the docs), or setting the `PIPX_HOME` environment variable (discarding files existing in
-the fallback location).
+(see the [Moving your pipx installation](installation.md#moving-your-pipx-installation) section of the docs), or setting the
+`PIPX_HOME` environment variable (discarding files existing in the fallback location).
 
 As an example, you can install global apps accessible by all users on your system with the following command (on MacOS,
 Linux, and Windows WSL):
@@ -184,7 +184,7 @@ sudo PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin PIPX_MAN_DIR=/usr/local/sha
 > See the [platformdirs documentation](https://platformdirs.readthedocs.io/en/latest/api.html#platforms) for details.
 >
 > This was reverted in 1.5.0 for Windows and MacOS. We heavily recommend not using these locations on Windows and MacOS anymore, due to
-> multiple incompatibilities discovered with these locations, documented [here](https://github.com/pypa/pipx/discussions/1247#discussion-6188916).
+> multiple incompatibilities discovered with these locations, documented [here](troubleshooting.md#why-are-spaces-in-the-pipx_home-path-bad).
 
 ### Customising your installation
 
@@ -257,3 +257,65 @@ You can easily get your shell's tab completions working by following instruction
 ```
 pipx completions
 ```
+
+## Moving your pipx installation
+
+The below code snippets show how to move your pipx installation to a new directory.
+As an example, they move from a non-default location to the current default locations.
+If you wish to move to a different location, just replace the `NEW_LOCATION` value.
+
+### MacOS
+
+Current default location: `~/.local`
+
+```bash
+NEW_LOCATION=~/.local
+cache_dir=$(pipx environment --value PIPX_VENV_CACHEDIR)
+logs_dir=$(pipx environment --value PIPX_LOG_DIR)
+trash_dir=$(pipx environment --value PIPX_TRASH_DIR)
+home_dir=$(pipx environment --value PIPX_HOME)
+rm -rf "$cache_dir" "$logs_dir" "$trash_dir"
+mkdir -p $NEW_LOCATION && mv "$home_dir" $NEW_LOCATION
+pipx reinstall-all
+```
+
+### Linux
+
+Current default location: `~/.local/share`
+
+```bash
+cache_dir=$(pipx environment --value PIPX_VENV_CACHEDIR)
+logs_dir=$(pipx environment --value PIPX_LOG_DIR)
+trash_dir=$(pipx environment --value PIPX_TRASH_DIR)
+home_dir=$(pipx environment --value PIPX_HOME)
+# If you wish another location, replace the expression below
+# and set `NEW_LOCATION` explicitly
+NEW_LOCATION="${XDG_DATA_HOME:-$HOME/.local/share}"
+rm -rf "$cache_dir" "$logs_dir" "$trash_dir"
+mkdir -p $NEW_LOCATION && mv "$home_dir" $NEW_LOCATION
+pipx reinstall-all
+```
+
+### Windows
+
+Current default location: `~/pipx`
+
+```powershell
+$NEW_LOCATION = Join-Path "$HOME" 'pipx'
+$cache_dir = pipx environment --value PIPX_VENV_CACHEDIR
+$logs_dir = pipx environment --value PIPX_LOG_DIR
+$trash_dir = pipx environment --value PIPX_TRASH_DIR
+$home_dir = pipx environment --value PIPX_HOME
+
+Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "$cache_dir", "$logs_dir", "$trash_dir"
+
+# Remove the destination directory to ensure rename behavior of `Move-Item`
+Remove-Item -Recurse -Force -ErrorAction SilentlyContinue "$NEW_LOCATION"
+
+Move-Item -Path $home_dir -Destination "$NEW_LOCATION"
+pipx reinstall-all
+```
+
+If you would prefer doing it in bash via git-bash/WSL, feel free to use
+the MacOS/Linux instructions, changing the `$NEW_LOCATION` to the Windows
+version.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index b4b6503..d446c2d 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -148,13 +148,28 @@ In Pipx version 1.5.0, this was reverted for Windows and MacOS. It defaults agai
 
 If you have a `pipx` version later than 1.2.0 and want to migrate from the old path to the new paths, you can move the
 `~/.local/pipx` directory to the new location (after removing cache, log, and trash directories which will get recreated
-automatically) and then reinstall all packages. For example, on Linux systems, `PIPX_HOME` moves from `~/.local/pipx` to
-`~/.local/share/pipx` so you can do this:
+automatically) and then reinstall all packages.
 
-```
-rm -rf ~/.local/pipx/{.cache,logs,trash}
-mkdir -p ~/.local/share && mv ~/.local/pipx ~/.local/share/
-pipx reinstall-all
-```
+Please refer to [Installation](installation.md#moving-your-pipx-installation) on how to move it.
+
+## Warning: Found a space in the pipx home path
+
+In pipx version 1.5, we introduced the warning you're seeing, as multiple incompatibilites with spaces in the pipx home path were discovered. You may see this for the following reasons:
+
+1. From pipx version 1.3 to 1.5, we were by default using a path with a space on it on MacOS. This unfortunately means, that all users that installed pipx in this time frame and were using the default behavior are seeing this warning now.
+2. You set your `PIPX_HOME` to a path with spaces in it explicitly or because your `$HOME` path contains a space.
+
+### Why are spaces in the `PIPX_HOME` path bad
+
+The main reason we can't support paths with spaces is that shebangs don't support spaces in the interpreter path. All applications installed via `pipx` are installed via `pip`, which creates a script with a shebang at the top, defining the interpreter of the `venv` to use.
+
+`pip` does some magic to the shebang for scripts defined as a `script`, that resolves this issue. Unfortunately, many libraries define their scripts as `console_scripts`, where `pip` does not perform this logic. Therefore, these scripts cannot be run if installed with `pipx` in a path with spaces, as the path to the `venv` and therefore the interpreter to use will contain spaces.
+
+If you want to use a script installed via pipx in a shebang itself (common for example for the aws cli), you run into a similar problem, as the path to the installed script will contain a space.
+
+### How to fix
+
+You can generally fix this by using our default locations, as long as your `$HOME` path does not contain spaces.
+Please refer to our [Installation](installation.md#moving-your-pipx-installation) docs on how to move the `pipx` installation.
 
-For moving the paths back after 1.5.0, you can perform the same steps, switching the paths around.
+If you're really sure you want to stick to your path with spaces, to suppress the warning set the `PIPX_HOME_ALLOW_SPACE` environment variable to `true`.
diff --git a/src/pipx/commands/environment.py b/src/pipx/commands/environment.py
index cca49a1..deb8815 100644
--- a/src/pipx/commands/environment.py
+++ b/src/pipx/commands/environment.py
@@ -17,6 +17,7 @@ def environment(value: str) -> ExitCode:
         "PIPX_DEFAULT_PYTHON",
         "PIPX_FETCH_MISSING_PYTHON",
         "USE_EMOJI",
+        "PIPX_HOME_ALLOW_SPACE",
     ]
     derived_values = {
         "PIPX_HOME": paths.ctx.home,
@@ -30,6 +31,7 @@ def environment(value: str) -> ExitCode:
         "PIPX_STANDALONE_PYTHON_CACHEDIR": paths.ctx.standalone_python_cachedir,
         "PIPX_DEFAULT_PYTHON": DEFAULT_PYTHON,
         "USE_EMOJI": str(EMOJI_SUPPORT).lower(),
+        "PIPX_HOME_ALLOW_SPACE": str(paths.ctx.allow_spaces_in_home_path).lower(),
     }
     if value is None:
         print("Environment variables (set by user):")
diff --git a/src/pipx/main.py b/src/pipx/main.py
index ee97fba..5f2d20c 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -84,11 +84,12 @@ PIPX_DESCRIPTION = textwrap.dedent(
 PIPX_DESCRIPTION += pipx_wrap(
     """
     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.
-      PIPX_MAN_DIR          Overrides location of manual pages installations. Manual pages are symlinked or copied here.
-      PIPX_DEFAULT_PYTHON   Overrides default python used for commands.
-      USE_EMOJI             Overrides emoji behavior. Default value varies based on platform.
+      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.
+      PIPX_MAN_DIR           Overrides location of manual pages installations. Manual pages are symlinked or copied here.
+      PIPX_DEFAULT_PYTHON    Overrides default python used for commands.
+      USE_EMOJI              Overrides emoji behavior. Default value varies based on platform.
+      PIPX_HOME_ALLOW_SPACE  Overrides default warning on spaces in the home path
     """,
     subsequent_indent=" " * 24,  # match the indent of argparse options
     keep_newlines=True,
@@ -906,7 +907,7 @@ def _add_environment(subparsers: argparse._SubParsersAction, shared_parser: argp
 
             Available variables:
             PIPX_HOME, PIPX_BIN_DIR, PIPX_MAN_DIR, PIPX_SHARED_LIBS, PIPX_LOCAL_VENVS,
-            PIPX_LOG_DIR, PIPX_TRASH_DIR, PIPX_VENV_CACHEDIR, PIPX_DEFAULT_PYTHON, USE_EMOJI
+            PIPX_LOG_DIR, PIPX_TRASH_DIR, PIPX_VENV_CACHEDIR, PIPX_DEFAULT_PYTHON, USE_EMOJI, PIPX_HOME_ALLOW_SPACE
             """
         ),
         parents=[shared_parser],
diff --git a/src/pipx/paths.py b/src/pipx/paths.py
index cbb9c38..ab59930 100644
--- a/src/pipx/paths.py
+++ b/src/pipx/paths.py
@@ -6,7 +6,7 @@ from typing import List, Optional, Union
 from platformdirs import user_cache_path, user_data_path, user_log_path
 
 from pipx.constants import LINUX, WINDOWS
-from pipx.emojis import hazard
+from pipx.emojis import hazard, strtobool
 from pipx.util import pipx_wrap
 
 if LINUX:
@@ -106,12 +106,16 @@ class _PathContext:
     def standalone_python_cachedir(self) -> Path:
         return self.home / "py"
 
+    @property
+    def allow_spaces_in_home_path(self) -> bool:
+        return strtobool(os.getenv("PIPX_HOME_ALLOW_SPACE", "0"))
+
     def log_warnings(self):
-        if " " in str(self.home):
+        if " " in str(self.home) and not self.allow_spaces_in_home_path:
             logger.warning(
                 pipx_wrap(
                     (
-                        f"{hazard} Found a space in the home path. We heavily discourage this, due to "
+                        f"{hazard} Found a space in the pipx home path. We heavily discourage this, due to "
                         "multiple incompatibilities. Please check our docs for more information on this, "
                         "as well as some pointers on how to migrate to a different home path."
                     ),


Test patch:
diff --git a/tests/test_environment.py b/tests/test_environment.py
index bfd91c5..e2b0683 100644
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -2,6 +2,7 @@ import fnmatch
 from pathlib import Path
 
 from helpers import run_pipx_cli, skip_if_windows
+from pipx import paths
 from pipx.paths import get_expanded_environ
 
 
@@ -19,6 +20,7 @@ def test_cli(pipx_temp_env, monkeypatch, capsys):
     # Checking just for the sake of completeness
     assert "PIPX_DEFAULT_PYTHON" in captured.out
     assert "USE_EMOJI" in captured.out
+    assert "PIPX_HOME_ALLOW_SPACE" in captured.out
     assert "Environment variables (set by user):" in captured.out
 
 
@@ -33,6 +35,7 @@ def test_cli_with_args(monkeypatch, capsys):
     assert not run_pipx_cli(["environment", "--value", "PIPX_VENV_CACHEDIR"])
     assert not run_pipx_cli(["environment", "--value", "PIPX_DEFAULT_PYTHON"])
     assert not run_pipx_cli(["environment", "--value", "USE_EMOJI"])
+    assert not run_pipx_cli(["environment", "--value", "PIPX_HOME_ALLOW_SPACE"])
 
     assert run_pipx_cli(["environment", "--value", "SSS"])
     captured = capsys.readouterr()
@@ -49,6 +52,29 @@ def test_resolve_user_dir_in_env_paths(monkeypatch):
     assert env_dir is None
 
 
+def test_allow_space_in_pipx_home(
+    monkeypatch,
+    capsys,
+    tmp_path,
+):
+    home_dir = Path(tmp_path) / "path with space"
+    monkeypatch.setattr(paths.ctx, "_base_home", home_dir)
+    assert not run_pipx_cli(["environment", "--value", "PIPX_HOME_ALLOW_SPACE"])
+    paths.ctx.log_warnings()
+    captured = capsys.readouterr()
+    assert "Found a space" in captured.err
+    assert "false" in captured.out
+
+    monkeypatch.setenv("PIPX_HOME_ALLOW_SPACE", "1")
+    assert not run_pipx_cli(["environment", "--value", "PIPX_HOME_ALLOW_SPACE"])
+    paths.ctx.log_warnings()
+    captured = capsys.readouterr()
+    assert "Found a space" not in captured.err
+    assert "true" in captured.out
+
+    paths.ctx.make_local()
+
+
 @skip_if_windows
 def test_cli_global(pipx_temp_env, monkeypatch, capsys):
     assert not run_pipx_cli(["environment", "--global"])
@@ -64,3 +90,4 @@ def test_cli_global(pipx_temp_env, monkeypatch, capsys):
     # Checking just for the sake of completeness
     assert "PIPX_DEFAULT_PYTHON" in captured.out
     assert "USE_EMOJI" in captured.out
+    assert "PIPX_DEFAULT_PYTHON" in captured.out