From fffdfc482a05fff6afb673198ab9db303aa6bd25 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 23 Feb 2024 12:11:18 -0500 Subject: [PATCH 1/7] fix: rebuild env if making an incompatible change Signed-off-by: Henry Schreiner --- nox/virtualenv.py | 36 ++++++++++++++++++++++++-------- tests/test_virtualenv.py | 45 ++++++++++++++++++++-------------------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/nox/virtualenv.py b/nox/virtualenv.py index d11db749..87b39b91 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -35,7 +35,6 @@ ["PIP_RESPECT_VIRTUALENV", "PIP_REQUIRE_VIRTUALENV", "__PYVENV_LAUNCHER__"] ) _SYSTEM = platform.system() -_ENABLE_STALENESS_CHECK = "NOX_ENABLE_STALENESS_CHECK" in os.environ class InterpreterNotFound(OSError): @@ -335,21 +334,17 @@ def __init__( def _clean_location(self) -> bool: """Deletes any existing virtual environment""" if os.path.exists(self.location): - if self.reuse_existing and not _ENABLE_STALENESS_CHECK: - return False if ( self.reuse_existing and self._check_reused_environment_type() and self._check_reused_environment_interpreter() ): return False - else: - shutil.rmtree(self.location) - + shutil.rmtree(self.location) return True def _check_reused_environment_type(self) -> bool: - """Check if reused environment type is the same.""" + """Check if reused environment type is the same or equivalent.""" try: with open(os.path.join(self.location, "pyvenv.cfg")) as fp: parts = (x.partition("=") for x in fp if "=" in x) @@ -364,7 +359,26 @@ def _check_reused_environment_type(self) -> bool: # virtualenv < 20.0 does not create pyvenv.cfg old_env = "virtualenv" - return old_env == self.venv_backend + if os.path.isdir(os.path.join(self.location, "conda-meta")): + old_env = "conda" # Can't detect mamba, but shouldn't matter + + # 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 + + # Switching to "uv" is safe, but not the other direction (no pip) + if old_env in {"virtualenv", "venv"} and self.venv_backend == "uv": + return True + + return False def _check_reused_environment_interpreter(self) -> bool: """Check if reused environment interpreter is the same.""" @@ -384,7 +398,11 @@ def _check_reused_environment_interpreter(self) -> bool: ["python", "-c", program], silent=True, log=False, paths=self.bin_paths ) - return original == created + return ( + os.path.exists(original) + and os.path.exists(created) + and os.path.samefile(original, created) + ) def _read_base_prefix_from_pyvenv_cfg(self) -> str | None: """Return the base-prefix entry from pyvenv.cfg, if present.""" diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index b5cf49a9..0d37c937 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -383,15 +383,6 @@ def test_create_reuse_environment(make_one): assert reused -@pytest.fixture -def _enable_staleness_check(monkeypatch): - monkeypatch.setattr("nox.virtualenv._ENABLE_STALENESS_CHECK", True) - - -enable_staleness_check = pytest.mark.usefixtures("_enable_staleness_check") - - -@enable_staleness_check def test_create_reuse_environment_with_different_interpreter(make_one, monkeypatch): venv, location = make_one(reuse_existing=True) venv.create() @@ -408,37 +399,38 @@ def test_create_reuse_environment_with_different_interpreter(make_one, monkeypat assert not location.join("marker").check() -@enable_staleness_check +@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.") def test_create_reuse_stale_venv_environment(make_one): venv, location = make_one(reuse_existing=True) venv.create() - # Drop a venv-style pyvenv.cfg into the environment. + # Drop a uv-style pyvenv.cfg into the environment. pyvenv_cfg = """\ home = /usr/bin include-system-site-packages = false version = 3.9.6 + uv = 0.1.9 """ location.join("pyvenv.cfg").write(dedent(pyvenv_cfg)) reused = not venv.create() - # The environment is not reused because it does not look like a - # virtualenv-style environment. + # The environment is not reused because it is a uv-style + # environment. assert not reused -@enable_staleness_check +@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.") def test_create_reuse_stale_virtualenv_environment(make_one): venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() - # Drop a virtualenv-style pyvenv.cfg into the environment. + # Drop a uv-style pyvenv.cfg into the environment. pyvenv_cfg = """\ home = /usr implementation = CPython version_info = 3.9.6.final.0 - virtualenv = 20.4.6 + uv = 0.1.9 include-system-site-packages = false base-prefix = /usr base-exec-prefix = /usr @@ -453,7 +445,21 @@ def test_create_reuse_stale_virtualenv_environment(make_one): assert not reused -@enable_staleness_check +@pytest.mark.skipif(not HAS_UV, reason="Missing uv command.") +def test_create_reuse_uv_environment(make_one): + venv, location = make_one(reuse_existing=True, venv_backend="uv") + venv.create() + + # Place a spurious occurrence of "uv" in the pyvenv.cfg. + pyvenv_cfg = location.join("pyvenv.cfg") + pyvenv_cfg.write(pyvenv_cfg.read() + "bogus = uv\n") + + reused = not venv.create() + + # The environment is reused because it looks like a uv environment + assert reused + + def test_create_reuse_venv_environment(make_one): venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() @@ -468,8 +474,6 @@ def test_create_reuse_venv_environment(make_one): assert reused -@enable_staleness_check -@pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") def test_create_reuse_oldstyle_virtualenv_environment(make_one): venv, location = make_one(reuse_existing=True) venv.create() @@ -487,8 +491,6 @@ def test_create_reuse_oldstyle_virtualenv_environment(make_one): assert reused -@enable_staleness_check -@pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") def test_inner_functions_reusing_venv(make_one): venv, location = make_one(reuse_existing=True) venv.create() @@ -510,7 +512,6 @@ def test_inner_functions_reusing_venv(make_one): assert not reused_interpreter -@enable_staleness_check @pytest.mark.skipif( version.parse(VIRTUALENV_VERSION) >= version.parse("20.22.0"), reason="Python 2.7 unsupported for virtualenv>=20.22.0", From 0a6779ff4d05ab573f31dfff21dde47df7b47901 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 23 Feb 2024 12:56:52 -0500 Subject: [PATCH 2/7] tests: restore windows skip --- tests/test_virtualenv.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 0d37c937..3d9d53c3 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -474,6 +474,7 @@ def test_create_reuse_venv_environment(make_one): assert reused +@pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") def test_create_reuse_oldstyle_virtualenv_environment(make_one): venv, location = make_one(reuse_existing=True) venv.create() @@ -491,6 +492,7 @@ def test_create_reuse_oldstyle_virtualenv_environment(make_one): assert reused +@pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") def test_inner_functions_reusing_venv(make_one): venv, location = make_one(reuse_existing=True) venv.create() From b3a4c378423af7b091c1f1d45e0f1b5f794c99ca Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 23 Feb 2024 16:29:52 -0500 Subject: [PATCH 3/7] fix: support conda switching properly Signed-off-by: Henry Schreiner --- nox/virtualenv.py | 21 ++++---- tests/test_virtualenv.py | 105 ++++++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 87b39b91..d1d13e9e 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -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, @@ -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 @@ -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: @@ -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 diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 3d9d53c3..a6a60652 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -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 @@ -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 @@ -119,6 +129,11 @@ def test_process_env_create(): penv.create() +def test_invalid_venv_create(make_one): + with pytest.raises(ValueError): + make_one(venv_backend="invalid") + + def test_condaenv_constructor_defaults(make_conda): venv, _ = make_conda() assert venv.location @@ -133,7 +148,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() @@ -162,7 +177,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() @@ -174,7 +189,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() @@ -188,7 +203,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: @@ -218,13 +233,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() @@ -241,7 +256,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 @@ -399,7 +414,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() @@ -420,7 +435,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() @@ -445,7 +526,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() From 152bb3dd3fac12b39f7565cf34834db75eeeedcf Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 26 Feb 2024 10:04:11 -0500 Subject: [PATCH 4/7] fix: make stale Python check opt-in again Signed-off-by: Henry Schreiner --- nox/virtualenv.py | 12 +++++++++++- tests/test_virtualenv.py | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/nox/virtualenv.py b/nox/virtualenv.py index d1d13e9e..842ade47 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -384,7 +384,17 @@ def _check_reused_environment_type(self) -> bool: return False def _check_reused_environment_interpreter(self) -> bool: - """Check if reused environment interpreter is the same.""" + """ + Check if reused environment interpreter is the same. Currently only checks if + NOX_ENABLE_STALENESS_CHECK is set in the environment. See + + * https://github.com/wntrblm/nox/issues/449#issuecomment-860030890 + * https://github.com/wntrblm/nox/issues/441 + * https://github.com/pypa/virtualenv/issues/2130 + """ + if not os.environ.get("NOX_ENABLE_STALENESS_CHECK", ""): + return True + original = self._read_base_prefix_from_pyvenv_cfg() program = ( "import sys; sys.stdout.write(getattr(sys, 'real_prefix', sys.base_prefix))" diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index a6a60652..dd185d10 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -574,7 +574,8 @@ def test_create_reuse_oldstyle_virtualenv_environment(make_one): @pytest.mark.skipif(IS_WINDOWS, reason="Avoid 'No pyvenv.cfg file' error on Windows.") -def test_inner_functions_reusing_venv(make_one): +def test_inner_functions_reusing_venv(make_one, monkeypatch): + monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") venv, location = make_one(reuse_existing=True) venv.create() From 540cdbdce535d783c773d09864815772c5ab3cd2 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Mon, 26 Feb 2024 11:53:50 -0500 Subject: [PATCH 5/7] tests: increase coverage of check again Signed-off-by: Henry Schreiner --- tests/test_virtualenv.py | 49 +++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index dd185d10..35f06780 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -399,6 +399,9 @@ def test_create_reuse_environment(make_one): def test_create_reuse_environment_with_different_interpreter(make_one, monkeypatch): + # Making the reuse requirement more strict + monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") + venv, location = make_one(reuse_existing=True) venv.create() @@ -430,36 +433,23 @@ def test_create_reuse_stale_venv_environment(make_one): reused = not venv.create() - # The environment is not reused because it is a uv-style - # environment. assert not reused -def test_stale_venv_to_conda_environment(make_one): - venv, location = make_one(reuse_existing=True, venv_backend="virtualenv") - venv.create() +def test_not_stale_virtualenv_environment(make_one, monkeypatch): + # Making the reuse requirement more strict + monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") 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() + reused = not venv.create() - # The environment is not reused because it is now conda style - # environment. - assert not reused + assert reused +@has_conda def test_stale_virtualenv_to_conda_environment(make_one): venv, location = make_one(reuse_existing=True, venv_backend="virtualenv") venv.create() @@ -472,11 +462,12 @@ def test_stale_virtualenv_to_conda_environment(make_one): assert not reused +@has_conda def test_reuse_conda_environment(make_one): - venv, location = make_one(reuse_existing=True, venv_backend="conda") + venv, _ = make_one(reuse_existing=True, venv_backend="conda") venv.create() - venv, location = make_one(reuse_existing=True, venv_backend="conda") + venv, _ = make_one(reuse_existing=True, venv_backend="conda") reused = not venv.create() assert reused @@ -489,20 +480,23 @@ def test_reuse_conda_environment(make_one): ("venv", "virtualenv", True), ("virtualenv", "uv", True), pytest.param("uv", "virtualenv", False, marks=has_uv), + pytest.param("conda", "virtualenv", False, marks=has_conda), ], ) -def test_stale_environment(make_one, frm, to, result): - venv, location = make_one(reuse_existing=True, venv_backend=frm) +def test_stale_environment(make_one, frm, to, result, monkeypatch): + monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") + venv, _ = make_one(reuse_existing=True, venv_backend=frm) venv.create() - venv.venv_backend = to + venv, _ = make_one(reuse_existing=True, venv_backend=to) reused = venv._check_reused_environment_type() assert reused == result @has_uv -def test_create_reuse_stale_virtualenv_environment(make_one): +def test_create_reuse_stale_virtualenv_environment(make_one, monkeypatch): + monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() @@ -541,7 +535,10 @@ def test_create_reuse_uv_environment(make_one): assert reused -def test_create_reuse_venv_environment(make_one): +def test_create_reuse_venv_environment(make_one, monkeypatch): + # Making the reuse requirement more strict + monkeypatch.setenv("NOX_ENABLE_STALENESS_CHECK", "1") + venv, location = make_one(reuse_existing=True, venv_backend="venv") venv.create() From 7882ee1595f83c42fd50e9aeab08288be9699996 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 27 Feb 2024 17:06:30 -0500 Subject: [PATCH 6/7] refactor: unify function Signed-off-by: Henry Schreiner --- nox/virtualenv.py | 45 +++++++++++++++++++--------------------- tests/test_virtualenv.py | 2 +- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/nox/virtualenv.py b/nox/virtualenv.py index 842ade47..1895f841 100644 --- a/nox/virtualenv.py +++ b/nox/virtualenv.py @@ -349,21 +349,27 @@ def _clean_location(self) -> bool: shutil.rmtree(self.location) return True + def _read_pyvenv_cfg(self) -> dict[str, str] | None: + """Read a pyvenv.cfg file into dict, returns None if missing.""" + path = os.path.join(self.location, "pyvenv.cfg") + with contextlib.suppress(FileNotFoundError), open(path) as fp: + parts = (x.partition("=") for x in fp if "=" in x) + return {k.strip(): v.strip() for k, _, v in parts} + return None + def _check_reused_environment_type(self) -> bool: """Check if reused environment type is the same or equivalent.""" - try: - with open(os.path.join(self.location, "pyvenv.cfg")) as fp: - parts = (x.partition("=") for x in fp if "=" in x) - config = {k.strip(): v.strip() for k, _, v in parts} - if "uv" in config or "gourgeist" in config: - old_env = "uv" - elif "virtualenv" in config: - old_env = "virtualenv" - else: - old_env = "venv" - except FileNotFoundError: # pragma: no cover - # virtualenv < 20.0 does not create pyvenv.cfg + + config = self._read_pyvenv_cfg() + # virtualenv < 20.0 does not create pyvenv.cfg + if config is None: old_env = "virtualenv" + elif "uv" in config or "gourgeist" in config: + old_env = "uv" + elif "virtualenv" in config: + old_env = "virtualenv" + else: + old_env = "venv" # Can't detect mamba separately, but shouldn't matter if os.path.isdir(os.path.join(self.location, "conda-meta")): @@ -395,7 +401,9 @@ def _check_reused_environment_interpreter(self) -> bool: if not os.environ.get("NOX_ENABLE_STALENESS_CHECK", ""): return True - original = self._read_base_prefix_from_pyvenv_cfg() + config = self._read_pyvenv_cfg() or {} + original = config.get("base-prefix", None) + program = ( "import sys; sys.stdout.write(getattr(sys, 'real_prefix', sys.base_prefix))" ) @@ -417,17 +425,6 @@ def _check_reused_environment_interpreter(self) -> bool: and os.path.samefile(original, created) ) - def _read_base_prefix_from_pyvenv_cfg(self) -> str | None: - """Return the base-prefix entry from pyvenv.cfg, if present.""" - path = os.path.join(self.location, "pyvenv.cfg") - if os.path.isfile(path): - with open(path) as io: - for line in io: - key, _, value = line.partition("=") - if key.strip() == "base-prefix": - return value.strip() - return None - @property def _resolved_interpreter(self) -> str: """Return the interpreter, appropriately resolved for the platform. diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 35f06780..8e4dc4e1 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -585,7 +585,7 @@ def test_inner_functions_reusing_venv(make_one, monkeypatch): """ location.join("pyvenv.cfg").write(dedent(pyvenv_cfg)) - base_prefix = venv._read_base_prefix_from_pyvenv_cfg() + base_prefix = venv._read_pyvenv_cfg()["base-prefix"] assert base_prefix == "foo" reused_interpreter = venv._check_reused_environment_interpreter() From 61689705bc66467917d0c10c971380f224bdb2d6 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 28 Feb 2024 09:50:59 -0500 Subject: [PATCH 7/7] Update tests/test_virtualenv.py Co-authored-by: Claudio Jolowicz --- tests/test_virtualenv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_virtualenv.py b/tests/test_virtualenv.py index 8e4dc4e1..e028750b 100644 --- a/tests/test_virtualenv.py +++ b/tests/test_virtualenv.py @@ -36,7 +36,7 @@ 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.") +has_conda = pytest.mark.skipif(not HAS_CONDA, reason="Missing conda command.") class TextProcessResult(NamedTuple):