1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
import shutil
import subprocess
import sys
from unittest.mock import Mock
import pytest # type: ignore[import-not-found]
import pipx.interpreter
import pipx.paths
import pipx.standalone_python
from pipx.constants import WINDOWS
from pipx.interpreter import (
InterpreterResolutionError,
_find_default_windows_python,
_get_absolute_python_interpreter,
find_python_interpreter,
)
from pipx.util import PipxError
original_which = shutil.which
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Looks for Python.exe")
@pytest.mark.parametrize("venv", [True, False])
def test_windows_python_with_version(monkeypatch, venv):
def which(name):
if name == "py":
return "py"
return original_which(name)
major = sys.version_info.major
minor = sys.version_info.minor
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: venv)
monkeypatch.setattr(shutil, "which", which)
python_path = find_python_interpreter(f"{major}.{minor}")
assert python_path is not None
assert f"{major}.{minor}" in python_path or f"{major}{minor}" in python_path
assert python_path.endswith("python.exe")
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Looks for Python.exe")
@pytest.mark.parametrize("venv", [True, False])
def test_windows_python_with_python_and_version(monkeypatch, venv):
def which(name):
if name == "py":
return "py"
return original_which(name)
major = sys.version_info.major
minor = sys.version_info.minor
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: venv)
monkeypatch.setattr(shutil, "which", which)
python_path = find_python_interpreter(f"python{major}.{minor}")
assert python_path is not None
assert f"{major}.{minor}" in python_path or f"{major}{minor}" in python_path
assert python_path.endswith("python.exe")
@pytest.mark.skipif(not sys.platform.startswith("win"), reason="Looks for Python.exe")
@pytest.mark.parametrize("venv", [True, False])
def test_windows_python_with_python_and_unavailable_version(monkeypatch, venv):
def which(name):
if name == "py":
return "py"
return original_which(name)
major = sys.version_info.major + 99
minor = sys.version_info.minor
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: venv)
monkeypatch.setattr(shutil, "which", which)
with pytest.raises(InterpreterResolutionError) as e:
find_python_interpreter(f"python{major}.{minor}")
assert "py --list" in str(e)
def test_windows_python_no_version_with_venv(monkeypatch):
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: True)
assert _find_default_windows_python() == sys.executable
def test_windows_python_no_version_no_venv_with_py(monkeypatch):
def which(name):
return "py"
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
assert _find_default_windows_python() == "py"
def test_windows_python_no_version_no_venv_python_present(monkeypatch):
def which(name):
if name == "python":
return "python"
# Note: returns False for "py"
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
assert _find_default_windows_python() == "python"
def test_windows_python_no_version_no_venv_no_python(monkeypatch):
def which(name):
return None
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
with pytest.raises(PipxError):
_find_default_windows_python()
# Test the checks for the store Python.
def test_windows_python_no_venv_store_python(monkeypatch):
def which(name):
if name == "python":
return "WindowsApps"
class dummy_runner:
def __init__(self, rc, out):
self.rc = rc
self.out = out
def __call__(self, *args, **kw):
ret = Mock()
ret.returncode = self.rc
ret.stdout = self.out
return ret
monkeypatch.setattr(pipx.interpreter, "has_venv", lambda: False)
monkeypatch.setattr(shutil, "which", which)
# Store version stub gives return code 9009
monkeypatch.setattr(subprocess, "run", dummy_runner(9009, ""))
with pytest.raises(PipxError):
_find_default_windows_python()
# Even if it doesn't, it returns no output
monkeypatch.setattr(subprocess, "run", dummy_runner(0, ""))
with pytest.raises(PipxError):
_find_default_windows_python()
# 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
def test_find_python_interpreter_by_path(monkeypatch):
interpreter_path = sys.executable
assert interpreter_path == find_python_interpreter(interpreter_path)
def test_find_python_interpreter_by_version(monkeypatch):
major = sys.version_info.major
minor = sys.version_info.minor
python_path = find_python_interpreter(f"python{major}.{minor}")
assert python_path == f"python{major}.{minor}" or f"Python\\{major}.{minor}" in python_path
def test_find_python_interpreter_by_wrong_path_raises(monkeypatch):
interpreter_path = sys.executable + "99"
with pytest.raises(InterpreterResolutionError) as e:
find_python_interpreter(interpreter_path)
assert "like a path" in str(e)
def test_find_python_interpreter_missing_on_path_raises(monkeypatch):
interpreter = "1.1"
with pytest.raises(InterpreterResolutionError) as e:
find_python_interpreter(interpreter)
assert "Python Launcher" in str(e)
assert "on your PATH" in str(e)
def test_fetch_missing_python(monkeypatch, mocked_github_api):
def which(name):
return None
monkeypatch.setattr(shutil, "which", which)
major = sys.version_info.major
minor = sys.version_info.minor
target_python = f"{major}.{minor}"
python_path = find_python_interpreter(target_python, fetch_missing_python=True)
assert python_path is not None
assert target_python in python_path
assert str(pipx.paths.ctx.standalone_python_cachedir) in python_path
if WINDOWS:
assert python_path.endswith("python.exe")
else:
assert python_path.endswith("python3")
subprocess.run([python_path, "-c", "import sys; print(sys.executable)"], check=True)
|