show.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import csv
  2. import logging
  3. import pathlib
  4. from optparse import Values
  5. from typing import Iterator, List, NamedTuple, Optional, Tuple
  6. from pip._vendor.packaging.utils import canonicalize_name
  7. from pip._internal.cli.base_command import Command
  8. from pip._internal.cli.status_codes import ERROR, SUCCESS
  9. from pip._internal.metadata import BaseDistribution, get_default_environment
  10. from pip._internal.utils.misc import write_output
  11. logger = logging.getLogger(__name__)
  12. class ShowCommand(Command):
  13. """
  14. Show information about one or more installed packages.
  15. The output is in RFC-compliant mail header format.
  16. """
  17. usage = """
  18. %prog [options] <package> ..."""
  19. ignore_require_venv = True
  20. def add_options(self) -> None:
  21. self.cmd_opts.add_option(
  22. '-f', '--files',
  23. dest='files',
  24. action='store_true',
  25. default=False,
  26. help='Show the full list of installed files for each package.')
  27. self.parser.insert_option_group(0, self.cmd_opts)
  28. def run(self, options: Values, args: List[str]) -> int:
  29. if not args:
  30. logger.warning('ERROR: Please provide a package name or names.')
  31. return ERROR
  32. query = args
  33. results = search_packages_info(query)
  34. if not print_results(
  35. results, list_files=options.files, verbose=options.verbose):
  36. return ERROR
  37. return SUCCESS
  38. class _PackageInfo(NamedTuple):
  39. name: str
  40. version: str
  41. location: str
  42. requires: List[str]
  43. required_by: List[str]
  44. installer: str
  45. metadata_version: str
  46. classifiers: List[str]
  47. summary: str
  48. homepage: str
  49. author: str
  50. author_email: str
  51. license: str
  52. entry_points: List[str]
  53. files: Optional[List[str]]
  54. def _covert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str:
  55. """Convert a legacy installed-files.txt path into modern RECORD path.
  56. The legacy format stores paths relative to the info directory, while the
  57. modern format stores paths relative to the package root, e.g. the
  58. site-packages directory.
  59. :param entry: Path parts of the installed-files.txt entry.
  60. :param info: Path parts of the egg-info directory relative to package root.
  61. :returns: The converted entry.
  62. For best compatibility with symlinks, this does not use ``abspath()`` or
  63. ``Path.resolve()``, but tries to work with path parts:
  64. 1. While ``entry`` starts with ``..``, remove the equal amounts of parts
  65. from ``info``; if ``info`` is empty, start appending ``..`` instead.
  66. 2. Join the two directly.
  67. """
  68. while entry and entry[0] == "..":
  69. if not info or info[-1] == "..":
  70. info += ("..",)
  71. else:
  72. info = info[:-1]
  73. entry = entry[1:]
  74. return str(pathlib.Path(*info, *entry))
  75. def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
  76. """
  77. Gather details from installed distributions. Print distribution name,
  78. version, location, and installed files. Installed files requires a
  79. pip generated 'installed-files.txt' in the distributions '.egg-info'
  80. directory.
  81. """
  82. env = get_default_environment()
  83. installed = {
  84. dist.canonical_name: dist
  85. for dist in env.iter_distributions()
  86. }
  87. query_names = [canonicalize_name(name) for name in query]
  88. missing = sorted(
  89. [name for name, pkg in zip(query, query_names) if pkg not in installed]
  90. )
  91. if missing:
  92. logger.warning('Package(s) not found: %s', ', '.join(missing))
  93. def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]:
  94. return [
  95. dist.metadata["Name"] or "UNKNOWN"
  96. for dist in installed.values()
  97. if current_dist.canonical_name in {
  98. canonicalize_name(d.name) for d in dist.iter_dependencies()
  99. }
  100. ]
  101. def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]:
  102. try:
  103. text = dist.read_text('RECORD')
  104. except FileNotFoundError:
  105. return None
  106. # This extra Path-str cast normalizes entries.
  107. return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
  108. def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
  109. try:
  110. text = dist.read_text('installed-files.txt')
  111. except FileNotFoundError:
  112. return None
  113. paths = (p for p in text.splitlines(keepends=False) if p)
  114. root = dist.location
  115. info = dist.info_directory
  116. if root is None or info is None:
  117. return paths
  118. try:
  119. info_rel = pathlib.Path(info).relative_to(root)
  120. except ValueError: # info is not relative to root.
  121. return paths
  122. if not info_rel.parts: # info *is* root.
  123. return paths
  124. return (
  125. _covert_legacy_entry(pathlib.Path(p).parts, info_rel.parts)
  126. for p in paths
  127. )
  128. for query_name in query_names:
  129. try:
  130. dist = installed[query_name]
  131. except KeyError:
  132. continue
  133. try:
  134. entry_points_text = dist.read_text('entry_points.txt')
  135. entry_points = entry_points_text.splitlines(keepends=False)
  136. except FileNotFoundError:
  137. entry_points = []
  138. files_iter = _files_from_record(dist) or _files_from_legacy(dist)
  139. if files_iter is None:
  140. files: Optional[List[str]] = None
  141. else:
  142. files = sorted(files_iter)
  143. metadata = dist.metadata
  144. yield _PackageInfo(
  145. name=dist.raw_name,
  146. version=str(dist.version),
  147. location=dist.location or "",
  148. requires=[req.name for req in dist.iter_dependencies()],
  149. required_by=_get_requiring_packages(dist),
  150. installer=dist.installer,
  151. metadata_version=dist.metadata_version or "",
  152. classifiers=metadata.get_all("Classifier", []),
  153. summary=metadata.get("Summary", ""),
  154. homepage=metadata.get("Home-page", ""),
  155. author=metadata.get("Author", ""),
  156. author_email=metadata.get("Author-email", ""),
  157. license=metadata.get("License", ""),
  158. entry_points=entry_points,
  159. files=files,
  160. )
  161. def print_results(
  162. distributions: Iterator[_PackageInfo],
  163. list_files: bool,
  164. verbose: bool,
  165. ) -> bool:
  166. """
  167. Print the information from installed distributions found.
  168. """
  169. results_printed = False
  170. for i, dist in enumerate(distributions):
  171. results_printed = True
  172. if i > 0:
  173. write_output("---")
  174. write_output("Name: %s", dist.name)
  175. write_output("Version: %s", dist.version)
  176. write_output("Summary: %s", dist.summary)
  177. write_output("Home-page: %s", dist.homepage)
  178. write_output("Author: %s", dist.author)
  179. write_output("Author-email: %s", dist.author_email)
  180. write_output("License: %s", dist.license)
  181. write_output("Location: %s", dist.location)
  182. write_output("Requires: %s", ', '.join(dist.requires))
  183. write_output("Required-by: %s", ', '.join(dist.required_by))
  184. if verbose:
  185. write_output("Metadata-Version: %s", dist.metadata_version)
  186. write_output("Installer: %s", dist.installer)
  187. write_output("Classifiers:")
  188. for classifier in dist.classifiers:
  189. write_output(" %s", classifier)
  190. write_output("Entry-points:")
  191. for entry in dist.entry_points:
  192. write_output(" %s", entry.strip())
  193. if list_files:
  194. write_output("Files:")
  195. if dist.files is None:
  196. write_output("Cannot locate RECORD or installed-files.txt")
  197. else:
  198. for line in dist.files:
  199. write_output(" %s", line.strip())
  200. return results_printed