Skip to content

Commit cd26c8d

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 cd26c8d

File tree

4 files changed

+97
-2
lines changed

4 files changed

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