3535 ["PIP_RESPECT_VIRTUALENV" , "PIP_REQUIRE_VIRTUALENV" , "__PYVENV_LAUNCHER__" ]
3636)
3737_SYSTEM = platform .system ()
38- _ENABLE_STALENESS_CHECK = "NOX_ENABLE_STALENESS_CHECK" in os .environ
3938
4039
4140class InterpreterNotFound (OSError ):
@@ -214,9 +213,12 @@ def __init__(
214213
215214 def _clean_location (self ) -> bool :
216215 """Deletes existing conda environment"""
216+ is_conda = os .path .isdir (os .path .join (self .location , "conda-meta" ))
217217 if os .path .exists (self .location ):
218- if self .reuse_existing :
218+ if self .reuse_existing and is_conda :
219219 return False
220+ if not is_conda :
221+ shutil .rmtree (self .location )
220222 else :
221223 cmd = [
222224 self .conda_cmd ,
@@ -227,9 +229,9 @@ def _clean_location(self) -> bool:
227229 "--all" ,
228230 ]
229231 nox .command .run (cmd , silent = True , log = False )
230- # Make sure that location is clean
231- with contextlib .suppress (FileNotFoundError ):
232- shutil .rmtree (self .location )
232+ # Make sure that location is clean
233+ with contextlib .suppress (FileNotFoundError ):
234+ shutil .rmtree (self .location )
233235
234236 return True
235237
@@ -330,45 +332,78 @@ def __init__(
330332 self .reuse_existing = reuse_existing
331333 self .venv_backend = venv_backend
332334 self .venv_params = venv_params or []
335+ if venv_backend not in {"virtualenv" , "venv" , "uv" }:
336+ msg = f"venv_backend { venv_backend } not recognized"
337+ raise ValueError (msg )
333338 super ().__init__ (env = {"VIRTUAL_ENV" : self .location })
334339
335340 def _clean_location (self ) -> bool :
336341 """Deletes any existing virtual environment"""
337342 if os .path .exists (self .location ):
338- if self .reuse_existing and not _ENABLE_STALENESS_CHECK :
339- return False
340343 if (
341344 self .reuse_existing
342345 and self ._check_reused_environment_type ()
343346 and self ._check_reused_environment_interpreter ()
344347 ):
345348 return False
346- else :
347- shutil .rmtree (self .location )
348-
349+ shutil .rmtree (self .location )
349350 return True
350351
352+ def _read_pyvenv_cfg (self ) -> dict [str , str ] | None :
353+ """Read a pyvenv.cfg file into dict, returns None if missing."""
354+ path = os .path .join (self .location , "pyvenv.cfg" )
355+ with contextlib .suppress (FileNotFoundError ), open (path ) as fp :
356+ parts = (x .partition ("=" ) for x in fp if "=" in x )
357+ return {k .strip (): v .strip () for k , _ , v in parts }
358+ return None
359+
351360 def _check_reused_environment_type (self ) -> bool :
352- """Check if reused environment type is the same."""
353- try :
354- with open (os .path .join (self .location , "pyvenv.cfg" )) as fp :
355- parts = (x .partition ("=" ) for x in fp if "=" in x )
356- config = {k .strip (): v .strip () for k , _ , v in parts }
357- if "uv" in config or "gourgeist" in config :
358- old_env = "uv"
359- elif "virtualenv" in config :
360- old_env = "virtualenv"
361- else :
362- old_env = "venv"
363- except FileNotFoundError : # pragma: no cover
364- # virtualenv < 20.0 does not create pyvenv.cfg
361+ """Check if reused environment type is the same or equivalent."""
362+
363+ config = self ._read_pyvenv_cfg ()
364+ # virtualenv < 20.0 does not create pyvenv.cfg
365+ if config is None :
365366 old_env = "virtualenv"
367+ elif "uv" in config or "gourgeist" in config :
368+ old_env = "uv"
369+ elif "virtualenv" in config :
370+ old_env = "virtualenv"
371+ else :
372+ old_env = "venv"
373+
374+ # Can't detect mamba separately, but shouldn't matter
375+ if os .path .isdir (os .path .join (self .location , "conda-meta" )):
376+ return False
366377
367- return old_env == self .venv_backend
378+ # Matching is always true
379+ if old_env == self .venv_backend :
380+ return True
381+
382+ # venv family with pip installed
383+ if {old_env , self .venv_backend } <= {"virtualenv" , "venv" }:
384+ return True
385+
386+ # Switching to "uv" is safe, but not the other direction (no pip)
387+ if old_env in {"virtualenv" , "venv" } and self .venv_backend == "uv" :
388+ return True
389+
390+ return False
368391
369392 def _check_reused_environment_interpreter (self ) -> bool :
370- """Check if reused environment interpreter is the same."""
371- original = self ._read_base_prefix_from_pyvenv_cfg ()
393+ """
394+ Check if reused environment interpreter is the same. Currently only checks if
395+ NOX_ENABLE_STALENESS_CHECK is set in the environment. See
396+
397+ * https://github.com/wntrblm/nox/issues/449#issuecomment-860030890
398+ * https://github.com/wntrblm/nox/issues/441
399+ * https://github.com/pypa/virtualenv/issues/2130
400+ """
401+ if not os .environ .get ("NOX_ENABLE_STALENESS_CHECK" , "" ):
402+ return True
403+
404+ config = self ._read_pyvenv_cfg () or {}
405+ original = config .get ("base-prefix" , None )
406+
372407 program = (
373408 "import sys; sys.stdout.write(getattr(sys, 'real_prefix', sys.base_prefix))"
374409 )
@@ -384,18 +419,11 @@ def _check_reused_environment_interpreter(self) -> bool:
384419 ["python" , "-c" , program ], silent = True , log = False , paths = self .bin_paths
385420 )
386421
387- return original == created
388-
389- def _read_base_prefix_from_pyvenv_cfg (self ) -> str | None :
390- """Return the base-prefix entry from pyvenv.cfg, if present."""
391- path = os .path .join (self .location , "pyvenv.cfg" )
392- if os .path .isfile (path ):
393- with open (path ) as io :
394- for line in io :
395- key , _ , value = line .partition ("=" )
396- if key .strip () == "base-prefix" :
397- return value .strip ()
398- return None
422+ return (
423+ os .path .exists (original )
424+ and os .path .exists (created )
425+ and os .path .samefile (original , created )
426+ )
399427
400428 @property
401429 def _resolved_interpreter (self ) -> str :
0 commit comments