diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 80776a47..6909e4cf 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -733,11 +733,7 @@ def _clean_location(self) -> bool: def _check_reused_environment_links(self) -> bool: """Check that interpreter links in the reused environment are not broken.""" bin_dir = self.bin - names = ( - ["python.exe"] - if _PLATFORM.startswith("win") and not _IS_MINGW - else ["python", "python3"] - ) + names = ["python.exe"] if self._windows_layout else ["python", "python3"] for name in names: path = os.path.join(bin_dir, name) @@ -895,10 +891,21 @@ def _install_python(self, cleaned_interpreter: str) -> str | None: return None return pbs_install_python(cleaned_interpreter) + @property + def _windows_layout(self) -> bool: + """Whether the venv uses a Windows-style layout (``Scripts``/``python.exe``). + + uv always builds a Windows-style venv, even under MSYS2/MinGW, where the + native interpreter and virtualenv/venv use a POSIX layout (``bin``/``python``). + """ + if not _PLATFORM.startswith("win"): + return False + return not _IS_MINGW or self.venv_backend == "uv" + @property def bin_paths(self) -> list[str]: """Returns the location of the virtualenv's bin folder.""" - if _PLATFORM.startswith("win") and not _IS_MINGW: + if self._windows_layout: return [os.path.join(self.location, "Scripts")] return [os.path.join(self.location, "bin")] @@ -930,13 +937,14 @@ def create(self) -> bool: cmd.extend(["-p", self._resolved_interpreter]) case "uv": _, uv, uv_ver = _uv_state() - cmd = [ - uv, - "venv", - "-p", - self._resolved_interpreter if self.interpreter else sys.executable, - self.location, - ] + cmd = [uv, "venv"] + if self.interpreter: + cmd += ["-p", self._resolved_interpreter] + elif not _IS_MINGW: + # uv can't inspect the MSYS2/MinGW interpreter, so let it + # pick the default rather than passing sys.executable. + cmd += ["-p", sys.executable] + cmd += [self.location] if version.Version("0.8") <= uv_ver: cmd += ["--clear"] case _: diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index b195abc9..31226d21 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -465,6 +465,41 @@ def test_create_args_old_uv( assert run_mock.call_args.args[0][-1] != "--clear" +def test_create_uv_with_interpreter( + make_one: Callable[..., tuple[VirtualEnv, Path]], + monkeypatch: pytest.MonkeyPatch, +) -> None: + run_mock = mock.Mock() + monkeypatch.setattr(nox.command, "run", run_mock) + monkeypatch.setattr(nox.virtualenv, "UV", "uv") + monkeypatch.setattr(nox.virtualenv, "HAS_UV", True) + monkeypatch.setattr(nox.virtualenv, "UV_VERSION", version.Version("0.10.0")) + venv, _ = make_one(interpreter="3.12", venv_backend="uv") + venv._resolved = "/path/to/python3.12" + venv.create() + run_mock.assert_called_once() + cmd = run_mock.call_args.args[0] + assert cmd[cmd.index("-p") + 1] == "/path/to/python3.12" + + +@mock.patch("nox.virtualenv._PLATFORM", new="win32") +@mock.patch("nox.virtualenv._IS_MINGW", new=True) +def test_create_uv_mingw_no_interpreter( + make_one: Callable[..., tuple[VirtualEnv, Path]], + monkeypatch: pytest.MonkeyPatch, +) -> None: + # uv can't inspect the MSYS2/MinGW interpreter, so nox must not pass ``-p``. + run_mock = mock.Mock() + monkeypatch.setattr(nox.command, "run", run_mock) + monkeypatch.setattr(nox.virtualenv, "UV", "uv") + monkeypatch.setattr(nox.virtualenv, "HAS_UV", True) + monkeypatch.setattr(nox.virtualenv, "UV_VERSION", version.Version("0.10.0")) + venv, _ = make_one(venv_backend="uv") + venv.create() + run_mock.assert_called_once() + assert "-p" not in run_mock.call_args.args[0] + + @has_uv def test_uv_creation( make_one: Callable[..., tuple[VirtualEnv, Path]], @@ -617,6 +652,19 @@ def test_bin_windows_mingw( assert str(dir_.joinpath("bin")) == venv.bin +@mock.patch("nox.virtualenv._PLATFORM", new="win32") +@mock.patch("nox.virtualenv._IS_MINGW", new=True) +def test_bin_windows_mingw_uv( + make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Path]], +) -> None: + # uv builds a Windows-style venv even under MinGW, unlike the native tools. + venv, dir_ = make_one(venv_backend="uv") + assert venv.bin_paths + assert len(venv.bin_paths) == 1 + assert venv.bin_paths[0] == venv.bin + assert str(dir_.joinpath("Scripts")) == venv.bin + + def test_create( monkeypatch: pytest.MonkeyPatch, make_one: Callable[..., tuple[VirtualEnv, Path]],