Skip to content

Commit 07aa872

Browse files
committed
[commands/cache] make pip cache purge remove everything from http + wheels caches; make pip cache remove prune empty directories.
1 parent 3682309 commit 07aa872

File tree

3 files changed

+72
-2
lines changed

3 files changed

+72
-2
lines changed

src/pip/_internal/commands/cache.py

+21
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,28 @@ def remove_cache_items(self, options: Values, args: List[Any]) -> None:
183183
for filename in files:
184184
os.unlink(filename)
185185
logger.verbose("Removed %s", filename)
186+
187+
http_dirs = filesystem.subdirs_with_no_files(self._cache_dir(options, "http"))
188+
wheel_dirs = filesystem.subdirs_with_no_files(
189+
self._cache_dir(options, "wheels")
190+
)
191+
dirs = [*http_dirs, *wheel_dirs]
192+
for dirname in dirs:
193+
try:
194+
os.rmdir(dirname)
195+
except FileNotFoundError:
196+
# If the file is already gone, that's fine.
197+
pass
198+
logger.verbose("Removed %s", dirname)
199+
200+
# selfcheck.json is no longer used by pip.
201+
selfcheck_json = self._cache_dir(options, "selfcheck.json")
202+
if os.path.isfile(selfcheck_json):
203+
os.remove(selfcheck_json)
204+
logger.verbose("Removed legacy selfcheck.json file")
205+
186206
logger.info("Files removed: %s", len(files))
207+
logger.info("Empty directories removed: %s", len(dirs))
187208

188209
def purge_cache(self, options: Values, args: List[Any]) -> None:
189210
if args:

src/pip/_internal/utils/filesystem.py

+49
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import random
55
import sys
66
from contextlib import contextmanager
7+
from pathlib import Path
78
from tempfile import NamedTemporaryFile
89
from typing import Any, BinaryIO, Generator, List, Union, cast
910

1011
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
1112

13+
from pip._internal.exceptions import PipError
1214
from pip._internal.utils.compat import get_path_uid
1315
from pip._internal.utils.misc import format_size
1416

@@ -151,3 +153,50 @@ def directory_size(path: str) -> Union[int, float]:
151153

152154
def format_directory_size(path: str) -> str:
153155
return format_size(directory_size(path))
156+
157+
158+
def _leaf_subdirs(path):
159+
"""Traverses the file tree, finding every empty directory."""
160+
161+
path_obj = Path(path)
162+
163+
if not path_obj.exists():
164+
return
165+
166+
for item in path_obj.iterdir():
167+
if not item.is_dir():
168+
continue
169+
170+
subitems = item.iterdir()
171+
172+
# ASSUMPTION: Nothing in subitems will be None or False.
173+
if not any(subitems):
174+
yield item
175+
176+
if not any(subitem.is_file() for subitem in subitems):
177+
yield from _leaf_subdirs(item)
178+
179+
180+
def _leaf_parents_without_files(path, leaf):
181+
"""Yields +leaf+ and each parent directory below +path+, until one of
182+
them includes a file (as opposed to directories or nothing)."""
183+
184+
if not str(leaf).startswith(str(path)):
185+
# If +leaf+ is not a subdirectory of +path+, bail early to avoid
186+
# an endless loop.
187+
raise PipError("leaf is not a subdirectory of path")
188+
189+
path = Path(path)
190+
leaf = Path(leaf)
191+
while leaf != path:
192+
if all(item.is_dir() for item in leaf.iterdir()):
193+
yield str(leaf)
194+
else:
195+
break
196+
leaf = leaf.parent
197+
198+
199+
def subdirs_with_no_files(path):
200+
"""Yields every subdirectory of +path+ that has no files under it."""
201+
for leaf in _leaf_subdirs(path):
202+
yield from _leaf_parents_without_files(path, leaf)

tests/functional/test_cache.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def test_cache_purge_with_empty_cache(script: PipTestEnvironment) -> None:
256256
and exit without an error code."""
257257
result = script.pip("cache", "purge", allow_stderr_warning=True)
258258
assert result.stderr == "WARNING: No matching packages\n"
259-
assert result.stdout == "Files removed: 0\n"
259+
assert result.stdout == "Files removed: 0\nEmpty directories removed: 0\n"
260260

261261

262262
@pytest.mark.usefixtures("populate_wheel_cache")
@@ -265,7 +265,7 @@ def test_cache_remove_with_bad_pattern(script: PipTestEnvironment) -> None:
265265
and exit without an error code."""
266266
result = script.pip("cache", "remove", "aaa", allow_stderr_warning=True)
267267
assert result.stderr == 'WARNING: No matching packages for pattern "aaa"\n'
268-
assert result.stdout == "Files removed: 0\n"
268+
assert result.stdout == "Files removed: 0\nEmpty directories removed: 0\n"
269269

270270

271271
def test_cache_list_too_many_args(script: PipTestEnvironment) -> None:

0 commit comments

Comments
 (0)