Skip to content

Commit e7ff0f6

Browse files
RonnyPfannschmidtCursor AIclaude
committed
cacheprovider: simplify cache directory creation
Refactor _ensure_cache_dir_and_supporting_files to use a dedicated _make_cachedir function that atomically creates the cache directory with its supporting files (README.md, .gitignore, CACHEDIR.TAG). - Store all file contents as bytes in CACHEDIR_FILES dict - Use tempfile.mkdtemp + shutil.rmtree instead of TemporaryDirectory - Simplify cleanup logic for race conditions Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
1 parent 14ac82d commit e7ff0f6

2 files changed

Lines changed: 45 additions & 49 deletions

File tree

src/_pytest/cacheprovider.py

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import json
1313
import os
1414
from pathlib import Path
15+
import shutil
1516
import tempfile
1617
from typing import final
1718

@@ -33,7 +34,8 @@
3334
from _pytest.reports import TestReport
3435

3536

36-
README_CONTENT = """\
37+
CACHEDIR_FILES: dict[str, bytes] = {
38+
"README.md": b"""\
3739
# pytest cache directory #
3840
3941
This directory contains data from the pytest's cache plugin,
@@ -42,14 +44,48 @@
4244
**Do not** commit this to version control.
4345
4446
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
45-
"""
46-
47-
CACHEDIR_TAG_CONTENT = b"""\
47+
""",
48+
".gitignore": b"# Created by pytest automatically.\n*\n",
49+
"CACHEDIR.TAG": b"""\
4850
Signature: 8a477f597d28d172789f06886806bc55
4951
# This file is a cache directory tag created by pytest.
5052
# For information about cache directory tags, see:
5153
# https://bford.info/cachedir/spec.html
52-
"""
54+
""",
55+
}
56+
57+
58+
def _make_cachedir(target: Path) -> None:
59+
"""Create the pytest cache directory atomically with supporting files.
60+
61+
Creates a temporary directory with README.md, .gitignore, and CACHEDIR.TAG,
62+
then atomically renames it to the target location. If another process wins
63+
the race, the temporary directory is cleaned up.
64+
"""
65+
target.parent.mkdir(parents=True, exist_ok=True)
66+
path = Path(tempfile.mkdtemp(prefix="pytest-cache-files-", dir=target.parent))
67+
try:
68+
# Reset permissions to the default, see #12308.
69+
# Note: there's no way to get the current umask atomically, eek.
70+
umask = os.umask(0o022)
71+
os.umask(umask)
72+
path.chmod(0o777 - umask)
73+
74+
for name, content in CACHEDIR_FILES.items():
75+
path.joinpath(name).write_bytes(content)
76+
77+
path.rename(target)
78+
except OSError as e:
79+
# If 2 concurrent pytests both race to the rename, the loser
80+
# gets "Directory not empty" from the rename. In this case,
81+
# everything is handled so just continue after cleanup.
82+
# On Windows, the error is a FileExistsError which translates to EEXIST.
83+
shutil.rmtree(path, ignore_errors=True)
84+
if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
85+
raise
86+
except BaseException:
87+
shutil.rmtree(path, ignore_errors=True)
88+
raise
5389

5490

5591
@final
@@ -202,48 +238,8 @@ def set(self, key: str, value: object) -> None:
202238

203239
def _ensure_cache_dir_and_supporting_files(self) -> None:
204240
"""Create the cache dir and its supporting files."""
205-
if self._cachedir.is_dir():
206-
return
207-
208-
self._cachedir.parent.mkdir(parents=True, exist_ok=True)
209-
with tempfile.TemporaryDirectory(
210-
prefix="pytest-cache-files-",
211-
dir=self._cachedir.parent,
212-
) as newpath:
213-
path = Path(newpath)
214-
215-
# Reset permissions to the default, see #12308.
216-
# Note: there's no way to get the current umask atomically, eek.
217-
umask = os.umask(0o022)
218-
os.umask(umask)
219-
path.chmod(0o777 - umask)
220-
221-
with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f:
222-
f.write(README_CONTENT)
223-
with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f:
224-
f.write("# Created by pytest automatically.\n*\n")
225-
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
226-
f.write(CACHEDIR_TAG_CONTENT)
227-
228-
try:
229-
path.rename(self._cachedir)
230-
except OSError as e:
231-
# If 2 concurrent pytests both race to the rename, the loser
232-
# gets "Directory not empty" from the rename. In this case,
233-
# everything is handled so just continue (while letting the
234-
# temporary directory be cleaned up).
235-
# On Windows, the error is a FileExistsError which translates to EEXIST.
236-
if e.errno not in (errno.ENOTEMPTY, errno.EEXIST):
237-
raise
238-
else:
239-
# Create a directory in place of the one we just moved so that
240-
# `TemporaryDirectory`'s cleanup doesn't complain.
241-
#
242-
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10.
243-
# See https://github.com/python/cpython/issues/74168. Note that passing
244-
# delete=False would do the wrong thing in case of errors and isn't supported
245-
# until python 3.12.
246-
path.mkdir()
241+
if not self._cachedir.is_dir():
242+
_make_cachedir(self._cachedir)
247243

248244

249245
class LFPluginCollWrapper:

testing/test_cacheprovider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,13 +1348,13 @@ def test_does_not_create_boilerplate_in_existing_dirs(pytester: Pytester) -> Non
13481348
def test_cachedir_tag(pytester: Pytester) -> None:
13491349
"""Ensure we automatically create CACHEDIR.TAG file in the pytest_cache directory (#4278)."""
13501350
from _pytest.cacheprovider import Cache
1351-
from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT
1351+
from _pytest.cacheprovider import CACHEDIR_FILES
13521352

13531353
config = pytester.parseconfig()
13541354
cache = Cache.for_config(config, _ispytest=True)
13551355
cache.set("foo", "bar")
13561356
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
1357-
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT
1357+
assert cachedir_tag_path.read_bytes() == CACHEDIR_FILES["CACHEDIR.TAG"]
13581358

13591359

13601360
def test_clioption_with_cacheshow_and_help(pytester: Pytester) -> None:

0 commit comments

Comments
 (0)