show.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from __future__ import annotations
  2. import logging
  3. import string
  4. from collections.abc import Generator, Iterable, Iterator
  5. from optparse import Values
  6. from typing import NamedTuple
  7. from pip._vendor.packaging.requirements import InvalidRequirement
  8. from pip._vendor.packaging.utils import canonicalize_name
  9. from pip._internal.cli.base_command import Command
  10. from pip._internal.cli.status_codes import ERROR, SUCCESS
  11. from pip._internal.metadata import BaseDistribution, get_default_environment
  12. from pip._internal.utils.misc import write_output
  13. logger = logging.getLogger(__name__)
  14. def normalize_project_url_label(label: str) -> str:
  15. # This logic is from PEP 753 (Well-known Project URLs in Metadata).
  16. chars_to_remove = string.punctuation + string.whitespace
  17. removal_map = str.maketrans("", "", chars_to_remove)
  18. return label.translate(removal_map).lower()
  19. class ShowCommand(Command):
  20. """
  21. Show information about one or more installed packages.
  22. The output is in RFC-compliant mail header format.
  23. """
  24. usage = """
  25. %prog [options] <package> ..."""
  26. ignore_require_venv = True
  27. def add_options(self) -> None:
  28. self.cmd_opts.add_option(
  29. "-f",
  30. "--files",
  31. dest="files",
  32. action="store_true",
  33. default=False,
  34. help="Show the full list of installed files for each package.",
  35. )
  36. self.parser.insert_option_group(0, self.cmd_opts)
  37. def run(self, options: Values, args: list[str]) -> int:
  38. if not args:
  39. logger.warning("ERROR: Please provide a package name or names.")
  40. return ERROR
  41. query = args
  42. results = search_packages_info(query)
  43. if not print_results(
  44. results, list_files=options.files, verbose=options.verbose
  45. ):
  46. return ERROR
  47. return SUCCESS
  48. class _PackageInfo(NamedTuple):
  49. name: str
  50. version: str
  51. location: str
  52. editable_project_location: str | None
  53. requires: list[str]
  54. required_by: list[str]
  55. installer: str
  56. metadata_version: str
  57. classifiers: list[str]
  58. summary: str
  59. homepage: str
  60. project_urls: list[str]
  61. author: str
  62. author_email: str
  63. license: str
  64. license_expression: str
  65. entry_points: list[str]
  66. files: list[str] | None
  67. def search_packages_info(query: list[str]) -> Generator[_PackageInfo, None, None]:
  68. """
  69. Gather details from installed distributions. Print distribution name,
  70. version, location, and installed files. Installed files requires a
  71. pip generated 'installed-files.txt' in the distributions '.egg-info'
  72. directory.
  73. """
  74. env = get_default_environment()
  75. installed = {dist.canonical_name: dist for dist in env.iter_all_distributions()}
  76. query_names = [canonicalize_name(name) for name in query]
  77. missing = sorted(
  78. [name for name, pkg in zip(query, query_names) if pkg not in installed]
  79. )
  80. if missing:
  81. logger.warning("Package(s) not found: %s", ", ".join(missing))
  82. def _get_requiring_packages(current_dist: BaseDistribution) -> Iterator[str]:
  83. return (
  84. dist.metadata["Name"] or "UNKNOWN"
  85. for dist in installed.values()
  86. if current_dist.canonical_name
  87. in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
  88. )
  89. for query_name in query_names:
  90. try:
  91. dist = installed[query_name]
  92. except KeyError:
  93. continue
  94. try:
  95. requires = sorted(
  96. # Avoid duplicates in requirements (e.g. due to environment markers).
  97. {req.name for req in dist.iter_dependencies()},
  98. key=str.lower,
  99. )
  100. except InvalidRequirement:
  101. requires = sorted(dist.iter_raw_dependencies(), key=str.lower)
  102. try:
  103. required_by = sorted(_get_requiring_packages(dist), key=str.lower)
  104. except InvalidRequirement:
  105. required_by = ["#N/A"]
  106. try:
  107. entry_points_text = dist.read_text("entry_points.txt")
  108. entry_points = entry_points_text.splitlines(keepends=False)
  109. except FileNotFoundError:
  110. entry_points = []
  111. files_iter = dist.iter_declared_entries()
  112. if files_iter is None:
  113. files: list[str] | None = None
  114. else:
  115. files = sorted(files_iter)
  116. metadata = dist.metadata
  117. project_urls = metadata.get_all("Project-URL", [])
  118. homepage = metadata.get("Home-page", "")
  119. if not homepage:
  120. # It's common that there is a "homepage" Project-URL, but Home-page
  121. # remains unset (especially as PEP 621 doesn't surface the field).
  122. for url in project_urls:
  123. url_label, url = url.split(",", maxsplit=1)
  124. normalized_label = normalize_project_url_label(url_label)
  125. if normalized_label == "homepage":
  126. homepage = url.strip()
  127. break
  128. yield _PackageInfo(
  129. name=dist.raw_name,
  130. version=dist.raw_version,
  131. location=dist.location or "",
  132. editable_project_location=dist.editable_project_location,
  133. requires=requires,
  134. required_by=required_by,
  135. installer=dist.installer,
  136. metadata_version=dist.metadata_version or "",
  137. classifiers=metadata.get_all("Classifier", []),
  138. summary=metadata.get("Summary", ""),
  139. homepage=homepage,
  140. project_urls=project_urls,
  141. author=metadata.get("Author", ""),
  142. author_email=metadata.get("Author-email", ""),
  143. license=metadata.get("License", ""),
  144. license_expression=metadata.get("License-Expression", ""),
  145. entry_points=entry_points,
  146. files=files,
  147. )
  148. def print_results(
  149. distributions: Iterable[_PackageInfo],
  150. list_files: bool,
  151. verbose: bool,
  152. ) -> bool:
  153. """
  154. Print the information from installed distributions found.
  155. """
  156. results_printed = False
  157. for i, dist in enumerate(distributions):
  158. results_printed = True
  159. if i > 0:
  160. write_output("---")
  161. metadata_version_tuple = tuple(map(int, dist.metadata_version.split(".")))
  162. write_output("Name: %s", dist.name)
  163. write_output("Version: %s", dist.version)
  164. write_output("Summary: %s", dist.summary)
  165. write_output("Home-page: %s", dist.homepage)
  166. write_output("Author: %s", dist.author)
  167. write_output("Author-email: %s", dist.author_email)
  168. if metadata_version_tuple >= (2, 4) and dist.license_expression:
  169. write_output("License-Expression: %s", dist.license_expression)
  170. else:
  171. write_output("License: %s", dist.license)
  172. write_output("Location: %s", dist.location)
  173. if dist.editable_project_location is not None:
  174. write_output(
  175. "Editable project location: %s", dist.editable_project_location
  176. )
  177. write_output("Requires: %s", ", ".join(dist.requires))
  178. write_output("Required-by: %s", ", ".join(dist.required_by))
  179. if verbose:
  180. write_output("Metadata-Version: %s", dist.metadata_version)
  181. write_output("Installer: %s", dist.installer)
  182. write_output("Classifiers:")
  183. for classifier in dist.classifiers:
  184. write_output(" %s", classifier)
  185. write_output("Entry-points:")
  186. for entry in dist.entry_points:
  187. write_output(" %s", entry.strip())
  188. write_output("Project-URLs:")
  189. for project_url in dist.project_urls:
  190. write_output(" %s", project_url)
  191. if list_files:
  192. write_output("Files:")
  193. if dist.files is None:
  194. write_output("Cannot locate RECORD or installed-files.txt")
  195. else:
  196. for line in dist.files:
  197. write_output(" %s", line.strip())
  198. return results_printed