Repository: pipxproject/pipx (python)

Issue:
Add pyenv compatibility
I've got a CLI-tool (tmux-res) I built for handling some common tasks in my TMUX-environment which I built a while back. For fun I wanted to try installing it via pipx but it doesn't work.

This is the package:
https://pypi.org/project/tmux-res/

```sh
 $ pyenv global 3.7.2
 $ python --version
Python 3.7.2

 $ pipx install tmux-res
  Could not find a version that satisfies the requirement tmux-res (from versions: )
No matching distribution found for tmux-res
'/Users/hultner/.local/pipx/venvs/tmux-res/bin/python -m pip install tmux-res -q' failed

 $ pip install tmux-res
Collecting tmux-res
  Downloading https://files.pythonhosted.org/packages/09/80/a0294c244aebbdc9f13c06b182a7c1857618da20eabd1c88c4f449dbdf09/tmux_res-0.4.8-py3-none-any.whl
Collecting python-dotenv (from tmux-res)
  Downloading https://files.pythonhosted.org/packages/8c/14/501508b016e7b1ad0eb91bba581e66ad9bfc7c66fcacbb580eaf9bc38458/python_dotenv-0.10.1-py2.py3-none-any.whl
Collecting Click (from tmux-res)
  Using cached https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl
Installing collected packages: python-dotenv, Click, tmux-res
Successfully installed Click-7.0 python-dotenv-0.10.1 tmux-res-0.4.8
You are using pip version 18.1, however version 19.0.3 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
```


Pull request description:
Add env var to set DEFAULT_PYTHON (add Pyenv support)
Fixes #113.

This keeps sys.executable as the default, but will use the PIPX_DEFAULT_PYTHON environment variable, if defined. This mean no change in functionality for any users until they define the variable.

I'm suggesting the var `PIPX_DEFAULT_PYTHON` (no `PYENV` even though the original issue asked for pyenv support), because this approach will work with any python manager, and I think it might be confusing to enshrine one manager in the var name over others.

Also, it didn't look like the constants are tested, so I didn't update any tests. But let me know if I missed something and I'll be happy to add.


<!---
Thank you for your soon-to-be pull request. Before you submit this, please
double check to make sure that you've added an entry to docs/changelog.md.
-->

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 0b72694..8287291 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -4,6 +4,7 @@ dev
 - [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`
 - Update help text to show description for `ensurepath` and `completions` help
+- Add support for user-defined default python interpreter via new PIPX_DEFAULT_PYTHON.  Helpful for use with pyenv among other uses.
 
 
 0.15.5.1
diff --git a/src/pipx/interpreter.py b/src/pipx/interpreter.py
index cb876c4..0757b12 100644
--- a/src/pipx/interpreter.py
+++ b/src/pipx/interpreter.py
@@ -1,3 +1,4 @@
+import os
 import shutil
 import subprocess
 import sys
@@ -56,7 +57,23 @@ def _find_default_windows_python() -> str:
     return python  # This executable seems to work.
 
 
-if WINDOWS:
-    DEFAULT_PYTHON = _find_default_windows_python()
+def _get_sys_executable() -> str:
+    if WINDOWS:
+        return _find_default_windows_python()
+    else:
+        return sys.executable
+
+
+def _get_absolute_python_interpreter(env_python: str) -> str:
+    which_python = shutil.which(env_python)
+    if not which_python:
+        raise PipxError(f"Default python interpreter {repr(env_python)} is invalid")
+    return which_python
+
+
+env_default_python = os.environ.get("PIPX_DEFAULT_PYTHON")
+
+if not env_default_python:
+    DEFAULT_PYTHON = _get_sys_executable()
 else:
-    DEFAULT_PYTHON = sys.executable
+    DEFAULT_PYTHON = _get_absolute_python_interpreter(env_default_python)
diff --git a/src/pipx/main.py b/src/pipx/main.py
index 41a4898..89ac57f 100644
--- a/src/pipx/main.py
+++ b/src/pipx/main.py
@@ -49,6 +49,7 @@ 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.
 """
 )
 
@@ -78,6 +79,10 @@ and can be overridden by setting the environment variable `PIPX_HOME`
 
 The default app location is {constants.DEFAULT_PIPX_BIN_DIR} and can be
 overridden by setting the environment variable `PIPX_BIN_DIR`.
+
+The default python executable used to install a package is
+{DEFAULT_PYTHON} and can be overridden by setting the environment
+variable `PIPX_DEFAULT_PYTHON`.
 """
 
 
@@ -110,7 +115,6 @@ def get_venv_args(parsed_args: Dict) -> List[str]:
 
 
 def run_pipx_command(args: argparse.Namespace):  # noqa: C901
-    setup(args)
     verbose = args.verbose if "verbose" in args else False
     pip_args = get_pip_args(vars(args))
     venv_args = get_venv_args(vars(args))
@@ -561,6 +565,8 @@ def setup(args):
         logging.basicConfig(level=logging.WARNING, format="%(message)s")
 
     logging.info(f"pipx version is {__version__}")
+    logging.info(f"Default python interpreter is {repr(DEFAULT_PYTHON)}")
+
     mkdir(constants.PIPX_LOCAL_VENVS)
     mkdir(constants.LOCAL_BIN_DIR)
     mkdir(constants.PIPX_VENV_CACHEDIR)


Test patch:
diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py
index c7a93e2..f54ea5e 100644
--- a/tests/test_interpreter.py
+++ b/tests/test_interpreter.py
@@ -3,7 +3,10 @@ import subprocess
 import sys
 import pytest  # type: ignore
 import pipx.interpreter
-from pipx.interpreter import _find_default_windows_python
+from pipx.interpreter import (
+    _find_default_windows_python,
+    _get_absolute_python_interpreter,
+)
 from pipx.util import PipxError
 
 
@@ -79,3 +82,13 @@ def test_windows_python_no_venv_store_python(monkeypatch):
     # If it *does* pass the tests, we use it as it's not the stub
     monkeypatch.setattr(subprocess, "run", dummy_runner(0, "3.8"))
     assert _find_default_windows_python() == "WindowsApps"
+
+
+def test_bad_env_python(monkeypatch):
+    with pytest.raises(PipxError):
+        _get_absolute_python_interpreter("bad_python")
+
+
+def test_good_env_python(monkeypatch, capsys):
+    good_exec = _get_absolute_python_interpreter(sys.executable)
+    assert good_exec == sys.executable