cache.py 8.0 KB

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