Skip to content

Commit 8284080

Browse files
committed
Make pip cache purge/pip cache remove delete additional unneeded files.
These commands now remove: - wheel cache folders without `.whl` files. - empty folders in the HTTP cache. - `selfcheck.json`, which pip does not use anymore.
1 parent c46141c commit 8284080

File tree

4 files changed

+104
-2
lines changed

4 files changed

+104
-2
lines changed

news/9058.feature.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Make `pip cache purge`/`pip cache remove` delete additional unneeded files.
2+
3+
These commands now remove:
4+
- wheel cache folders without `.whl` files.
5+
- empty folders in the HTTP cache.
6+
- some files created by old versions of pip, which aren't used anymore.

src/pip/_internal/commands/cache.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,28 @@ def remove_cache_items(self, options: Values, args: list[str]) -> None:
189189
bytes_removed += os.stat(filename).st_size
190190
os.unlink(filename)
191191
logger.verbose("Removed %s", filename)
192+
193+
http_dirs = filesystem.subdirs_without_files(self._cache_dir(options, "http"))
194+
wheel_dirs = filesystem.subdirs_without_wheels(
195+
self._cache_dir(options, "wheels")
196+
)
197+
dirs = [*http_dirs, *wheel_dirs]
198+
for dirname in dirs:
199+
try:
200+
os.rmdir(dirname)
201+
except FileNotFoundError:
202+
# If the file is already gone, that's fine.
203+
pass
204+
logger.verbose("Removed %s", dirname)
205+
206+
# selfcheck.json is no longer used by pip.
207+
selfcheck_json = self._cache_dir(options, "selfcheck.json")
208+
if os.path.isfile(selfcheck_json):
209+
os.remove(selfcheck_json)
210+
logger.verbose("Removed legacy selfcheck.json file")
211+
192212
logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
213+
logger.info("Directories removed: %s", len(dirs))
193214

194215
def purge_cache(self, options: Values, args: list[str]) -> None:
195216
if args:

src/pip/_internal/utils/filesystem.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
from collections.abc import Generator
99
from contextlib import contextmanager
10+
from pathlib import Path
1011
from tempfile import NamedTemporaryFile
1112
from typing import Any, BinaryIO, cast
1213

@@ -150,3 +151,77 @@ def directory_size(path: str) -> int | float:
150151

151152
def format_directory_size(path: str) -> str:
152153
return format_size(directory_size(path))
154+
155+
156+
def leaf_dirs_without_files(path: Path, parents: list[Path]) -> Generator[Path]:
157+
"""
158+
Yields every subdirectory of +path+ that has no files under it.
159+
If no subdirectories contain any files, also yields +path+.
160+
"""
161+
162+
subdirs = []
163+
for item in path.iterdir():
164+
if item.is_dir():
165+
subdirs.append(item)
166+
else:
167+
# If we find a file, we want to preserve the whole subtree,
168+
# so bail immediately.
169+
return
170+
171+
# If we get to this point, we didn't find a file yet.
172+
173+
parents = parents + [path]
174+
if subdirs:
175+
for subdir in subdirs:
176+
yield from subdirs_without_wheels(subdir, parents)
177+
else:
178+
yield from reversed(sorted(parents))
179+
180+
181+
def subdirs_without_files(path: str) -> Generator[Path]:
182+
"""Yields every subdirectory of +path+ that has no files under it."""
183+
184+
path_obj = Path(path)
185+
186+
if not path_obj.exists():
187+
return
188+
189+
for subdir in path_obj.iterdir():
190+
yield from leaf_dirs_without_files(subdir, [])
191+
192+
193+
def leaf_dirs_without_wheels(path: Path, parents: list[Path]) -> Generator[Path]:
194+
"""
195+
Yields every subdirectory of +path+ that has no .whl files under it.
196+
If no subdirectories include .whl files, also yields +path+.
197+
"""
198+
subdirs = []
199+
for item in path.iterdir():
200+
if item.is_dir():
201+
subdirs.append(item)
202+
else:
203+
if item.name.endswith(".whl"):
204+
# If we found a wheel, we want to preserve this whole subtree,
205+
# so we bail immediately and don't return any results.
206+
return
207+
208+
# If we get to this point, we didn't find a wheel yet.
209+
210+
parents = parents + [path]
211+
if subdirs:
212+
for subdir in subdirs:
213+
yield from leaf_dirs_without_wheels(subdir, parents)
214+
else:
215+
yield from reversed(sorted(parents))
216+
217+
218+
def subdirs_without_wheels(path: str) -> Generator[Path]:
219+
"""Yields every subdirectory of +path+ that has no .whl files under it."""
220+
221+
path_obj = Path(path)
222+
223+
if not path_obj.exists():
224+
return
225+
226+
for subdir in path_obj.iterdir():
227+
yield from leaf_dirs_without_wheels(subdir, [])

tests/functional/test_cache.py

Lines changed: 2 additions & 2 deletions
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 (0 bytes)\n"
259+
assert result.stdout == "Files removed: 0 (0 bytes)\nDirectories 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 (0 bytes)\n"
268+
assert result.stdout == "Files removed: 0 (0 bytes)\nDirectories removed: 0\n"
269269

270270

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

0 commit comments

Comments
 (0)