Skip to content

Commit

Permalink
feat: support fallback for nox/mamba/conda
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii committed Feb 29, 2024
1 parent 86d7895 commit e43784f
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ Note that using this option does not change the backend for sessions where ``ven
name from the install process like pip does if the name is omitted. Editable
installs do not require a name.

Backends that could be missing (``uv``, ``conda``, and ``mamba``) can have a fallback using ``|``, such as ``uv|virtualenv`` or ``mamba|conda``. This will use the first item that is available on the users system.

.. _opt-force-venv-backend:

Expand Down
25 changes: 20 additions & 5 deletions nox/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,15 +760,30 @@ def envdir(self) -> str:
def _create_venv(self) -> None:
reuse_existing = self.reuse_existing_venv()

backend = (
backends = (
self.global_config.force_venv_backend
or self.func.venv_backend
or self.global_config.default_venv_backend
or "virtualenv"
)

if backend not in nox.virtualenv.ALL_VENVS:
msg = f"Expected venv_backend one of {list(nox.virtualenv.ALL_VENVS)!r}, but got {backend!r}."
).split("|")

# Support fallback backends
for bk in backends:
if bk not in nox.virtualenv.ALL_VENVS:
msg = f"Expected venv_backend one of {list(nox.virtualenv.ALL_VENVS)!r}, but got {bk!r}."
raise ValueError(msg)

for bk in backends[:-1]:
if bk not in nox.virtualenv.OPTIONAL_VENVS:
msg = f"Only optional backends ({list(nox.virtualenv.OPTIONAL_VENVS)!r}) may have a fallback, {bk!r} is not optional."
raise ValueError(msg)

for bk in backends:
if nox.virtualenv.OPTIONAL_VENVS.get(bk, True):
backend = bk
break
else:
msg = f"No backends present, looked for {backends!r}."
raise ValueError(msg)

if backend == "none" or self.func.python is False:
Expand Down
23 changes: 21 additions & 2 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import annotations

import abc
import contextlib
import functools
import os
Expand Down Expand Up @@ -44,7 +45,7 @@ def __init__(self, interpreter: str) -> None:
self.interpreter = interpreter


class ProcessEnv:
class ProcessEnv(abc.ABC):
"""An environment with a 'bin' directory and a set of 'env' vars."""

location: str
Expand Down Expand Up @@ -85,8 +86,12 @@ def bin(self) -> str:
raise ValueError("The environment does not have a bin directory.")
return paths[0]

@abc.abstractmethod
def create(self) -> bool:
raise NotImplementedError("ProcessEnv.create should be overwritten in subclass")
"""Create a new environment.
Returns True if the environment is new, and False if it was reused.
"""


def locate_via_py(version: str) -> str | None:
Expand Down Expand Up @@ -170,6 +175,11 @@ def is_offline() -> bool:
"""As of now this is only used in conda_install"""
return CondaEnv.is_offline() # pragma: no cover

def create(self) -> bool:
"""Does nothing, since this is an existing environment. Always returns
False since it's always reused."""
return False


class CondaEnv(ProcessEnv):
"""Conda environment management class.
Expand Down Expand Up @@ -543,3 +553,12 @@ def create(self) -> bool:
"uv": functools.partial(VirtualEnv, venv_backend="uv"),
"none": PassthroughEnv,
}

# Any environment in this dict could be missing, and is only available if the
# value is True. If an environment is always available, it should not be in this
# dict. "virtualenv" is not considered optional since it's a dependency of nox.
OPTIONAL_VENVS = {
"conda": shutil.which("conda") is not None,
"mamba": shutil.which("mamba") is not None,
"uv": shutil.which("uv") is not None,
}
42 changes: 40 additions & 2 deletions tests/test_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def test_run_external_not_a_virtualenv(self):
# Non-virtualenv sessions should always allow external programs.
session, runner = self.make_session_and_runner()

runner.venv = nox.virtualenv.ProcessEnv()
runner.venv = nox.virtualenv.PassthroughEnv()

with mock.patch("nox.command.run", autospec=True) as run:
session.run(sys.executable, "--version")
Expand Down Expand Up @@ -402,7 +402,7 @@ def test_run_shutdown_process_timeouts(
):
session, runner = self.make_session_and_runner()

runner.venv = nox.virtualenv.ProcessEnv()
runner.venv = nox.virtualenv.PassthroughEnv()

subp_popen_instance = mock.Mock()
subp_popen_instance.communicate.side_effect = KeyboardInterrupt()
Expand Down Expand Up @@ -963,6 +963,44 @@ def test__create_venv_unexpected_venv_backend(self):
with pytest.raises(ValueError, match="venv_backend"):
runner._create_venv()

@pytest.mark.parametrize(
"venv_backend",
["uv|virtualenv", "conda|virtualenv", "mamba|conda|venv"],
)
def test_fallback_venv(self, venv_backend, monkeypatch):
runner = self.make_runner()
runner.func.venv_backend = venv_backend
monkeypatch.setattr(
nox.virtualenv,
"OPTIONAL_VENVS",
{"uv": False, "conda": False, "mamba": False},
)
with mock.patch("nox.virtualenv.VirtualEnv.create", autospec=True):
runner._create_venv()
assert runner.venv.venv_backend == venv_backend.split("|")[-1]

@pytest.mark.parametrize(
"venv_backend",
[
"uv|virtualenv|unknown",
"conda|unknown|virtualenv",
"virtualenv|venv",
"conda|mamba",
],
)
def test_invalid_fallback_venv(self, venv_backend, monkeypatch):
runner = self.make_runner()
runner.func.venv_backend = venv_backend
monkeypatch.setattr(
nox.virtualenv,
"OPTIONAL_VENVS",
{"uv": False, "conda": False, "mamba": False},
)
with mock.patch(
"nox.virtualenv.VirtualEnv.create", autospec=True
), pytest.raises(ValueError):
runner._create_venv()

@pytest.mark.parametrize(
("reuse_venv", "reuse_venv_func", "should_reuse"),
[
Expand Down
8 changes: 4 additions & 4 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,22 @@ def special_run(cmd, *args, **kwargs):


def test_process_env_constructor():
penv = nox.virtualenv.ProcessEnv()
penv = nox.virtualenv.PassthroughEnv()
assert not penv.bin_paths
with pytest.raises(
ValueError, match=r"^The environment does not have a bin directory\.$"
):
print(penv.bin)

penv = nox.virtualenv.ProcessEnv(env={"SIGIL": "123"})
penv = nox.virtualenv.PassthroughEnv(env={"SIGIL": "123"})
assert penv.env["SIGIL"] == "123"

penv = nox.virtualenv.ProcessEnv(bin_paths=["/bin"])
penv = nox.virtualenv.PassthroughEnv(bin_paths=["/bin"])
assert penv.bin == "/bin"


def test_process_env_create():
penv = nox.virtualenv.ProcessEnv()
penv = nox.virtualenv.PassthroughEnv()
with pytest.raises(NotImplementedError):
penv.create()

Expand Down

0 comments on commit e43784f

Please sign in to comment.