cache.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. from __future__ import absolute_import
  2. import logging
  3. import os
  4. import textwrap
  5. import pip._internal.utils.filesystem as filesystem
  6. from pip._internal.cli.base_command import Command
  7. from pip._internal.cli.status_codes import ERROR, SUCCESS
  8. from pip._internal.exceptions import CommandError, PipError
  9. from pip._internal.utils.typing import MYPY_CHECK_RUNNING
  10. if MYPY_CHECK_RUNNING:
  11. from optparse import Values
  12. from typing import Any, List
  13. logger = logging.getLogger(__name__)
  14. class CacheCommand(Command):
  15. """
  16. Inspect and manage pip's wheel cache.
  17. Subcommands:
  18. - dir: Show the cache directory.
  19. - info: Show information about the cache.
  20. - list: List filenames of packages stored in the cache.
  21. - remove: Remove one or more package from the cache.
  22. - purge: Remove all items from the cache.
  23. ``<pattern>`` can be a glob expression or a package name.
  24. """
  25. ignore_require_venv = True
  26. usage = """
  27. %prog dir
  28. %prog info
  29. %prog list [<pattern>] [--format=[human, abspath]]
  30. %prog remove <pattern>
  31. %prog purge
  32. """
  33. def add_options(self):
  34. # type: () -> None
  35. self.cmd_opts.add_option(
  36. '--format',
  37. action='store',
  38. dest='list_format',
  39. default="human",
  40. choices=('human', 'abspath'),
  41. help="Select the output format among: human (default) or abspath"
  42. )
  43. self.parser.insert_option_group(0, self.cmd_opts)
  44. def run(self, options, args):
  45. # type: (Values, List[Any]) -> int
  46. handlers = {
  47. "dir": self.get_cache_dir,
  48. "info": self.get_cache_info,
  49. "list": self.list_cache_items,
  50. "remove": self.remove_cache_items,
  51. "purge": self.purge_cache,
  52. }
  53. if not options.cache_dir:
  54. logger.error("pip cache commands can not "
  55. "function since cache is disabled.")
  56. return ERROR
  57. # Determine action
  58. if not args or args[0] not in handlers:
  59. logger.error(
  60. "Need an action (%s) to perform.",
  61. ", ".join(sorted(handlers)),
  62. )
  63. return ERROR
  64. action = args[0]
  65. # Error handling happens here, not in the action-handlers.
  66. try:
  67. handlers[action](options, args[1:])
  68. except PipError as e:
  69. logger.error(e.args[0])
  70. return ERROR
  71. return SUCCESS
  72. def get_cache_dir(self, options, args):
  73. # type: (Values, List[Any]) -> None
  74. if args:
  75. raise CommandError('Too many arguments')
  76. logger.info(options.cache_dir)
  77. def get_cache_info(self, options, args):
  78. # type: (Values, List[Any]) -> None
  79. if args:
  80. raise CommandError('Too many arguments')
  81. num_http_files = len(self._find_http_files(options))
  82. num_packages = len(self._find_wheels(options, '*'))
  83. http_cache_location = self._cache_dir(options, 'http')
  84. wheels_cache_location = self._cache_dir(options, 'wheels')
  85. http_cache_size = filesystem.format_directory_size(http_cache_location)
  86. wheels_cache_size = filesystem.format_directory_size(
  87. wheels_cache_location
  88. )
  89. message = textwrap.dedent("""
  90. Package index page cache location: {http_cache_location}
  91. Package index page cache size: {http_cache_size}
  92. Number of HTTP files: {num_http_files}
  93. Wheels location: {wheels_cache_location}
  94. Wheels size: {wheels_cache_size}
  95. Number of wheels: {package_count}
  96. """).format(
  97. http_cache_location=http_cache_location,
  98. http_cache_size=http_cache_size,
  99. num_http_files=num_http_files,
  100. wheels_cache_location=wheels_cache_location,
  101. package_count=num_packages,
  102. wheels_cache_size=wheels_cache_size,
  103. ).strip()
  104. logger.info(message)
  105. def list_cache_items(self, options, args):
  106. # type: (Values, List[Any]) -> None
  107. if len(args) > 1:
  108. raise CommandError('Too many arguments')
  109. if args:
  110. pattern = args[0]
  111. else:
  112. pattern = '*'
  113. files = self._find_wheels(options, pattern)
  114. if options.list_format == 'human':
  115. self.format_for_human(files)
  116. else:
  117. self.format_for_abspath(files)
  118. def format_for_human(self, files):
  119. # type: (List[str]) -> None
  120. if not files:
  121. logger.info('Nothing cached.')
  122. return
  123. results = []
  124. for filename in files:
  125. wheel = os.path.basename(filename)
  126. size = filesystem.format_file_size(filename)
  127. results.append(' - {} ({})'.format(wheel, size))
  128. logger.info('Cache contents:\n')
  129. logger.info('\n'.join(sorted(results)))
  130. def format_for_abspath(self, files):
  131. # type: (List[str]) -> None
  132. if not files:
  133. return
  134. results = []
  135. for filename in files:
  136. results.append(filename)
  137. logger.info('\n'.join(sorted(results)))
  138. def remove_cache_items(self, options, args):
  139. # type: (Values, List[Any]) -> None
  140. if len(args) > 1:
  141. raise CommandError('Too many arguments')
  142. if not args:
  143. raise CommandError('Please provide a pattern')
  144. files = self._find_wheels(options, args[0])
  145. # Only fetch http files if no specific pattern given
  146. if args[0] == '*':
  147. files += self._find_http_files(options)
  148. if not files:
  149. raise CommandError('No matching packages')
  150. for filename in files:
  151. os.unlink(filename)
  152. logger.debug('Removed %s', filename)
  153. logger.info('Files removed: %s', len(files))
  154. def purge_cache(self, options, args):
  155. # type: (Values, List[Any]) -> None
  156. if args:
  157. raise CommandError('Too many arguments')
  158. return self.remove_cache_items(options, ['*'])
  159. def _cache_dir(self, options, subdir):
  160. # type: (Values, str) -> str
  161. return os.path.join(options.cache_dir, subdir)
  162. def _find_http_files(self, options):
  163. # type: (Values) -> List[str]
  164. http_dir = self._cache_dir(options, 'http')
  165. return filesystem.find_files(http_dir, '*')
  166. def _find_wheels(self, options, pattern):
  167. # type: (Values, str) -> List[str]
  168. wheel_dir = self._cache_dir(options, 'wheels')
  169. # The wheel filename format, as specified in PEP 427, is:
  170. # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
  171. #
  172. # Additionally, non-alphanumeric values in the distribution are
  173. # normalized to underscores (_), meaning hyphens can never occur
  174. # before `-{version}`.
  175. #
  176. # Given that information:
  177. # - If the pattern we're given contains a hyphen (-), the user is
  178. # providing at least the version. Thus, we can just append `*.whl`
  179. # to match the rest of it.
  180. # - If the pattern we're given doesn't contain a hyphen (-), the
  181. # user is only providing the name. Thus, we append `-*.whl` to
  182. # match the hyphen before the version, followed by anything else.
  183. #
  184. # PEP 427: https://www.python.org/dev/peps/pep-0427/
  185. pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl")
  186. return filesystem.find_files(wheel_dir, pattern)