Skip to content

Commit 5eeed83

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 5eeed83

File tree

4 files changed

+101
-2
lines changed

4 files changed

+101
-2
lines changed

news/9058.feature.rst

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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,31 @@ 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+
199+
for subdir in dirs:
200+
try:
201+
for file in subdir.iterdir():
202+
file.unlink(missing_ok=True)
203+
subdir.rmdir()
204+
except FileNotFoundError:
205+
# If the file is already gone, that's fine.
206+
pass
207+
logger.verbose("Removed %s", subdir)
208+
209+
# selfcheck.json is no longer used by pip.
210+
selfcheck_json = self._cache_dir(options, "selfcheck.json")
211+
if os.path.isfile(selfcheck_json):
212+
os.remove(selfcheck_json)
213+
logger.verbose("Removed legacy selfcheck.json file")
214+
192215
logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
216+
logger.info("Directories removed: %s", len(dirs))
193217

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

src/pip/_internal/utils/filesystem.py

Lines changed: 69 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,71 @@ 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 subdirs_without_files(path: str) -> Generator[Path]:
157+
"""Yields every subdirectory of +path+ that has no files under it."""
158+
159+
def inner(path: Path, parents: list[Path]) -> Generator[Path]:
160+
path_obj = Path(path)
161+
162+
if not path_obj.exists():
163+
return
164+
165+
subdirs = []
166+
for item in path_obj.iterdir():
167+
if item.is_dir():
168+
subdirs.append(item)
169+
else:
170+
# If we find a file, we want to preserve the whole subtree,
171+
# so bail immediately.
172+
return
173+
174+
# If we get to this point, we didn't find a file yet.
175+
176+
if parents is None:
177+
parents = []
178+
else:
179+
parents += [path_obj]
180+
181+
if subdirs:
182+
for subdir in subdirs:
183+
yield from inner(subdir, parents)
184+
else:
185+
yield from parents
186+
187+
yield from sorted(set(inner(Path(path), [])), reverse=True)
188+
189+
190+
def subdirs_without_wheels(path: str) -> Generator[Path]:
191+
"""Yields every subdirectory of +path+ that has no .whl files under it."""
192+
193+
def inner(path: str | Path, parents: list[Path]) -> Generator[Path]:
194+
path_obj = Path(path)
195+
196+
if not path_obj.exists():
197+
return
198+
199+
subdirs = []
200+
for item in path_obj.iterdir():
201+
if item.is_dir():
202+
subdirs.append(item)
203+
elif 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+
if parents is None:
211+
parents = []
212+
else:
213+
parents += [path_obj]
214+
215+
if subdirs:
216+
for subdir in subdirs:
217+
yield from inner(subdir, parents)
218+
else:
219+
yield from parents
220+
221+
yield from sorted(set(inner(path, [])), reverse=True)

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)