Skip to content

Commit

Permalink
Merge pull request #6391 from duckinator/pip-cache
Browse files Browse the repository at this point in the history
Add 'pip cache' command
  • Loading branch information
pradyunsg authored Apr 13, 2020
2 parents 92c9f81 + b988417 commit bdff935
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/html/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Reference Guide
pip_list
pip_show
pip_search
pip_cache
pip_check
pip_config
pip_wheel
Expand Down
22 changes: 22 additions & 0 deletions docs/html/reference/pip_cache.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

.. _`pip cache`:

pip cache
---------

.. contents::

Usage
*****

.. pip-command-usage:: cache

Description
***********

.. pip-command-description:: cache

Options
*******

.. pip-command-options:: cache
20 changes: 20 additions & 0 deletions docs/man/commands/cache.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
:orphan:

=========
pip-cache
=========

Description
***********

.. pip-command-description:: cache

Usage
*****

.. pip-command-usage:: cache

Options
*******

.. pip-command-options:: cache
1 change: 1 addition & 0 deletions news/6391.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``pip cache`` command for inspecting/managing pip's wheel cache.
4 changes: 4 additions & 0 deletions src/pip/_internal/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
'pip._internal.commands.search', 'SearchCommand',
'Search PyPI for packages.',
)),
('cache', CommandInfo(
'pip._internal.commands.cache', 'CacheCommand',
"Inspect and manage pip's wheel cache.",
)),
('wheel', CommandInfo(
'pip._internal.commands.wheel', 'WheelCommand',
'Build wheels from your requirements.',
Expand Down
165 changes: 165 additions & 0 deletions src/pip/_internal/commands/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from __future__ import absolute_import

import logging
import os
import textwrap

import pip._internal.utils.filesystem as filesystem
from pip._internal.cli.base_command import Command
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.exceptions import CommandError, PipError
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
from optparse import Values
from typing import Any, List


logger = logging.getLogger(__name__)


class CacheCommand(Command):
"""
Inspect and manage pip's wheel cache.
Subcommands:
info: Show information about the cache.
list: List filenames of packages stored in the cache.
remove: Remove one or more package from the cache.
purge: Remove all items from the cache.
<pattern> can be a glob expression or a package name.
"""

usage = """
%prog info
%prog list [<pattern>]
%prog remove <pattern>
%prog purge
"""

def run(self, options, args):
# type: (Values, List[Any]) -> int
handlers = {
"info": self.get_cache_info,
"list": self.list_cache_items,
"remove": self.remove_cache_items,
"purge": self.purge_cache,
}

# Determine action
if not args or args[0] not in handlers:
logger.error("Need an action ({}) to perform.".format(
", ".join(sorted(handlers)))
)
return ERROR

action = args[0]

# Error handling happens here, not in the action-handlers.
try:
handlers[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR

return SUCCESS

def get_cache_info(self, options, args):
# type: (Values, List[Any]) -> None
if args:
raise CommandError('Too many arguments')

num_packages = len(self._find_wheels(options, '*'))

cache_location = self._wheels_cache_dir(options)
cache_size = filesystem.format_directory_size(cache_location)

message = textwrap.dedent("""
Location: {location}
Size: {size}
Number of wheels: {package_count}
""").format(
location=cache_location,
package_count=num_packages,
size=cache_size,
).strip()

logger.info(message)

def list_cache_items(self, options, args):
# type: (Values, List[Any]) -> None
if len(args) > 1:
raise CommandError('Too many arguments')

if args:
pattern = args[0]
else:
pattern = '*'

files = self._find_wheels(options, pattern)

if not files:
logger.info('Nothing cached.')
return

results = []
for filename in files:
wheel = os.path.basename(filename)
size = filesystem.format_file_size(filename)
results.append(' - {} ({})'.format(wheel, size))
logger.info('Cache contents:\n')
logger.info('\n'.join(sorted(results)))

def remove_cache_items(self, options, args):
# type: (Values, List[Any]) -> None
if len(args) > 1:
raise CommandError('Too many arguments')

if not args:
raise CommandError('Please provide a pattern')

files = self._find_wheels(options, args[0])
if not files:
raise CommandError('No matching packages')

for filename in files:
os.unlink(filename)
logger.debug('Removed %s', filename)
logger.info('Files removed: %s', len(files))

def purge_cache(self, options, args):
# type: (Values, List[Any]) -> None
if args:
raise CommandError('Too many arguments')

return self.remove_cache_items(options, ['*'])

def _wheels_cache_dir(self, options):
# type: (Values) -> str
return os.path.join(options.cache_dir, 'wheels')

def _find_wheels(self, options, pattern):
# type: (Values, str) -> List[str]
wheel_dir = self._wheels_cache_dir(options)

# The wheel filename format, as specified in PEP 427, is:
# {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
#
# Additionally, non-alphanumeric values in the distribution are
# normalized to underscores (_), meaning hyphens can never occur
# before `-{version}`.
#
# Given that information:
# - If the pattern we're given contains a hyphen (-), the user is
# providing at least the version. Thus, we can just append `*.whl`
# to match the rest of it.
# - If the pattern we're given doesn't contain a hyphen (-), the
# user is only providing the name. Thus, we append `-*.whl` to
# match the hyphen before the version, followed by anything else.
#
# PEP 427: https://www.python.org/dev/peps/pep-0427/
pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl")

return filesystem.find_files(wheel_dir, pattern)
43 changes: 42 additions & 1 deletion src/pip/_internal/utils/filesystem.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import errno
import fnmatch
import os
import os.path
import random
Expand All @@ -14,10 +15,11 @@
from pip._vendor.six import PY2

from pip._internal.utils.compat import get_path_uid
from pip._internal.utils.misc import format_size
from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast

if MYPY_CHECK_RUNNING:
from typing import Any, BinaryIO, Iterator
from typing import Any, BinaryIO, Iterator, List, Union

class NamedTemporaryFileResult(BinaryIO):
@property
Expand Down Expand Up @@ -176,3 +178,42 @@ def _test_writable_dir_win(path):
raise EnvironmentError(
'Unexpected condition testing for writable directory'
)


def find_files(path, pattern):
# type: (str, str) -> List[str]
"""Returns a list of absolute paths of files beneath path, recursively,
with filenames which match the UNIX-style shell glob pattern."""
result = [] # type: List[str]
for root, dirs, files in os.walk(path):
matches = fnmatch.filter(files, pattern)
result.extend(os.path.join(root, f) for f in matches)
return result


def file_size(path):
# type: (str) -> Union[int, float]
# If it's a symlink, return 0.
if os.path.islink(path):
return 0
return os.path.getsize(path)


def format_file_size(path):
# type: (str) -> str
return format_size(file_size(path))


def directory_size(path):
# type: (str) -> Union[int, float]
size = 0.0
for root, _dirs, files in os.walk(path):
for filename in files:
file_path = os.path.join(root, filename)
size += file_size(file_path)
return size


def format_directory_size(path):
# type: (str) -> str
return format_size(directory_size(path))
Loading

0 comments on commit bdff935

Please sign in to comment.