Skip to content

Commit

Permalink
fix: support conda switching properly
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
  • Loading branch information
henryiii committed Feb 23, 2024
1 parent 0a6779f commit b4cdfe3
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 21 deletions.
21 changes: 12 additions & 9 deletions nox/virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,12 @@ def __init__(

def _clean_location(self) -> bool:
"""Deletes existing conda environment"""
is_conda = os.path.isdir(os.path.join(self.location, "conda-meta"))
if os.path.exists(self.location):
if self.reuse_existing:
if self.reuse_existing and is_conda:
return False
if not is_conda:
shutil.rmtree(self.location)
else:
cmd = [
self.conda_cmd,
Expand All @@ -226,9 +229,9 @@ def _clean_location(self) -> bool:
"--all",
]
nox.command.run(cmd, silent=True, log=False)
# Make sure that location is clean
with contextlib.suppress(FileNotFoundError):
shutil.rmtree(self.location)
# Make sure that location is clean
with contextlib.suppress(FileNotFoundError):
shutil.rmtree(self.location)

return True

Expand Down Expand Up @@ -329,6 +332,9 @@ def __init__(
self.reuse_existing = reuse_existing
self.venv_backend = venv_backend
self.venv_params = venv_params or []
if venv_backend not in {"virtualenv", "venv", "uv"}:
msg = f"venv_backend {venv_backend} not recognized"
raise ValueError(msg)
super().__init__(env={"VIRTUAL_ENV": self.location})

def _clean_location(self) -> bool:
Expand Down Expand Up @@ -359,17 +365,14 @@ def _check_reused_environment_type(self) -> bool:
# virtualenv < 20.0 does not create pyvenv.cfg
old_env = "virtualenv"

# Can't detect mamba separately, but shouldn't matter
if os.path.isdir(os.path.join(self.location, "conda-meta")):
old_env = "conda" # Can't detect mamba, but shouldn't matter
return False

# Matching is always true
if old_env == self.venv_backend:
return True

# conda family
if {old_env, self.venv_backend} <= {"conda", "mamba"}:
return True

# venv family with pip installed
if {old_env, self.venv_backend} <= {"virtualenv", "venv"}:
return True
Expand Down
100 changes: 88 additions & 12 deletions tests/test_virtualenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
RAISE_ERROR = "RAISE_ERROR"
VIRTUALENV_VERSION = virtualenv.__version__

has_uv = pytest.mark.skipif(not HAS_UV, reason="Missing uv command.")
has_conda = pytest.mark.skipif(not HAS_UV, reason="Missing conda command.")


class TextProcessResult(NamedTuple):
stdout: str
Expand All @@ -43,9 +46,16 @@ class TextProcessResult(NamedTuple):

@pytest.fixture
def make_one(tmpdir):
def factory(*args, **kwargs):
def factory(*args, venv_backend: str = "virtualenv", **kwargs):
location = tmpdir.join("venv")
venv = nox.virtualenv.VirtualEnv(location.strpath, *args, **kwargs)
if venv_backend in {"mamba", "conda"}:
venv = nox.virtualenv.CondaEnv(
location.strpath, *args, conda_cmd=venv_backend, **kwargs
)
else:
venv = nox.virtualenv.VirtualEnv(
location.strpath, *args, venv_backend=venv_backend, **kwargs
)
return (venv, location)

return factory
Expand Down Expand Up @@ -133,7 +143,7 @@ def test_condaenv_constructor_explicit(make_conda):
assert venv.reuse_existing is True


@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
@has_conda
def test_condaenv_create(make_conda):
venv, dir_ = make_conda()
venv.create()
Expand Down Expand Up @@ -162,7 +172,7 @@ def test_condaenv_create(make_conda):
assert venv._reused


@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
@has_conda
def test_condaenv_create_with_params(make_conda):
venv, dir_ = make_conda(venv_params=["--verbose"])
venv.create()
Expand All @@ -174,7 +184,7 @@ def test_condaenv_create_with_params(make_conda):
assert dir_.join("bin", "pip").check()


@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
@has_conda
def test_condaenv_create_interpreter(make_conda):
venv, dir_ = make_conda(interpreter="3.7")
venv.create()
Expand All @@ -188,7 +198,7 @@ def test_condaenv_create_interpreter(make_conda):
assert dir_.join("bin", "python3.7").check()


@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
@has_conda
def test_conda_env_create_verbose(make_conda):
venv, dir_ = make_conda()
with mock.patch("nox.virtualenv.nox.command.run") as mock_run:
Expand Down Expand Up @@ -218,13 +228,13 @@ def test_condaenv_bin_windows(make_conda):
] == venv.bin_paths


@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
@has_conda
def test_condaenv_(make_conda):
venv, dir_ = make_conda()
assert not venv.is_offline()


@pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.")
@has_conda
def test_condaenv_detection(make_conda):
venv, dir_ = make_conda()
venv.create()
Expand All @@ -241,7 +251,7 @@ 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.")
@has_uv
def test_uv_creation(make_one):
venv, _ = make_one(venv_backend="uv")
assert venv.location
Expand Down Expand Up @@ -399,7 +409,7 @@ def test_create_reuse_environment_with_different_interpreter(make_one, monkeypat
assert not location.join("marker").check()


@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.")
@has_uv
def test_create_reuse_stale_venv_environment(make_one):
venv, location = make_one(reuse_existing=True)
venv.create()
Expand All @@ -420,7 +430,73 @@ def test_create_reuse_stale_venv_environment(make_one):
assert not reused


@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.")
def test_stale_venv_to_conda_environment(make_one):
venv, location = make_one(reuse_existing=True, venv_backend="virtualenv")
venv.create()

venv, location = make_one(reuse_existing=True, venv_backend="virtualenv")
reused = venv.create()

# The environment is not reused because it is now venv style
# environment.
assert not reused


@has_conda
def test_stale_conda_to_venv_environment(make_one):
venv, location = make_one(reuse_existing=True, venv_backend="conda")
venv.create()

venv, location = make_one(reuse_existing=True, venv_backend="virtualenv")
reused = venv._check_reused_environment_type()

# The environment is not reused because it is now conda style
# environment.
assert not reused


def test_stale_virtualenv_to_conda_environment(make_one):
venv, location = make_one(reuse_existing=True, venv_backend="virtualenv")
venv.create()

venv, location = make_one(reuse_existing=True, venv_backend="conda")
reused = not venv.create()

# The environment is not reused because it is now conda style
# environment.
assert not reused


def test_reuse_conda_environment(make_one):
venv, location = make_one(reuse_existing=True, venv_backend="conda")
venv.create()

venv, location = make_one(reuse_existing=True, venv_backend="conda")
reused = not venv.create()

assert reused


@pytest.mark.parametrize(
("frm", "to", "result"),
[
("virtualenv", "venv", True),
("venv", "virtualenv", True),
("virtualenv", "uv", True),
pytest.param("uv", "virtualenv", False, marks=has_uv),
],
)
def test_stale_environment(make_one, frm, to, result):
venv, location = make_one(reuse_existing=True, venv_backend=frm)
venv.create()

venv.venv_backend = to
reused = venv._check_reused_environment_type()

assert reused == result


@has_uv
def test_create_reuse_stale_virtualenv_environment(make_one):
venv, location = make_one(reuse_existing=True, venv_backend="venv")
venv.create()
Expand All @@ -445,7 +521,7 @@ def test_create_reuse_stale_virtualenv_environment(make_one):
assert not reused


@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.")
@has_uv
def test_create_reuse_uv_environment(make_one):
venv, location = make_one(reuse_existing=True, venv_backend="uv")
venv.create()
Expand Down

0 comments on commit b4cdfe3

Please sign in to comment.