Skip to content

Commit

Permalink
fix: support broken uv (via pyenv)
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Jan 31, 2025
1 parent 97e0fad commit ad52a9e
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 25 deletions.
34 changes: 19 additions & 15 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"HAS_UV",
"OPTIONAL_VENVS",
"UV",
"UV_PYTHON_SUPPORT",
"UV_VERSION",
"CondaEnv",
"InterpreterNotFound",
"PassthroughEnv",
Expand Down Expand Up @@ -81,7 +81,7 @@ def __dir__() -> list[str]:
)


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

# Look for uv in Nox's environment, to handle `pipx install nox[uv]`.
Expand All @@ -90,22 +90,25 @@ def find_uv() -> tuple[bool, str]:

uv_bin = find_uv_bin()

# 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"
uv_vers = uv_version(uv_bin)
if uv_vers > version.Version("0"):
# 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", uv_vers

return True, uv_bin
return True, uv_bin, uv_vers

# Fall back to PATH.
return uv_on_path is not None, "uv"
uv_vers = uv_version("uv")
return uv_on_path is not None and uv_vers > version.Version("0"), "uv", uv_vers


def uv_version() -> version.Version:
def uv_version(uv_bin: str) -> version.Version:
"""Returns uv's version defaulting to 0.0 if uv is not available"""
try:
ret = subprocess.run(
[UV, "version", "--output-format", "json"],
[uv_bin, "version", "--output-format", "json"],
check=False,
text=True,
capture_output=True,
Expand All @@ -130,10 +133,7 @@ def uv_install_python(python_version: str) -> bool:
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")
HAS_UV, UV, UV_VERSION = find_uv()


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

# Supported since uv 0.3 but 0.4.16 is the first version that doesn't cause
# issues for nox with pypy/cpython confusion
if (
self.venv_backend == "uv" and HAS_UV and UV_PYTHON_SUPPORT
self.venv_backend == "uv"
and HAS_UV
and version.Version("0.4.16") <= UV_VERSION
): # pragma: nocover
uv_python_success = uv_install_python(cleaned_interpreter)
if uv_python_success:
Expand Down
39 changes: 29 additions & 10 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,12 +722,14 @@ def test_create_reuse_uv_environment(


@pytest.mark.parametrize(
("which_result", "find_uv_bin_result", "found", "path"),
("which_result", "find_uv_bin_result", "found", "path", "vers", "vers_rc"),
[
("/usr/bin/uv", UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV),
("/usr/bin/uv", None, True, "uv"),
(None, UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV),
(None, None, False, "uv"),
("/usr/bin/uv", UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV, "0.5.0", 0),
("/usr/bin/uv", None, True, "uv", "0.6.0", 0),
("/usr/bin/uv", None, False, "uv", "0.0.0", 0),
("/usr/bin/uv", None, False, "uv", "0.6.0", 1),
(None, UV_IN_PIPX_VENV, True, UV_IN_PIPX_VENV, "0.5.0", 0),
(None, None, False, "uv", "0.5.0", 0),
],
)
def test_find_uv(
Expand All @@ -736,19 +738,34 @@ def test_find_uv(
find_uv_bin_result: str | None,
found: bool,
path: str,
vers: str,
vers_rc: int,
) -> None:
def find_uv_bin() -> str:
if find_uv_bin_result:
return find_uv_bin_result
raise FileNotFoundError()

def mock_run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
return subprocess.CompletedProcess(
args=["uv", "version", "--output-format", "json"],
stdout=f'{{"version": "{vers}", "commit_info": null}}',
returncode=vers_rc,
)

monkeypatch.setattr(subprocess, "run", mock_run)

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() == (found, path)
assert nox.virtualenv.find_uv() == (
found,
path,
version.Version(vers if vers_rc == 0 else "0"),
)


@pytest.mark.parametrize(
Expand All @@ -773,15 +790,17 @@ def mock_run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str
)

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


def test_uv_version_no_uv(monkeypatch: pytest.MonkeyPatch) -> None:
def mock_exception(*args: object, **kwargs: object) -> NoReturn:
raise FileNotFoundError

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


@pytest.mark.parametrize(
Expand Down Expand Up @@ -1064,7 +1083,7 @@ def special_run(cmd: str, *args: str, **kwargs: object) -> TextProcessResult: #


@mock.patch("nox.virtualenv._PLATFORM", new="win32")
@mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False)
@mock.patch("nox.virtualenv.UV_VERSION", new=version.Version("0.3"))
def test__resolved_interpreter_windows_path_and_version(
make_one: Callable[..., tuple[VirtualEnv, Path]],
patch_sysfind: Callable[..., None],
Expand Down Expand Up @@ -1093,7 +1112,7 @@ def test__resolved_interpreter_windows_path_and_version(
@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._PLATFORM", new="win32")
@mock.patch("nox.virtualenv.UV_PYTHON_SUPPORT", new=False)
@mock.patch("nox.virtualenv.UV_VERSION", new=version.Version("0.3"))
def test__resolved_interpreter_windows_path_and_version_fails(
input_: str,
sysfind_result: None | str,
Expand Down

0 comments on commit ad52a9e

Please # to comment.