diff --git a/nox/_options.py b/nox/_options.py index 24987b44..02a20814 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -356,7 +356,7 @@ def _session_completer( " ``'virtualenv'`` by default but any of ``('virtualenv', 'conda', 'mamba'," " 'venv')`` are accepted." ), - choices=["none", "virtualenv", "conda", "mamba", "venv"], + choices=["none", "virtualenv", "conda", "mamba", "venv", "uv"], ), _option_set.Option( "force_venv_backend", @@ -371,7 +371,7 @@ def _session_completer( " the default backend. Any of ``('virtualenv', 'conda', 'mamba', 'venv')``" " are accepted." ), - choices=["none", "virtualenv", "conda", "mamba", "venv"], + choices=["none", "virtualenv", "conda", "mamba", "venv", "uv"], ), _option_set.Option( "no_venv", diff --git a/nox/sessions.py b/nox/sessions.py index 2c8db437..56cd3252 100644 --- a/nox/sessions.py +++ b/nox/sessions.py @@ -632,7 +632,7 @@ def install(self, *args: str, **kwargs: Any) -> None: if "silent" not in kwargs: kwargs["silent"] = True - if getattr(venv, "venv_prog", "") == "uv": + if isinstance(venv, VirtualEnv) and venv.venv_backend == "uv": self._run("uv", "pip", "install", *args, external="error", **kwargs) else: self._run( @@ -758,7 +758,7 @@ def _create_venv(self) -> None: self.envdir, interpreter=self.func.python, # type: ignore[arg-type] reuse_existing=reuse_existing, - venv=backend or "virtualenv", + venv_backend=backend or "virtualenv", venv_params=self.func.venv_params, ) elif backend in {"conda", "mamba"}: diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 8942b4d1..7bd468b4 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -320,7 +320,7 @@ def __init__( interpreter: str | None = None, reuse_existing: bool = False, *, - venv: str = "virtualenv", + venv_backend: str = "virtualenv", venv_params: Any = None, ): self.location_name = location @@ -328,7 +328,7 @@ def __init__( self.interpreter = interpreter self._resolved: None | str | InterpreterNotFound = None self.reuse_existing = reuse_existing - self.venv_prog = venv + self.venv_backend = venv_backend self.venv_params = venv_params or [] super().__init__(env={"VIRTUAL_ENV": self.location}) @@ -360,7 +360,11 @@ def _check_reused_environment_type(self) -> bool: old_env = ( "virtualenv" if any(pattern.match(line) for line in fp) else "venv" ) - return old_env == self.venv_prog + # We can't distinguish a uv env from a venv env, so just treat them + # the same. + return old_env == self.venv_backend or ( + {old_env, self.venv_backend} == {"uv", "venv"} + ) def _check_reused_environment_interpreter(self) -> bool: """Check if reused environment interpreter is the same.""" @@ -475,11 +479,11 @@ def create(self) -> bool: return False - if self.venv_prog == "virtualenv": + if self.venv_backend == "virtualenv": cmd = [sys.executable, "-m", "virtualenv", self.location] if self.interpreter: cmd.extend(["-p", self._resolved_interpreter]) - elif self.venv_prog == "uv": + elif self.venv_backend == "uv": cmd = [ "uv", "virtualenv", @@ -494,7 +498,7 @@ def create(self) -> bool: resolved_interpreter_name = os.path.basename(self._resolved_interpreter) logger.info( - f"Creating virtual environment ({self.venv_prog}) using" + f"Creating virtual environment ({self.venv_backend}) using" f" {resolved_interpreter_name} in {self.location_name}" ) nox.command.run(cmd, silent=True, log=nox.options.verbose or False) diff --git a/noxfile.py b/noxfile.py index ff1e9599..3ebed4b6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -86,7 +86,7 @@ def lint(session: nox.Session) -> None: ) -@nox.session(venv_backend="uv") +@nox.session def docs(session: nox.Session) -> None: """Build the documentation.""" output_dir = os.path.join(session.create_tmp(), "output") @@ -95,7 +95,7 @@ def docs(session: nox.Session) -> None: ) shutil.rmtree(output_dir, ignore_errors=True) session.install("-r", "requirements-test.txt") - session.install("nox @ .") + session.install(".") session.cd("docs") sphinx_args = ["-b", "html", "-W", "-d", doctrees, ".", html] diff --git a/pyproject.toml b/pyproject.toml index 8c535d71..2903e4f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ tox_to_nox = [ "tox<4", ] uv = [ - "uv" + "uv", ] [project.urls] bug-tracker = "https://github.com/wntrblm/nox/issues" diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 00a912c6..2c308146 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -79,6 +79,7 @@ def make_session_and_runner(self): runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) runner.venv.env = {} runner.venv.bin_paths = ["/no/bin/for/you"] + runner.venv.venv_backend = "venv" return nox.sessions.Session(runner=runner), runner def test_create_tmp(self): @@ -629,6 +630,7 @@ def test_install(self): ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) runner.venv.env = {} + runner.venv.venv_backend = "venv" class SessionNoSlots(nox.sessions.Session): pass @@ -658,6 +660,7 @@ def test_install_non_default_kwargs(self): ) runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv) runner.venv.env = {} + runner.venv.venv_backend = "venv" class SessionNoSlots(nox.sessions.Session): pass @@ -794,6 +797,35 @@ def test_session_venv_reused_with_no_install(self, no_install, reused, run_calle assert run.called is run_called + def test_install_uv(self): + 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) + + with mock.patch.object(session, "_run", autospec=True) as run: + session.install("requests", "urllib3", silent=False) + run.assert_called_once_with( + "uv", + "pip", + "install", + "requests", + "urllib3", + silent=False, + external=True, + ) + def test___slots__(self): session, _ = self.make_session_and_runner() with pytest.raises(AttributeError): diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 36d5a2a3..5fe51a3e 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -31,6 +31,7 @@ IS_WINDOWS = nox.virtualenv._SYSTEM == "Windows" HAS_CONDA = shutil.which("conda") is not None +HAS_UV = shutil.which("uv") is not None RAISE_ERROR = "RAISE_ERROR" VIRTUALENV_VERSION = virtualenv.__version__ @@ -240,6 +241,17 @@ def test_condaenv_detection(make_conda): assert path_regex.search(output).group("env_dir") == dir_.strpath +@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.") +def test_uv_creation(make_one): + venv, _ = make_one(venv_backend="uv") + assert venv.location + assert venv.interpreter is None + assert venv.reuse_existing is False + assert venv.venv_backend == "uv" + + venv.create() + + def test_constructor_defaults(make_one): venv, _ = make_one() assert venv.location @@ -417,7 +429,7 @@ def test_create_reuse_stale_venv_environment(make_one): @enable_staleness_check def test_create_reuse_stale_virtualenv_environment(make_one): - venv, location = make_one(reuse_existing=True, venv=True) + venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() # Drop a virtualenv-style pyvenv.cfg into the environment. @@ -442,7 +454,7 @@ def test_create_reuse_stale_virtualenv_environment(make_one): @enable_staleness_check def test_create_reuse_venv_environment(make_one): - venv, location = make_one(reuse_existing=True, venv="venv") + venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() # Place a spurious occurrence of "virtualenv" in the pyvenv.cfg. @@ -516,7 +528,7 @@ def test_create_reuse_python2_environment(make_one): def test_create_venv_backend(make_one): - venv, dir_ = make_one(venv=True) + venv, dir_ = make_one(venv_backend="venv") venv.create()