From 2a29a1b09b7502850ac123e9c51573ecd67a567e Mon Sep 17 00:00:00 2001 From: Neil Ramsay Date: Tue, 26 Nov 2024 17:31:10 +1300 Subject: [PATCH] feat: Write CACHEDIR.TAG file (#2805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Bernát Gábor Co-authored-by: Neil Ramsay <2934552+neilramsay@users.noreply.github.com> Co-authored-by: Bernát Gábor --- docs/changelog/2803.feature.rst | 1 + src/virtualenv/create/creator.py | 14 +++++++++++ tests/integration/test_cachedir_tag.py | 32 ++++++++++++++++++++++++++ tests/unit/create/test_creator.py | 28 ++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 docs/changelog/2803.feature.rst create mode 100644 tests/integration/test_cachedir_tag.py diff --git a/docs/changelog/2803.feature.rst b/docs/changelog/2803.feature.rst new file mode 100644 index 000000000..c4bc00c2a --- /dev/null +++ b/docs/changelog/2803.feature.rst @@ -0,0 +1 @@ +Write CACHEDIR.TAG file on creation - by "user:`neilramsay`. diff --git a/src/virtualenv/create/creator.py b/src/virtualenv/create/creator.py index 26cce3299..1e577ada5 100644 --- a/src/virtualenv/create/creator.py +++ b/src/virtualenv/create/creator.py @@ -4,6 +4,7 @@ import logging import os import sys +import textwrap from abc import ABC, abstractmethod from argparse import ArgumentTypeError from ast import literal_eval @@ -157,10 +158,23 @@ def run(self): LOGGER.debug("delete %s", self.dest) safe_delete(self.dest) self.create() + self.add_cachedir_tag() self.set_pyenv_cfg() if not self.no_vcs_ignore: self.setup_ignore_vcs() + def add_cachedir_tag(self): + """Generate a file indicating that this is not meant to be backed up.""" + cachedir_tag_file = self.dest / "CACHEDIR.TAG" + if not cachedir_tag_file.exists(): + cachedir_tag_text = textwrap.dedent(""" + Signature: 8a477f597d28d172789f06886806bc55 + # This file is a cache directory tag created by Python virtualenv. + # For information about cache directory tags, see: + # https://bford.info/cachedir/ + """).strip() + cachedir_tag_file.write_text(cachedir_tag_text, encoding="utf-8") + def set_pyenv_cfg(self): self.pyenv_cfg.content = OrderedDict() self.pyenv_cfg["home"] = os.path.dirname(os.path.abspath(self.interpreter.system_executable)) diff --git a/tests/integration/test_cachedir_tag.py b/tests/integration/test_cachedir_tag.py new file mode 100644 index 000000000..324bb036f --- /dev/null +++ b/tests/integration/test_cachedir_tag.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import shutil +import sys +from subprocess import check_output, run +from typing import TYPE_CHECKING + +import pytest + +from virtualenv import cli_run + +if TYPE_CHECKING: + from pathlib import Path + +# gtar => gnu-tar on macOS +TAR = next((target for target in ("gtar", "tar") if shutil.which(target)), None) + + +def compatible_is_tar_present() -> bool: + return TAR and "--exclude-caches" in check_output(args=[TAR, "--help"], text=True) + + +@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not have tar") +@pytest.mark.skipif(not compatible_is_tar_present(), reason="Compatible tar is not installed") +def test_cachedir_tag_ignored_by_tag(tmp_path: Path) -> None: + venv = tmp_path / ".venv" + cli_run(["--activators", "", "--without-pip", str(venv)]) + + args = [TAR, "--create", "--file", "/dev/null", "--exclude-caches", "--verbose", venv.name] + tar_result = run(args=args, capture_output=True, text=True, cwd=tmp_path) + assert tar_result.stdout == ".venv/\n.venv/CACHEDIR.TAG\n" + assert tar_result.stderr == f"{TAR}: .venv/: contains a cache directory tag CACHEDIR.TAG; contents not dumped\n" diff --git a/tests/unit/create/test_creator.py b/tests/unit/create/test_creator.py index ff8f08577..2e4078d1c 100644 --- a/tests/unit/create/test_creator.py +++ b/tests/unit/create/test_creator.py @@ -11,6 +11,7 @@ import stat import subprocess import sys +import textwrap import zipfile from collections import OrderedDict from itertools import product @@ -223,6 +224,33 @@ def list_to_str(iterable): assert git_ignore.splitlines() == [comment, "*"] +def test_create_cachedir_tag(tmp_path): + cachedir_tag_file = tmp_path / "CACHEDIR.TAG" + cli_run([str(tmp_path), "--without-pip", "--activators", ""]) + + expected = """ + Signature: 8a477f597d28d172789f06886806bc55 + # This file is a cache directory tag created by Python virtualenv. + # For information about cache directory tags, see: + # https://bford.info/cachedir/ + """ + assert cachedir_tag_file.read_text(encoding="utf-8") == textwrap.dedent(expected).strip() + + +def test_create_cachedir_tag_exists(tmp_path: Path) -> None: + cachedir_tag_file = tmp_path / "CACHEDIR.TAG" + cachedir_tag_file.write_text("magic", encoding="utf-8") + cli_run([str(tmp_path), "--without-pip", "--activators", ""]) + assert cachedir_tag_file.read_text(encoding="utf-8") == "magic" + + +def test_create_cachedir_tag_exists_override(tmp_path: Path) -> None: + cachedir_tag_file = tmp_path / "CACHEDIR.TAG" + cachedir_tag_file.write_text("magic", encoding="utf-8") + cli_run([str(tmp_path), "--without-pip", "--activators", ""]) + assert cachedir_tag_file.read_text(encoding="utf-8") == "magic" + + def test_create_vcs_ignore_exists(tmp_path): git_ignore = tmp_path / ".gitignore" git_ignore.write_text("magic", encoding="utf-8")