Skip to content

Commit a9892d2

Browse files
committed
Validate cache directory before cleaning it
1 parent 767c1bf commit a9892d2

File tree

4 files changed

+81
-18
lines changed

4 files changed

+81
-18
lines changed

src/docstub/_cache.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,21 @@
55
logger = logging.getLogger(__name__)
66

77

8+
CACHE_DIR_NAME = ".docstub_cache"
9+
10+
811
CACHEDIR_TAG_CONTENT = """\
9-
Signature: 8a477f597d28d172789f06886806bc55\
10-
# This file is a cache directory tag automatically created by docstub.\n"
11-
# For information about cache directory tags see https://bford.info/cachedir/\n"
12+
Signature: 8a477f597d28d172789f06886806bc55
13+
# Mark this directory as a cache [1], created by docstub [2]
14+
# [1] https://bford.info/cachedir/
15+
# [2] https://github.com/scientific-python/docstub
16+
"""
17+
18+
19+
GITHUB_IGNORE_CONTENT = """\
20+
# Make git ignore this cache directory, created by docstub [1]
21+
# [1] https://github.com/scientific-python/docstub
22+
*
1223
"""
1324

1425

@@ -43,22 +54,42 @@ def create_cache(path):
4354
"""
4455
path.mkdir(parents=True, exist_ok=True)
4556
cachdir_tag_path = path / "CACHEDIR.TAG"
46-
cachdir_tag_content = (
47-
"Signature: 8a477f597d28d172789f06886806bc55\n"
48-
"# This file is a cache directory tag automatically created by docstub.\n"
49-
"# For information about cache directory tags see https://bford.info/cachedir/\n"
50-
)
57+
5158
if not cachdir_tag_path.is_file():
5259
with open(cachdir_tag_path, "w") as fp:
53-
fp.write(cachdir_tag_content)
60+
fp.write(CACHEDIR_TAG_CONTENT)
5461

5562
gitignore_path = path / ".gitignore"
56-
gitignore_content = (
57-
"# This file is a cache directory automatically created by docstub.\n" "*\n"
58-
)
5963
if not gitignore_path.is_file():
6064
with open(gitignore_path, "w") as fp:
61-
fp.write(gitignore_content)
65+
fp.write(GITHUB_IGNORE_CONTENT)
66+
67+
68+
def validate_cache(path):
69+
"""Make sure the given path is a cache created by docstub.
70+
71+
Parameters
72+
----------
73+
path : Path
74+
75+
Raises
76+
------
77+
FileNotFoundError
78+
"""
79+
if not path.is_dir():
80+
raise FileNotFoundError(f"expected '{path}' to be a valid directory")
81+
82+
if not path.name == CACHE_DIR_NAME:
83+
raise FileNotFoundError(
84+
f"expected directory '{path}' be named '{CACHE_DIR_NAME}'"
85+
)
86+
87+
cachdir_tag_path = path / "CACHEDIR.TAG"
88+
if not cachdir_tag_path.is_file():
89+
raise FileNotFoundError(f"expected '{path}' to contain a 'CACHEDIR.TAG' file")
90+
gitignore_path = path / ".gitignore"
91+
if not gitignore_path.is_file():
92+
raise FileNotFoundError(f"expected '{path}' to contain a '.gitignore' file")
6293

6394

6495
class FuncSerializer[T](Protocol):

src/docstub/_cli.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
TypeMatcher,
1515
common_known_types,
1616
)
17-
from ._cache import FileCache
17+
from ._cache import CACHE_DIR_NAME, FileCache, validate_cache
1818
from ._config import Config
1919
from ._path_utils import (
2020
STUB_HEADER_COMMENT,
@@ -35,7 +35,7 @@ def _cache_dir_in_cwd():
3535
-------
3636
cache_dir : Path
3737
"""
38-
return Path.cwd() / ".docstub_cache"
38+
return Path.cwd() / CACHE_DIR_NAME
3939

4040

4141
def _load_configuration(config_paths=None):
@@ -385,8 +385,19 @@ def clean(verbose):
385385

386386
path = _cache_dir_in_cwd()
387387
if path.exists():
388-
assert path.is_dir()
389-
shutil.rmtree(_cache_dir_in_cwd())
390-
logger.info("cleaned %s", path)
388+
try:
389+
validate_cache(path)
390+
except (FileNotFoundError, ValueError) as e:
391+
logger.error(
392+
"'%s' might not be a valid cache or might be corrupted. Not "
393+
"removing it out of caution. Manually remove it after checking "
394+
"if it is safe to do so.\n\nDetails: %s",
395+
path,
396+
"\n".join(e.args),
397+
)
398+
sys.exit(1)
399+
else:
400+
shutil.rmtree(_cache_dir_in_cwd())
401+
logger.info("cleaned %s", path)
391402
else:
392403
logger.info("no cache to clean")

tests/test_cache.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ def test_create_cache(tmp_path):
3737
_cache.create_cache(cache_dir)
3838

3939

40+
def test_create_validate_cache(tmp_path):
41+
cache_dir = tmp_path / _cache.CACHE_DIR_NAME
42+
_cache.create_cache(cache_dir)
43+
_cache.validate_cache(cache_dir)
44+
45+
with pytest.raises(FileNotFoundError, match="expected directory .* named .*"):
46+
_cache.validate_cache(tmp_path)
47+
48+
4049
class Test_FileCache:
4150
def test_basic(self, tmp_path):
4251

tests/test_cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,15 @@ def test_basic(self, tmp_path_cwd, verbosity):
9090
assert run_result.exception is None
9191
assert run_result.exit_code == 0
9292
assert not cache_dir.exists()
93+
94+
def test_corrupted_cache(self, tmp_path_cwd, caplog):
95+
cache_dir = tmp_path_cwd / ".docstub_cache"
96+
cache_dir.mkdir()
97+
98+
runner = CliRunner()
99+
run_result = runner.invoke(_cli.clean, args=[])
100+
assert run_result.exit_code == 1
101+
assert "might not be a valid cache or might be corrupted" in "\n".join(
102+
caplog.messages
103+
)
104+
assert cache_dir.is_dir()

0 commit comments

Comments
 (0)