From ad52a9e3d22015833113370150db3ab081c1199e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 31 Jan 2025 15:48:41 -0500 Subject: [PATCH] fix: support broken uv (via pyenv) Signed-off-by: Henry Schreiner --- nox/virtualenv.py | 34 +++++++++++++++++++--------------- tests/test_virtualenv.py | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/nox/virtualenv.py b/nox/virtualenv.py index bb29e60d..3b97ab1f 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -44,7 +44,7 @@ "HAS_UV", "OPTIONAL_VENVS", "UV", - "UV_PYTHON_SUPPORT", + "UV_VERSION", "CondaEnv", "InterpreterNotFound", "PassthroughEnv", @@ -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]`. @@ -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, @@ -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): @@ -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: diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index dcc620a1..850aa63f 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -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( @@ -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( @@ -773,7 +790,9 @@ 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: @@ -781,7 +800,7 @@ 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( @@ -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], @@ -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,