Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a virtualenv backend for using uv #763

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
18 changes: 8 additions & 10 deletions nox/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,12 @@ def install(self, *args: str, **kwargs: Any) -> None:
if "silent" not in kwargs:
kwargs["silent"] = True

self._run("python", "-m", "pip", "install", *args, external="error", **kwargs)
if isinstance(venv, VirtualEnv) and venv.venv_or_virtualenv == "uv":
self._run("uv", "pip", "install", *args, external=True, **kwargs)
else:
self._run(
"python", "-m", "pip", "install", *args, external="error", **kwargs
)

def notify(
self,
Expand Down Expand Up @@ -748,11 +753,12 @@ def _create_venv(self) -> None:
self.func.reuse_venv or self.global_config.reuse_existing_virtualenvs
)

if backend is None or backend == "virtualenv":
if backend is None or backend in ("virtualenv", "venv", "uv"):
self.venv = VirtualEnv(
self.envdir,
interpreter=self.func.python, # type: ignore[arg-type]
reuse_existing=reuse_existing,
venv_backend=backend,
venv_params=self.func.venv_params,
)
elif backend in {"conda", "mamba"}:
Expand All @@ -763,14 +769,6 @@ def _create_venv(self) -> None:
venv_params=self.func.venv_params,
conda_cmd=backend,
)
elif backend == "venv":
self.venv = VirtualEnv(
self.envdir,
interpreter=self.func.python, # type: ignore[arg-type]
reuse_existing=reuse_existing,
venv=True,
venv_params=self.func.venv_params,
)
else:
raise ValueError(
"Expected venv_backend one of ('virtualenv', 'conda', 'mamba',"
Expand Down
14 changes: 11 additions & 3 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,15 @@ def __init__(
interpreter: str | None = None,
reuse_existing: bool = False,
*,
venv: bool = False,
venv_backend: str = "virtualenv",
venv_params: Any = None,
):
self.location_name = location
self.location = os.path.abspath(location)
self.interpreter = interpreter
self._resolved: None | str | InterpreterNotFound = None
self.reuse_existing = reuse_existing
self.venv_or_virtualenv = "venv" if venv else "virtualenv"
self.venv_or_virtualenv = venv_backend
self.venv_params = venv_params or []
super().__init__(env={"VIRTUAL_ENV": self.location})

Expand Down Expand Up @@ -359,7 +359,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_or_virtualenv
# We can't distinguish a uv env from a venv env, so just treat them
# the same.
return old_env == self.venv_or_virtualenv or (
old_env == "venv" and self.venv_or_virtualenv == "uv"
)

def _check_reused_environment_interpreter(self) -> bool:
"""Check if reused environment interpreter is the same."""
Expand Down Expand Up @@ -478,6 +482,10 @@ def create(self) -> bool:
cmd = [sys.executable, "-m", "virtualenv", self.location]
if self.interpreter:
cmd.extend(["-p", self._resolved_interpreter])
elif self.venv_or_virtualenv == "uv":
cmd = ["uv", "venv", self.location]
if self.interpreter:
cmd.extend(["-p", self._resolved_interpreter])
else:
cmd = [self._resolved_interpreter, "-m", "venv", self.location]
cmd.extend(self.venv_params)
Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def cover(session: nox.Session) -> None:
session.run("coverage", "erase")


@nox.session(python="3.9")
@nox.session
def lint(session: nox.Session) -> None:
"""Run pre-commit linting."""
session.install("pre-commit")
Expand Down
32 changes: 32 additions & 0 deletions tests/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_or_virtualenv = "venv"
return nox.sessions.Session(runner=runner), runner

def test_create_tmp(self):
Expand Down Expand Up @@ -629,6 +630,7 @@ def test_install(self):
)
runner.venv = mock.create_autospec(nox.virtualenv.VirtualEnv)
runner.venv.env = {}
runner.venv.venv_or_virtualenv = "venv"

class SessionNoSlots(nox.sessions.Session):
pass
Expand Down Expand Up @@ -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_or_virtualenv = "venv"

class SessionNoSlots(nox.sessions.Session):
pass
Expand Down Expand Up @@ -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_or_virtualenv = "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):
Expand Down
18 changes: 15 additions & 3 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down Expand Up @@ -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_or_virtualenv == "uv"

venv.create()


def test_constructor_defaults(make_one):
venv, _ = make_one()
assert venv.location
Expand Down Expand Up @@ -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.
Expand All @@ -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=True)
venv, location = make_one(reuse_existing=True, venv_backend="venv")
venv.create()

# Place a spurious occurrence of "virtualenv" in the pyvenv.cfg.
Expand Down Expand Up @@ -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()


Expand Down