list.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. from __future__ import annotations
  2. import json
  3. import logging
  4. from collections.abc import Generator, Sequence
  5. from email.parser import Parser
  6. from optparse import Values
  7. from typing import TYPE_CHECKING, cast
  8. from pip._vendor.packaging.utils import canonicalize_name
  9. from pip._vendor.packaging.version import InvalidVersion, Version
  10. from pip._internal.cli import cmdoptions
  11. from pip._internal.cli.index_command import IndexGroupCommand
  12. from pip._internal.cli.status_codes import SUCCESS
  13. from pip._internal.exceptions import CommandError
  14. from pip._internal.metadata import BaseDistribution, get_environment
  15. from pip._internal.models.selection_prefs import SelectionPreferences
  16. from pip._internal.utils.compat import stdlib_pkgs
  17. from pip._internal.utils.misc import tabulate, write_output
  18. if TYPE_CHECKING:
  19. from pip._internal.index.package_finder import PackageFinder
  20. from pip._internal.network.session import PipSession
  21. class _DistWithLatestInfo(BaseDistribution):
  22. """Give the distribution object a couple of extra fields.
  23. These will be populated during ``get_outdated()``. This is dirty but
  24. makes the rest of the code much cleaner.
  25. """
  26. latest_version: Version
  27. latest_filetype: str
  28. _ProcessedDists = Sequence[_DistWithLatestInfo]
  29. logger = logging.getLogger(__name__)
  30. class ListCommand(IndexGroupCommand):
  31. """
  32. List installed packages, including editables.
  33. Packages are listed in a case-insensitive sorted order.
  34. """
  35. ignore_require_venv = True
  36. usage = """
  37. %prog [options]"""
  38. def add_options(self) -> None:
  39. self.cmd_opts.add_option(
  40. "-o",
  41. "--outdated",
  42. action="store_true",
  43. default=False,
  44. help="List outdated packages",
  45. )
  46. self.cmd_opts.add_option(
  47. "-u",
  48. "--uptodate",
  49. action="store_true",
  50. default=False,
  51. help="List uptodate packages",
  52. )
  53. self.cmd_opts.add_option(
  54. "-e",
  55. "--editable",
  56. action="store_true",
  57. default=False,
  58. help="List editable projects.",
  59. )
  60. self.cmd_opts.add_option(
  61. "-l",
  62. "--local",
  63. action="store_true",
  64. default=False,
  65. help=(
  66. "If in a virtualenv that has global access, do not list "
  67. "globally-installed packages."
  68. ),
  69. )
  70. self.cmd_opts.add_option(
  71. "--user",
  72. dest="user",
  73. action="store_true",
  74. default=False,
  75. help="Only output packages installed in user-site.",
  76. )
  77. self.cmd_opts.add_option(cmdoptions.list_path())
  78. self.cmd_opts.add_option(
  79. "--format",
  80. action="store",
  81. dest="list_format",
  82. default="columns",
  83. choices=("columns", "freeze", "json"),
  84. help=(
  85. "Select the output format among: columns (default), freeze, or json. "
  86. "The 'freeze' format cannot be used with the --outdated option."
  87. ),
  88. )
  89. self.cmd_opts.add_option(
  90. "--not-required",
  91. action="store_true",
  92. dest="not_required",
  93. help="List packages that are not dependencies of installed packages.",
  94. )
  95. self.cmd_opts.add_option(
  96. "--exclude-editable",
  97. action="store_false",
  98. dest="include_editable",
  99. help="Exclude editable package from output.",
  100. )
  101. self.cmd_opts.add_option(
  102. "--include-editable",
  103. action="store_true",
  104. dest="include_editable",
  105. help="Include editable package in output.",
  106. default=True,
  107. )
  108. self.cmd_opts.add_option(cmdoptions.list_exclude())
  109. index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser)
  110. selection_opts = cmdoptions.make_option_group(
  111. cmdoptions.package_selection_group,
  112. self.parser,
  113. )
  114. self.parser.insert_option_group(0, index_opts)
  115. self.parser.insert_option_group(0, selection_opts)
  116. self.parser.insert_option_group(0, self.cmd_opts)
  117. def handle_pip_version_check(self, options: Values) -> None:
  118. if options.outdated or options.uptodate:
  119. super().handle_pip_version_check(options)
  120. def _build_package_finder(
  121. self, options: Values, session: PipSession
  122. ) -> PackageFinder:
  123. """
  124. Create a package finder appropriate to this list command.
  125. """
  126. # Lazy import the heavy index modules as most list invocations won't need 'em.
  127. from pip._internal.index.collector import LinkCollector
  128. from pip._internal.index.package_finder import PackageFinder
  129. link_collector = LinkCollector.create(session, options=options)
  130. # Pass allow_yanked=False to ignore yanked versions.
  131. selection_prefs = SelectionPreferences(
  132. allow_yanked=False,
  133. release_control=options.release_control,
  134. )
  135. return PackageFinder.create(
  136. link_collector=link_collector,
  137. selection_prefs=selection_prefs,
  138. )
  139. def run(self, options: Values, args: list[str]) -> int:
  140. cmdoptions.check_release_control_exclusive(options)
  141. if options.outdated and options.uptodate:
  142. raise CommandError("Options --outdated and --uptodate cannot be combined.")
  143. if options.outdated and options.list_format == "freeze":
  144. raise CommandError(
  145. "List format 'freeze' cannot be used with the --outdated option."
  146. )
  147. cmdoptions.check_list_path_option(options)
  148. skip = set(stdlib_pkgs)
  149. if options.excludes:
  150. skip.update(canonicalize_name(n) for n in options.excludes)
  151. packages: _ProcessedDists = [
  152. cast("_DistWithLatestInfo", d)
  153. for d in get_environment(options.path).iter_installed_distributions(
  154. local_only=options.local,
  155. user_only=options.user,
  156. editables_only=options.editable,
  157. include_editables=options.include_editable,
  158. skip=skip,
  159. )
  160. ]
  161. # get_not_required must be called firstly in order to find and
  162. # filter out all dependencies correctly. Otherwise a package
  163. # can't be identified as requirement because some parent packages
  164. # could be filtered out before.
  165. if options.not_required:
  166. packages = self.get_not_required(packages, options)
  167. if options.outdated:
  168. packages = self.get_outdated(packages, options)
  169. elif options.uptodate:
  170. packages = self.get_uptodate(packages, options)
  171. self.output_package_listing(packages, options)
  172. return SUCCESS
  173. def get_outdated(
  174. self, packages: _ProcessedDists, options: Values
  175. ) -> _ProcessedDists:
  176. return [
  177. dist
  178. for dist in self.iter_packages_latest_infos(packages, options)
  179. if dist.latest_version > dist.version
  180. ]
  181. def get_uptodate(
  182. self, packages: _ProcessedDists, options: Values
  183. ) -> _ProcessedDists:
  184. return [
  185. dist
  186. for dist in self.iter_packages_latest_infos(packages, options)
  187. if dist.latest_version == dist.version
  188. ]
  189. def get_not_required(
  190. self, packages: _ProcessedDists, options: Values
  191. ) -> _ProcessedDists:
  192. dep_keys = {
  193. canonicalize_name(dep.name)
  194. for dist in packages
  195. for dep in (dist.iter_dependencies() or ())
  196. }
  197. # Create a set to remove duplicate packages, and cast it to a list
  198. # to keep the return type consistent with get_outdated and
  199. # get_uptodate
  200. return list({pkg for pkg in packages if pkg.canonical_name not in dep_keys})
  201. def iter_packages_latest_infos(
  202. self, packages: _ProcessedDists, options: Values
  203. ) -> Generator[_DistWithLatestInfo, None, None]:
  204. with self._build_session(options) as session:
  205. finder = self._build_package_finder(options, session)
  206. def latest_info(
  207. dist: _DistWithLatestInfo,
  208. ) -> _DistWithLatestInfo | None:
  209. all_candidates = finder.find_all_candidates(dist.canonical_name)
  210. if self.should_exclude_prerelease(options, dist.canonical_name):
  211. all_candidates = [
  212. candidate
  213. for candidate in all_candidates
  214. if not candidate.version.is_prerelease
  215. ]
  216. evaluator = finder.make_candidate_evaluator(
  217. project_name=dist.canonical_name,
  218. )
  219. best_candidate = evaluator.sort_best_candidate(all_candidates)
  220. if best_candidate is None:
  221. return None
  222. remote_version = best_candidate.version
  223. if best_candidate.link.is_wheel:
  224. typ = "wheel"
  225. else:
  226. typ = "sdist"
  227. dist.latest_version = remote_version
  228. dist.latest_filetype = typ
  229. return dist
  230. for dist in map(latest_info, packages):
  231. if dist is not None:
  232. yield dist
  233. def output_package_listing(
  234. self, packages: _ProcessedDists, options: Values
  235. ) -> None:
  236. packages = sorted(
  237. packages,
  238. key=lambda dist: dist.canonical_name,
  239. )
  240. if options.list_format == "columns" and packages:
  241. data, header = format_for_columns(packages, options)
  242. self.output_package_listing_columns(data, header)
  243. elif options.list_format == "freeze":
  244. for dist in packages:
  245. try:
  246. req_string = f"{dist.raw_name}=={dist.version}"
  247. except InvalidVersion:
  248. req_string = f"{dist.raw_name}==={dist.raw_version}"
  249. if options.verbose >= 1:
  250. write_output("%s (%s)", req_string, dist.location)
  251. else:
  252. write_output(req_string)
  253. elif options.list_format == "json":
  254. write_output(format_for_json(packages, options))
  255. def output_package_listing_columns(
  256. self, data: list[list[str]], header: list[str]
  257. ) -> None:
  258. # insert the header first: we need to know the size of column names
  259. if len(data) > 0:
  260. data.insert(0, header)
  261. pkg_strings, sizes = tabulate(data)
  262. # Create and add a separator.
  263. if len(data) > 0:
  264. pkg_strings.insert(1, " ".join("-" * x for x in sizes))
  265. for val in pkg_strings:
  266. write_output(val)
  267. def format_for_columns(
  268. pkgs: _ProcessedDists, options: Values
  269. ) -> tuple[list[list[str]], list[str]]:
  270. """
  271. Convert the package data into something usable
  272. by output_package_listing_columns.
  273. """
  274. header = ["Package", "Version"]
  275. running_outdated = options.outdated
  276. if running_outdated:
  277. header.extend(["Latest", "Type"])
  278. def wheel_build_tag(dist: BaseDistribution) -> str | None:
  279. try:
  280. wheel_file = dist.read_text("WHEEL")
  281. except FileNotFoundError:
  282. return None
  283. return Parser().parsestr(wheel_file).get("Build")
  284. build_tags = [wheel_build_tag(p) for p in pkgs]
  285. has_build_tags = any(build_tags)
  286. if has_build_tags:
  287. header.append("Build")
  288. has_editables = any(x.editable for x in pkgs)
  289. if has_editables:
  290. header.append("Editable project location")
  291. if options.verbose >= 1:
  292. header.append("Location")
  293. if options.verbose >= 1:
  294. header.append("Installer")
  295. data = []
  296. for i, proj in enumerate(pkgs):
  297. # if we're working on the 'outdated' list, separate out the
  298. # latest_version and type
  299. row = [proj.raw_name, proj.raw_version]
  300. if running_outdated:
  301. row.append(str(proj.latest_version))
  302. row.append(proj.latest_filetype)
  303. if has_build_tags:
  304. row.append(build_tags[i] or "")
  305. if has_editables:
  306. row.append(proj.editable_project_location or "")
  307. if options.verbose >= 1:
  308. row.append(proj.location or "")
  309. if options.verbose >= 1:
  310. row.append(proj.installer)
  311. data.append(row)
  312. return data, header
  313. def format_for_json(packages: _ProcessedDists, options: Values) -> str:
  314. data = []
  315. for dist in packages:
  316. try:
  317. version = str(dist.version)
  318. except InvalidVersion:
  319. version = dist.raw_version
  320. info = {
  321. "name": dist.raw_name,
  322. "version": version,
  323. }
  324. if options.verbose >= 1:
  325. info["location"] = dist.location or ""
  326. info["installer"] = dist.installer
  327. if options.outdated:
  328. info["latest_version"] = str(dist.latest_version)
  329. info["latest_filetype"] = dist.latest_filetype
  330. editable_project_location = dist.editable_project_location
  331. if editable_project_location:
  332. info["editable_project_location"] = editable_project_location
  333. data.append(info)
  334. return json.dumps(data)