Skip to content

Commit 4c7a215

Browse files
authored
fix: write out .gitignore/CACHEDIR.TAG to our dir (#1072)
* chore: write out gitignore to our dir Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * Update nox/virtualenv.py * chore: also CACHEDIR.TAG Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * tests: add some tests Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: address feedback Signed-off-by: Henry Schreiner <henryfs@princeton.edu> --------- Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
1 parent 47cb9c3 commit 4c7a215

File tree

3 files changed

+196
-3
lines changed

3 files changed

+196
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.mypy_cache/
22
.vscode/
3+
.DS_Store
34

45
# Byte-compiled / optimized / DLL files
56
__pycache__/

nox/virtualenv.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,36 @@ def pbs_install_python(python_version: str) -> str | None:
242242
HAS_UV, UV, UV_VERSION = find_uv()
243243

244244

245+
def _ensure_gitignore(envdir: Path) -> None:
246+
"""Ensure the shared environment directory has a broad gitignore."""
247+
envdir.mkdir(parents=True, exist_ok=True)
248+
249+
gitignore = envdir.joinpath(".gitignore")
250+
if gitignore.exists():
251+
return
252+
253+
try:
254+
gitignore.write_text("*\n", encoding="utf-8")
255+
except OSError: # pragma: no cover
256+
logger.debug(f"Failed to write {gitignore!s}")
257+
258+
259+
def _ensure_cachedir_tag(envdir: Path) -> None:
260+
"""Ensure the shared environment directory has a CACHEDIR.TAG"""
261+
envdir.mkdir(parents=True, exist_ok=True)
262+
263+
cachedir_tag = envdir.joinpath("CACHEDIR.TAG")
264+
if cachedir_tag.exists():
265+
return
266+
267+
try:
268+
cachedir_tag.write_text(
269+
"Signature: 8a477f597d28d172789f06886806bc55\n", encoding="utf-8"
270+
)
271+
except OSError: # pragma: no cover
272+
logger.debug(f"Failed to write {cachedir_tag!s}")
273+
274+
245275
class InterpreterNotFound(OSError):
246276
def __init__(self, interpreter: str) -> None:
247277
super().__init__(f"Python interpreter {interpreter} not found")
@@ -313,9 +343,11 @@ def _get_env(
313343
if include_outer_env:
314344
computed_env = {**os.environ, **computed_env}
315345
if self.bin_paths:
316-
computed_env["PATH"] = os.pathsep.join(
317-
[*self.bin_paths, computed_env.get("PATH") or ""]
318-
)
346+
path_parts = [*self.bin_paths]
347+
prior_path = computed_env.get("PATH")
348+
if prior_path:
349+
path_parts.append(prior_path)
350+
computed_env["PATH"] = os.pathsep.join(path_parts)
319351
return computed_env
320352

321353

@@ -494,6 +526,10 @@ def bin_paths(self) -> list[str]:
494526

495527
def create(self) -> bool:
496528
"""Create the conda env."""
529+
nox_dir = Path(self.location).parent
530+
_ensure_gitignore(nox_dir)
531+
_ensure_cachedir_tag(nox_dir)
532+
497533
if not self._clean_location():
498534
logger.debug(f"Reusing existing conda env at {self.location_name}.")
499535

@@ -797,6 +833,10 @@ def bin_paths(self) -> list[str]:
797833

798834
def create(self) -> bool:
799835
"""Create the virtualenv or venv."""
836+
nox_dir = Path(self.location).parent
837+
_ensure_gitignore(nox_dir)
838+
_ensure_cachedir_tag(nox_dir)
839+
800840
if not self._clean_location():
801841
logger.debug(
802842
f"Reusing existing virtual environment at {self.location_name}."

tests/test_virtualenv.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,64 @@ def test_process_env_create() -> None:
142142
nox.virtualenv.ProcessEnv() # type: ignore[abstract]
143143

144144

145+
def test_process_env_get_env_with_bin_paths() -> None:
146+
penv = nox.virtualenv.PassthroughEnv(bin_paths=["/test/bin"])
147+
env = penv._get_env({})
148+
path = env.get("PATH")
149+
assert path
150+
assert "/test/bin" in path
151+
152+
153+
def test_process_env_get_env_exclude_outer() -> None:
154+
penv = nox.virtualenv.PassthroughEnv(bin_paths=["/test/bin"], env={"TEST": "value"})
155+
env = penv._get_env({}, include_outer_env=False)
156+
assert env["TEST"] == "value"
157+
assert env["PATH"] == "/test/bin"
158+
159+
160+
def test_ensure_gitignore_creates_file(tmp_path: Path) -> None:
161+
envdir = tmp_path.joinpath(".nox")
162+
location = envdir.joinpath("session")
163+
164+
nox.virtualenv._ensure_gitignore(location.parent)
165+
166+
assert envdir.joinpath(".gitignore").read_text(encoding="utf-8") == "*\n"
167+
168+
169+
def test_ensure_cachedir_tag_creates_file(tmp_path: Path) -> None:
170+
envdir = tmp_path.joinpath(".nox")
171+
location = envdir.joinpath("session")
172+
173+
nox.virtualenv._ensure_cachedir_tag(location.parent)
174+
175+
assert (
176+
envdir.joinpath("CACHEDIR.TAG").read_text(encoding="utf-8")
177+
== "Signature: 8a477f597d28d172789f06886806bc55\n"
178+
)
179+
180+
181+
def test_ensure_parent_gitignore_keeps_existing_file(tmp_path: Path) -> None:
182+
envdir = tmp_path.joinpath(".nox")
183+
envdir.mkdir()
184+
gitignore = envdir.joinpath(".gitignore")
185+
gitignore.write_text("!keep\n", encoding="utf-8")
186+
187+
nox.virtualenv._ensure_gitignore(envdir)
188+
189+
assert gitignore.read_text(encoding="utf-8") == "!keep\n"
190+
191+
192+
def test_ensure_parent_cachedir_tag_keeps_existing_file(tmp_path: Path) -> None:
193+
envdir = tmp_path.joinpath(".nox")
194+
envdir.mkdir()
195+
cachedir_tag = envdir.joinpath("CACHEDIR.TAG")
196+
cachedir_tag.write_text("!keep\n", encoding="utf-8")
197+
198+
nox.virtualenv._ensure_cachedir_tag(envdir)
199+
200+
assert cachedir_tag.read_text(encoding="utf-8") == "!keep\n"
201+
202+
145203
def test_invalid_venv_create(
146204
make_one: Callable[
147205
..., tuple[nox.virtualenv.VirtualEnv | nox.virtualenv.ProcessEnv, Path]
@@ -151,6 +209,100 @@ def test_invalid_venv_create(
151209
make_one(venv_backend="invalid")
152210

153211

212+
def test_get_virtualenv_invalid_backend(
213+
tmp_path: Path,
214+
) -> None:
215+
with pytest.raises(ValueError, match="Expected venv_backend one of"):
216+
nox.virtualenv.get_virtualenv(
217+
"invalid",
218+
download_python="auto",
219+
envdir=str(tmp_path),
220+
reuse_existing=False,
221+
)
222+
223+
224+
def test_get_virtualenv_fallback_to_available_backend(
225+
tmp_path: Path,
226+
monkeypatch: pytest.MonkeyPatch,
227+
) -> None:
228+
# Force optional backends to be unavailable so fallback behavior is deterministic.
229+
monkeypatch.setattr(
230+
nox.virtualenv,
231+
"OPTIONAL_VENVS",
232+
{"conda": False, "mamba": False, "micromamba": False, "uv": False},
233+
)
234+
venv = nox.virtualenv.get_virtualenv(
235+
"conda",
236+
"mamba",
237+
"venv",
238+
download_python="auto",
239+
envdir=str(tmp_path),
240+
reuse_existing=False,
241+
)
242+
assert isinstance(venv, nox.virtualenv.VirtualEnv)
243+
assert venv.venv_backend == "venv"
244+
245+
246+
def test_get_virtualenv_no_backends_available(
247+
tmp_path: Path,
248+
monkeypatch: pytest.MonkeyPatch,
249+
) -> None:
250+
# Simulate no optional backends present.
251+
monkeypatch.setattr(
252+
nox.virtualenv,
253+
"OPTIONAL_VENVS",
254+
{"conda": False, "mamba": False, "micromamba": False, "uv": False},
255+
)
256+
with pytest.raises(ValueError, match="No backends present"):
257+
nox.virtualenv.get_virtualenv(
258+
"conda",
259+
"mamba",
260+
download_python="auto",
261+
envdir=str(tmp_path),
262+
reuse_existing=False,
263+
)
264+
265+
266+
def test_get_virtualenv_none_backend(
267+
tmp_path: Path,
268+
) -> None:
269+
venv = nox.virtualenv.get_virtualenv(
270+
"none",
271+
download_python="auto",
272+
envdir=str(tmp_path),
273+
reuse_existing=False,
274+
)
275+
assert isinstance(venv, nox.virtualenv.ProcessEnv)
276+
277+
278+
def test_get_virtualenv_interpreter_false(
279+
tmp_path: Path,
280+
) -> None:
281+
venv = nox.virtualenv.get_virtualenv(
282+
"venv",
283+
download_python="auto",
284+
envdir=str(tmp_path),
285+
reuse_existing=False,
286+
interpreter=False,
287+
)
288+
assert isinstance(venv, nox.virtualenv.ProcessEnv)
289+
290+
291+
def test_get_virtualenv_non_optional_fallback(
292+
tmp_path: Path,
293+
) -> None:
294+
with pytest.raises(
295+
ValueError, match=r"Only optional backends.*may have a fallback"
296+
):
297+
nox.virtualenv.get_virtualenv(
298+
"venv",
299+
"uv",
300+
download_python="auto",
301+
envdir=str(tmp_path),
302+
reuse_existing=False,
303+
)
304+
305+
154306
def test_condaenv_constructor_defaults(
155307
make_conda: Callable[..., tuple[CondaEnv, Path]],
156308
) -> None:

0 commit comments

Comments
 (0)