Skip to content

Commit 1816296

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 1816296

File tree

4 files changed

+94
-2
lines changed

4 files changed

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