Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dummy support for uv-installed pythons #842

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 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,32 @@ def find_uv() -> tuple[bool, str]:
return uv_on_path is not None, "uv"


def uv_version() -> version.Version:
ret = subprocess.run(
[UV, "version", "--output-format", "json"],
check=False,
text=True,
capture_output=True,
)
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()
if HAS_UV:
UV_PYTHON_SUPPORT = uv_version() >= version.Version("0.3")


class InterpreterNotFound(OSError):
Expand Down Expand Up @@ -526,6 +554,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(self.interpreter)
if uv_python_success:
self._resolved = self.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
43 changes: 43 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,42 @@ 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_error(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)


@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 +881,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 +907,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