Skip to content

Commit e0f64e6

Browse files
authored
fix: recreate if broken symlinks are present (#1078)
* fix: recreate if broken symlinks are present Signed-off-by: Henry Schreiner <[email protected]> * tests: ignore encoding warning from conda Signed-off-by: Henry Schreiner <[email protected]> * tests: avoid warning when calling conda instead Signed-off-by: Henry Schreiner <[email protected]> * tests: try different mechanism Signed-off-by: Henry Schreiner <[email protected]> * Remove reset_python_warn_env fixture Removed the reset_python_warn_env fixture to avoid issues with conda tests on Windows. --------- Signed-off-by: Henry Schreiner <[email protected]>
1 parent bf90952 commit e0f64e6

2 files changed

Lines changed: 47 additions & 0 deletions

File tree

nox/virtualenv.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ def _clean_location(self) -> bool:
603603
if (
604604
self.reuse_existing
605605
and self._check_reused_environment_type()
606+
and self._check_reused_environment_links()
606607
and self._check_reused_environment_interpreter()
607608
):
608609
return False
@@ -611,6 +612,23 @@ def _clean_location(self) -> bool:
611612
shutil.rmtree(self.location, ignore_errors=True)
612613
return True
613614

615+
def _check_reused_environment_links(self) -> bool:
616+
"""Check that interpreter links in the reused environment are not broken."""
617+
bin_dir = self.bin
618+
names = (
619+
["python.exe"]
620+
if _PLATFORM.startswith("win") and not _IS_MINGW
621+
else ["python", "python3"]
622+
)
623+
624+
for name in names:
625+
path = os.path.join(bin_dir, name)
626+
# ``lexists`` catches broken symlinks, ``exists`` verifies the target.
627+
if os.path.lexists(path) and not os.path.exists(path):
628+
return False
629+
630+
return True
631+
614632
def _read_pyvenv_cfg(self) -> dict[str, str] | None:
615633
"""Read a pyvenv.cfg file into dict, returns None if missing."""
616634
path = os.path.join(self.location, "pyvenv.cfg")

tests/test_virtualenv.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,35 @@ def test_create_reuse_environment_with_different_interpreter(
539539
assert not location.joinpath("marker").exists()
540540

541541

542+
@pytest.mark.skipif(IS_WINDOWS, reason="Uses POSIX symlinks.")
543+
@pytest.mark.parametrize(
544+
"venv_backend",
545+
[
546+
"venv",
547+
"virtualenv",
548+
pytest.param("uv", marks=has_uv),
549+
],
550+
)
551+
def test_create_reuse_environment_with_broken_interpreter_symlink(
552+
make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Path]],
553+
venv_backend: str,
554+
) -> None:
555+
venv, location = make_one(reuse_existing=True, venv_backend=venv_backend)
556+
venv.create()
557+
558+
marker = location.joinpath("marker")
559+
marker.touch()
560+
561+
python = location.joinpath("bin", "python")
562+
python.unlink()
563+
python.symlink_to("/definitely/missing/python")
564+
565+
reused = not venv.create()
566+
567+
assert not reused
568+
assert not marker.exists()
569+
570+
542571
@has_uv
543572
def test_create_reuse_stale_venv_environment(
544573
make_one: Callable[..., tuple[VirtualEnv | ProcessEnv, Path]],

0 commit comments

Comments
 (0)