cache.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import os
  2. import textwrap
  3. from optparse import Values
  4. from typing import Any, List
  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 run(self, options: Values, args: List[str]) -> int:
  42. handlers = {
  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. if not options.cache_dir:
  50. logger.error("pip cache commands can not function since cache is disabled.")
  51. return ERROR
  52. # Determine action
  53. if not args or args[0] not in handlers:
  54. logger.error(
  55. "Need an action (%s) to perform.",
  56. ", ".join(sorted(handlers)),
  57. )
  58. return ERROR
  59. action = args[0]
  60. # Error handling happens here, not in the action-handlers.
  61. try:
  62. handlers[action](options, args[1:])
  63. except PipError as e:
  64. logger.error(e.args[0])
  65. return ERROR
  66. return SUCCESS
  67. def get_cache_dir(self, options: Values, args: List[Any]) -> None:
  68. if args:
  69. raise CommandError("Too many arguments")
  70. logger.info(options.cache_dir)
  71. def get_cache_info(self, options: Values, args: List[Any]) -> None:
  72. if args:
  73. raise CommandError("Too many arguments")
  74. num_http_files = len(self._find_http_files(options))
  75. num_packages = len(self._find_wheels(options, "*"))
  76. http_cache_location = self._cache_dir(options, "http-v2")
  77. old_http_cache_location = self._cache_dir(options, "http")
  78. wheels_cache_location = self._cache_dir(options, "wheels")
  79. http_cache_size = filesystem.format_size(
  80. filesystem.directory_size(http_cache_location)
  81. + filesystem.directory_size(old_http_cache_location)
  82. )
  83. wheels_cache_size = filesystem.format_directory_size(wheels_cache_location)
  84. message = (
  85. textwrap.dedent(
  86. """
  87. Package index page cache location (pip v23.3+): {http_cache_location}
  88. Package index page cache location (older pips): {old_http_cache_location}
  89. Package index page cache size: {http_cache_size}
  90. Number of HTTP files: {num_http_files}
  91. Locally built wheels location: {wheels_cache_location}
  92. Locally built wheels size: {wheels_cache_size}
  93. Number of locally built wheels: {package_count}
  94. """ # noqa: E501
  95. )
  96. .format(
  97. http_cache_location=http_cache_location,
  98. old_http_cache_location=old_http_cache_location,
  99. http_cache_size=http_cache_size,
  100. num_http_files=num_http_files,
  101. wheels_cache_location=wheels_cache_location,
  102. package_count=num_packages,
  103. wheels_cache_size=wheels_cache_size,
  104. )
  105. .strip()
  106. )
  107. logger.info(message)
  108. def list_cache_items(self, options: Values, args: List[Any]) -> None:
  109. if len(args) > 1:
  110. raise CommandError("Too many arguments")
  111. if args:
  112. pattern = args[0]
  113. else:
  114. pattern = "*"
  115. files = self._find_wheels(options, pattern)
  116. if options.list_format == "human":
  117. self.format_for_human(files)
  118. else:
  119. self.format_for_abspath(files)
  120. def format_for_human(self, files: List[str]) -> None:
  121. if not files:
  122. logger.info("No locally built wheels cached.")
  123. return
  124. results = []
  125. for filename in files:
  126. wheel = os.path.basename(filename)
  127. size = filesystem.format_file_size(filename)
  128. results.append(f" - {wheel} ({size})")
  129. logger.info("Cache contents:\n")
  130. logger.info("\n".join(sorted(results)))
  131. def format_for_abspath(self, files: List[str]) -> None:
  132. if files:
  133. logger.info("\n".join(sorted(files)))
  134. def remove_cache_items(self, options: Values, args: List[Any]) -> None:
  135. if len(args) > 1:
  136. raise CommandError("Too many arguments")
  137. if not args:
  138. raise CommandError("Please provide a pattern")
  139. files = self._find_wheels(options, args[0])
  140. no_matching_msg = "No matching packages"
  141. if args[0] == "*":
  142. # Only fetch http files if no specific pattern given
  143. files += self._find_http_files(options)
  144. else:
  145. # Add the pattern to the log message
  146. no_matching_msg += f' for pattern "{args[0]}"'
  147. if not files:
  148. logger.warning(no_matching_msg)
  149. bytes_removed = 0
  150. for filename in files:
  151. bytes_removed += os.stat(filename).st_size
  152. os.unlink(filename)
  153. logger.verbose("Removed %s", filename)
  154. logger.info("Files removed: %s (%s)", len(files), format_size(bytes_removed))
  155. def purge_cache(self, options: Values, args: 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: Values, subdir: str) -> str:
  160. return os.path.join(options.cache_dir, subdir)
  161. def _find_http_files(self, options: Values) -> List[str]:
  162. old_http_dir = self._cache_dir(options, "http")
  163. new_http_dir = self._cache_dir(options, "http-v2")
  164. return filesystem.find_files(old_http_dir, "*") + filesystem.find_files(
  165. new_http_dir, "*"
  166. )
  167. def _find_wheels(self, options: Values, pattern: 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)