Skip to content

Commit

Permalink
feat: support for uv-installed pythons (#842)
Browse files Browse the repository at this point in the history
* dummy support for uv python installs

* attempt install before confirming the interpreter is available

* adding some tests

* change version output to json so it's consistent in linux vs windows

* skip test with uv-python-support in windows

* fix using the wrong variable

caused problems with pypy

* change min supported uv version

* remove if statement

* change global variable and handle uv missing in uv_version

* typo
  • Loading branch information
saucoide authored Oct 2, 2024
1 parent ba29b2c commit a49c730
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 0 deletions.
41 changes: 41 additions & 0 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import abc
import contextlib
import functools
import json
import os
import platform
import re
Expand All @@ -28,6 +29,8 @@
from socket import gethostbyname
from typing import Any, ClassVar

from packaging import version

import nox
import nox.command
from nox.logger import logger
Expand Down Expand Up @@ -65,7 +68,39 @@ def find_uv() -> tuple[bool, str]:
return uv_on_path is not None, "uv"


def uv_version() -> version.Version:
"""Returns uv's version defaulting to 0.0 if uv is not available"""
try:
ret = subprocess.run(
[UV, "version", "--output-format", "json"],
check=False,
text=True,
capture_output=True,
)
except FileNotFoundError:
logger.info("uv binary not found.")
return version.Version("0.0")

if ret.returncode == 0 and ret.stdout:
return version.Version(json.loads(ret.stdout).get("version"))
else:
logger.info("Failed to establish uv's version.")
return version.Version("0.0")


def uv_install_python(python_version: str) -> bool:
"""Attempts to install a given python version with uv"""
ret = subprocess.run(
[UV, "python", "install", python_version],
check=False,
)
return ret.returncode == 0


HAS_UV, UV = find_uv()
# supported since uv 0.3 but 0.4.16 is the first version that doesn't cause
# issues for nox with pypy/cpython confusion
UV_PYTHON_SUPPORT = uv_version() >= version.Version("0.4.16")


class InterpreterNotFound(OSError):
Expand Down Expand Up @@ -526,6 +561,12 @@ def _resolved_interpreter(self) -> str:
self._resolved = cleaned_interpreter
return self._resolved

if HAS_UV and UV_PYTHON_SUPPORT:
uv_python_success = uv_install_python(cleaned_interpreter)
if uv_python_success:
self._resolved = cleaned_interpreter
return self._resolved

# The rest of this is only applicable to Windows, so if we don't have
# an interpreter by now, raise.
if _SYSTEM != "Windows":
Expand Down
51 changes: 51 additions & 0 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ def test_uv_creation(make_one):
assert venv._check_reused_environment_type()


@has_uv
def test_uv_managed_python(make_one):
make_one(interpreter="cpython3.12", venv_backend="uv")


def test_constructor_defaults(make_one):
venv, _ = make_one()
assert venv.location
Expand Down Expand Up @@ -620,6 +625,50 @@ def find_uv_bin():
assert nox.virtualenv.find_uv() == (found, path)


@pytest.mark.parametrize(
["return_code", "stdout", "expected_result"],
[
(0, '{"version": "0.2.3", "commit_info": null}', "0.2.3"),
(1, None, "0.0"),
(1, '{"version": "9.9.9", "commit_info": null}', "0.0"),
],
)
def test_uv_version(monkeypatch, return_code, stdout, expected_result):
def mock_run(*args, **kwargs):
return subprocess.CompletedProcess(
args=["uv", "version", "--output-format", "json"],
stdout=stdout,
returncode=return_code,
)

monkeypatch.setattr(subprocess, "run", mock_run)
assert nox.virtualenv.uv_version() == version.Version(expected_result)


def test_uv_version_no_uv(monkeypatch):
def mock_exception(*args, **kwargs):
raise FileNotFoundError

monkeypatch.setattr(subprocess, "run", mock_exception)
assert nox.virtualenv.uv_version() == version.Version("0.0")


@pytest.mark.parametrize(
["requested_python", "expected_result"],
[
("3.11", True),
("pypy3.8", True),
("cpython3.9", True),
("python3.12", True),
("nonpython9.22", False),
("java11", False),
],
)
@has_uv
def test_uv_install(requested_python, expected_result):
assert nox.virtualenv.uv_install_python(requested_python) == expected_result


def test_create_reuse_venv_environment(make_one, monkeypatch):
# Making the reuse requirement more strict
monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1")
Expand Down Expand Up @@ -840,6 +889,7 @@ def special_run(cmd, *args, **kwargs):


@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False)
def test__resolved_interpreter_windows_path_and_version(make_one, patch_sysfind):
# Establish that if we get a standard pythonX.Y path, we look it
# up via the path on Windows.
Expand All @@ -865,6 +915,7 @@ def test__resolved_interpreter_windows_path_and_version(make_one, patch_sysfind)
@pytest.mark.parametrize("sysfind_result", [r"c:\python37-x64\python.exe", None])
@pytest.mark.parametrize("sysexec_result", ["3.7.3\\n", RAISE_ERROR])
@mock.patch("nox.virtualenv._SYSTEM", new="Windows")
@mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False)
def test__resolved_interpreter_windows_path_and_version_fails(
input_, sysfind_result, sysexec_result, make_one, patch_sysfind
):
Expand Down

0 comments on commit a49c730

Please sign in to comment.