Skip to content

Commit

Permalink
fix: ensure 'uv' always works in a uv venv (#818)
Browse files Browse the repository at this point in the history
* fix: ensure 'uv' always works in a uv venv

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: always use simplest form if on PATH

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: verify correction works

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: monkeypatch lower down for new test

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: handle corner case of user instalilng uv into a uv environment

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* tests: add tests for installed UV

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Apr 15, 2024
1 parent d6e1906 commit 8f33d1c
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 17 deletions.
14 changes: 12 additions & 2 deletions nox/sessions.py
Expand Up @@ -21,6 +21,7 @@
import os
import pathlib
import re
import shutil
import subprocess
import sys
import unicodedata
Expand All @@ -43,7 +44,7 @@
from nox._decorators import Func
from nox.logger import logger
from nox.popen import DEFAULT_INTERRUPT_TIMEOUT, DEFAULT_TERMINATE_TIMEOUT
from nox.virtualenv import UV, CondaEnv, PassthroughEnv, ProcessEnv, VirtualEnv
from nox.virtualenv import CondaEnv, PassthroughEnv, ProcessEnv, VirtualEnv

if TYPE_CHECKING:
from typing import IO
Expand Down Expand Up @@ -556,6 +557,15 @@ def _run(
if callable(args[0]):
return self._run_func(args[0], args[1:]) # type: ignore[unreachable]

# Using `"uv"` when `uv` is the backend is guaranteed to work, even if it was co-installed with nox.
if (
self.virtualenv.venv_backend == "uv"
and args[0] == "uv"
and nox.virtualenv.UV != "uv"
and shutil.which("uv", path=self.bin) is None # Session uv takes priority
):
args = (nox.virtualenv.UV, *args[1:])

# Combine the env argument with our virtualenv's env vars.
if include_outer_env:
overlay_env = env or {}
Expand Down Expand Up @@ -769,7 +779,7 @@ def install(
silent = True

if isinstance(venv, VirtualEnv) and venv.venv_backend == "uv":
cmd = [UV, "pip", "install"]
cmd = ["uv", "pip", "install"]
else:
cmd = ["python", "-m", "pip", "install"]
self._run(
Expand Down
18 changes: 12 additions & 6 deletions nox/virtualenv.py
Expand Up @@ -24,6 +24,7 @@
import subprocess
import sys
from collections.abc import Callable, Mapping
from pathlib import Path
from socket import gethostbyname
from typing import Any, ClassVar

Expand All @@ -45,18 +46,23 @@


def find_uv() -> tuple[bool, str]:
uv_on_path = shutil.which("uv")

# Look for uv in Nox's environment, to handle `pipx install nox[uv]`.
with contextlib.suppress(ImportError, FileNotFoundError):
from uv import find_uv_bin

return True, find_uv_bin()
uv_bin = find_uv_bin()

# Fall back to PATH.
uv = shutil.which("uv")
if uv is not None:
return True, uv
# If the returned value is the same as calling "uv" already, don't
# expand (simpler logging)
if uv_on_path and Path(uv_bin).samefile(uv_on_path):
return True, "uv"

return False, "uv"
return True, uv_bin

# Fall back to PATH.
return uv_on_path is not None, "uv"


HAS_UV, UV = find_uv()
Expand Down
53 changes: 52 additions & 1 deletion tests/test_sessions.py
Expand Up @@ -869,14 +869,65 @@ 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(
nox.virtualenv.UV,
"uv",
"pip",
"install",
"requests",
"urllib3",
**_run_with_defaults(silent=False, external="error"),
)

def test_install_uv_command(self, monkeypatch):
runner = nox.sessions.SessionRunner(
name="test",
signatures=["test"],
func=mock.sentinel.func,
global_config=_options.options.namespace(posargs=[]),
manifest=mock.create_autospec(nox.manifest.Manifest),
)
runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
runner.venv.env = {}
runner.venv.venv_backend = "uv"

class SessionNoSlots(nox.sessions.Session):
pass

session = SessionNoSlots(runner=runner)

monkeypatch.setattr(nox.virtualenv, "UV", "/some/uv")
monkeypatch.setattr(shutil, "which", lambda x, path=None: None)

with mock.patch.object(nox.command, "run", autospec=True) as run:
session.install("requests", "urllib3", silent=False)
run.assert_called_once()

((call_args,), _) = run.call_args
assert call_args == (
"/some/uv",
"pip",
"install",
"requests",
"urllib3",
)

# user installs uv in the session venv
monkeypatch.setattr(
shutil, "which", lambda x, path="": path + "/uv" if x == "uv" else None
)

with mock.patch.object(nox.command, "run", autospec=True) as run:
session.install("requests", "urllib3", silent=False)
run.assert_called_once()

((call_args,), _) = run.call_args
assert call_args == (
"uv",
"pip",
"install",
"requests",
"urllib3",
)

def test___slots__(self):
session, _ = self.make_session_and_runner()
with pytest.raises(AttributeError):
Expand Down
18 changes: 10 additions & 8 deletions tests/test_virtualenv.py
Expand Up @@ -21,6 +21,7 @@
import subprocess
import sys
import types
from pathlib import Path
from textwrap import dedent
from typing import NamedTuple
from unittest import mock
Expand Down Expand Up @@ -602,26 +603,27 @@ def test_create_reuse_uv_environment(make_one):


@pytest.mark.parametrize(
["which_result", "find_uv_bin_result", "expected"],
["which_result", "find_uv_bin_result", "found", "path"],
[
("/usr/bin/uv", UV_IN_PIPX_VENV, (True, UV_IN_PIPX_VENV)),
("/usr/bin/uv", FileNotFoundError, (True, "/usr/bin/uv")),
(None, UV_IN_PIPX_VENV, (True, UV_IN_PIPX_VENV)),
(None, FileNotFoundError, (False, "uv")),
("/usr/bin/uv", UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV),
("/usr/bin/uv", FileNotFoundError, True, "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 test_find_uv(monkeypatch, which_result, find_uv_bin_result, found, path):
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.setattr(Path, "samefile", lambda a, b: a == b)
monkeypatch.setitem(
sys.modules, "uv", types.SimpleNamespace(find_uv_bin=find_uv_bin)
)

assert nox.virtualenv.find_uv() == expected
assert nox.virtualenv.find_uv() == (found, path)


def test_create_reuse_venv_environment(make_one, monkeypatch):
Expand Down

0 comments on commit 8f33d1c

Please sign in to comment.