diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f4808f9..ec349577 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,6 +40,7 @@ repos: - jinja2 - packaging - importlib_metadata + - uv - repo: https://github.com/codespell-project/codespell rev: v2.2.6 diff --git a/nox/sessions.py b/nox/sessions.py index 2728b1fa..5ea14d1b 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -41,7 +41,7 @@ import nox.virtualenv from nox._decorators import Func from nox.logger import logger -from nox.virtualenv import CondaEnv, PassthroughEnv, ProcessEnv, VirtualEnv +from nox.virtualenv import UV, CondaEnv, PassthroughEnv, ProcessEnv, VirtualEnv if TYPE_CHECKING: from nox.manifest import Manifest @@ -656,7 +656,7 @@ def install(self, *args: str, **kwargs: Any) -> None: kwargs["silent"] = True if isinstance(venv, VirtualEnv) and venv.venv_backend == "uv": - self._run("uv", "pip", "install", *args, external="error", **kwargs) + self._run(UV, "pip", "install", *args, external="error", **kwargs) else: self._run( "python", "-m", "pip", "install", *args, external="error", **kwargs diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 6f4b6099..d8cd9c89 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -39,6 +39,23 @@ _SYSTEM = platform.system() +def find_uv() -> tuple[bool, str]: + uv = shutil.which("uv") + if uv is not None: + return True, uv + + # Look for uv in Nox's environment as well, to handle `pipx install nox[uv]`. + with contextlib.suppress(ImportError, FileNotFoundError): + from uv import find_uv_bin + + return True, find_uv_bin() + + return False, "uv" + + +HAS_UV, UV = find_uv() + + class InterpreterNotFound(OSError): def __init__(self, interpreter: str) -> None: super().__init__(f"Python interpreter {interpreter} not found") @@ -325,7 +342,7 @@ class VirtualEnv(ProcessEnv): """ is_sandboxed = True - allowed_globals = ("uv",) + allowed_globals = (UV,) def __init__( self, @@ -524,7 +541,7 @@ def create(self) -> bool: cmd.extend(["-p", self._resolved_interpreter]) elif self.venv_backend == "uv": cmd = [ - "uv", + UV, "venv", "-p", self._resolved_interpreter if self.interpreter else sys.executable, @@ -560,5 +577,5 @@ def create(self) -> bool: OPTIONAL_VENVS = { "conda": shutil.which("conda") is not None, "mamba": shutil.which("mamba") is not None, - "uv": shutil.which("uv") is not None, + "uv": HAS_UV, } diff --git a/pyproject.toml b/pyproject.toml index df7d531b..38b6ec87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ tox_to_nox = [ "tox", ] uv = [ - "uv", + "uv>=0.1.6", ] [project.urls] bug-tracker = "https://github.com/wntrblm/nox/issues" diff --git a/tests/test_sessions.py b/tests/test_sessions.py index ff09e42a..c700e235 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -825,7 +825,7 @@ class SessionNoSlots(nox.sessions.Session): with mock.patch.object(session, "_run", autospec=True) as run: session.install("requests", "urllib3", silent=False) run.assert_called_once_with( - "uv", + nox.virtualenv.UV, "pip", "install", "requests", diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 6af872bc..ed533952 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -19,6 +19,7 @@ import shutil import subprocess import sys +import types from textwrap import dedent from typing import NamedTuple from unittest import mock @@ -538,6 +539,32 @@ def test_create_reuse_uv_environment(make_one): assert reused +UV_IN_PIPX_VENV = "/home/user/.local/pipx/venvs/nox/bin/uv" + + +@pytest.mark.parametrize( + ["which_result", "find_uv_bin_result", "expected"], + [ + ("/usr/bin/uv", UV_IN_PIPX_VENV, (True, "/usr/bin/uv")), + ("/usr/bin/uv", FileNotFoundError, (True, "/usr/bin/uv")), + (None, UV_IN_PIPX_VENV, (True, UV_IN_PIPX_VENV)), + (None, FileNotFoundError, (False, "uv")), + ], +) # fmt: skip +def test_find_uv(monkeypatch, which_result, find_uv_bin_result, expected): + def find_uv_bin(): + if find_uv_bin_result is FileNotFoundError: + raise FileNotFoundError + return find_uv_bin_result + + monkeypatch.setattr(shutil, "which", lambda _: which_result) + monkeypatch.setitem( + sys.modules, "uv", types.SimpleNamespace(find_uv_bin=find_uv_bin) + ) + + assert nox.virtualenv.find_uv() == expected + + def test_create_reuse_venv_environment(make_one, monkeypatch): # Making the reuse requirement more strict monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1")