diff --git a/src/isolate/backends/virtualenv.py b/src/isolate/backends/virtualenv.py index 0ee68e4..a556863 100644 --- a/src/isolate/backends/virtualenv.py +++ b/src/isolate/backends/virtualenv.py @@ -77,17 +77,38 @@ def install_requirements(self, path: Path) -> None: except subprocess.SubprocessError as exc: raise EnvironmentCreationError("Failure during 'pip install'.") from exc + def _install_python_through_pyenv(self) -> str: + from isolate.backends.pyenv import PyenvEnvironment + + self.log( + f"Requested Python version of {self.python_version} is not available " + "in the system, attempting to install it through pyenv." + ) + + pyenv = PyenvEnvironment.from_config( + {"python_version": self.python_version}, + settings=self.settings, + ) + return str(get_executable_path(pyenv.create(), "python")) + def _decide_python(self) -> str: from virtualenv.discovery import builtin + from isolate.backends.pyenv import _get_pyenv_executable + interpreter = builtin.get_interpreter(self.python_version, ()) if interpreter is not None: return interpreter.executable - raise EnvironmentCreationError( - f"Python {self.python_version} is not available in your " - "system. Please install it first." - ) + try: + _get_pyenv_executable() + except Exception: + raise EnvironmentCreationError( + f"Python {self.python_version} is not available in your " + "system. Please install it first." + ) from None + else: + return self._install_python_through_pyenv() def create(self) -> Path: from virtualenv import cli_run diff --git a/tests/test_backends.py b/tests/test_backends.py index f0a4968..63ec625 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -278,7 +278,12 @@ def test_caching_with_constraints(self, tmp_path): ) assert environment_1.key != environment_2.key != environment_3.key - def test_custom_python_version(self, tmp_path): + def test_custom_python_version(self, tmp_path, monkeypatch): + # Disable pyenv to prevent auto-installation + monkeypatch.setattr( + "isolate.backends.pyenv._get_pyenv_executable", lambda: 1 / 0 + ) + for python_type, expected_python_version in [ ("old-python", "3.7"), ("new-python", "3.10"), @@ -294,7 +299,12 @@ def test_custom_python_version(self, tmp_path): python_version = self.get_python_version(environment, connection) assert python_version.startswith(expected_python_version) - def test_invalid_python_version_raises(self, tmp_path): + def test_invalid_python_version_raises(self, tmp_path, monkeypatch): + # Disable pyenv to prevent auto-installation + monkeypatch.setattr( + "isolate.backends.pyenv._get_pyenv_executable", lambda: 1 / 0 + ) + # Hopefully there will never be a Python 9.9.9 environment = self.get_environment(tmp_path, {"python_version": "9.9.9"}) with pytest.raises( @@ -636,3 +646,27 @@ def test_pyenv_environment(python_version, tmp_path): different_python.destroy(connection_key) assert not different_python.exists() + + +@pytest.mark.skipif(not IS_PYENV_AVAILABLE, reason="Pyenv is not available") +def test_virtual_env_custom_python_version_with_pyenv(tmp_path, monkeypatch): + pyjokes_env = VirtualPythonEnvironment( + requirements=["pyjokes==0.6.0"], + python_version="3.9", + ) + + test_settings = IsolateSettings(Path(tmp_path)) + pyjokes_env.apply_settings(test_settings) + + # Force it to choose pyenv as the python version manager. + pyjokes_env._decide_python = pyjokes_env._install_python_through_pyenv + + connection_key = pyjokes_env.create() + with pyjokes_env.open_connection(connection_key) as connection: + assert connection.run(partial(eval, "__import__('sys').version")).startswith( + "3.9" + ) + assert ( + connection.run(partial(eval, "__import__('pyjokes').__version__")) + == "0.6.0" + )