list.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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. "--pre",
  80. action="store_true",
  81. default=False,
  82. help=(
  83. "Include pre-release and development versions. By default, "
  84. "pip only finds stable versions."
  85. ),
  86. )
  87. self.cmd_opts.add_option(
  88. "--format",
  89. action="store",
  90. dest="list_format",
  91. default="columns",
  92. choices=("columns", "freeze", "json"),
  93. help=(
  94. "Select the output format among: columns (default), freeze, or json. "
  95. "The 'freeze' format cannot be used with the --outdated option."
  96. ),
  97. )
  98. self.cmd_opts.add_option(
  99. "--not-required",
  100. action="store_true",
  101. dest="not_required",
  102. help="List packages that are not dependencies of installed packages.",
  103. )
  104. self.cmd_opts.add_option(
  105. "--exclude-editable",
  106. action="store_false",
  107. dest="include_editable",
  108. help="Exclude editable package from output.",
  109. )
  110. self.cmd_opts.add_option(
  111. "--include-editable",
  112. action="store_true",
  113. dest="include_editable",
  114. help="Include editable package in output.",
  115. default=True,
  116. )
  117. self.cmd_opts.add_option(cmdoptions.list_exclude())
  118. index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser)
  119. self.parser.insert_option_group(0, index_opts)
  120. self.parser.insert_option_group(0, self.cmd_opts)
  121. def handle_pip_version_check(self, options: Values) -> None:
  122. if options.outdated or options.uptodate:
  123. super().handle_pip_version_check(options)
  124. def _build_package_finder(
  125. self, options: Values, session: PipSession
  126. ) -> PackageFinder:
  127. """
  128. Create a package finder appropriate to this list command.
  129. """
  130. # Lazy import the heavy index modules as most list invocations won't need 'em.
  131. from pip._internal.index.collector import LinkCollector
  132. from pip._internal.index.package_finder import PackageFinder
  133. link_collector = LinkCollector.create(session, options=options)
  134. # Pass allow_yanked=False to ignore yanked versions.
  135. selection_prefs = SelectionPreferences(
  136. allow_yanked=False,
  137. allow_all_prereleases=options.pre,
  138. )
  139. return PackageFinder.create(
  140. link_collector=link_collector,
  141. selection_prefs=selection_prefs,
  142. )
  143. def run(self, options: Values, args: list[str]) -> int:
  144. if options.outdated and options.uptodate:
  145. raise CommandError("Options --outdated and --uptodate cannot be combined.")
  146. if options.outdated and options.list_format == "freeze":
  147. raise CommandError(
  148. "List format 'freeze' cannot be used with the --outdated option."
  149. )
  150. cmdoptions.check_list_path_option(options)
  151. skip = set(stdlib_pkgs)
  152. if options.excludes:
  153. skip.update(canonicalize_name(n) for n in options.excludes)
  154. packages: _ProcessedDists = [
  155. cast("_DistWithLatestInfo", d)
  156. for d in get_environment(options.path).iter_installed_distributions(
  157. local_only=options.local,
  158. user_only=options.user,
  159. editables_only=options.editable,
  160. include_editables=options.include_editable,
  161. skip=skip,
  162. )
  163. ]
  164. # get_not_required must be called firstly in order to find and
  165. # filter out all dependencies correctly. Otherwise a package
  166. # can't be identified as requirement because some parent packages
  167. # could be filtered out before.
  168. if options.not_required:
  169. packages = self.get_not_required(packages, options)
  170. if options.outdated:
  171. packages = self.get_outdated(packages, options)
  172. elif options.uptodate:
  173. packages = self.get_uptodate(packages, options)
  174. self.output_package_listing(packages, options)
  175. return SUCCESS
  176. def get_outdated(
  177. self, packages: _ProcessedDists, options: Values
  178. ) -> _ProcessedDists:
  179. return [
  180. dist
  181. for dist in self.iter_packages_latest_infos(packages, options)
  182. if dist.latest_version > dist.version
  183. ]
  184. def get_uptodate(
  185. self, packages: _ProcessedDists, options: Values
  186. ) -> _ProcessedDists:
  187. return [
  188. dist
  189. for dist in self.iter_packages_latest_infos(packages, options)
  190. if dist.latest_version == dist.version
  191. ]
  192. def get_not_required(
  193. self, packages: _ProcessedDists, options: Values
  194. ) -> _ProcessedDists:
  195. dep_keys = {
  196. canonicalize_name(dep.name)
  197. for dist in packages
  198. for dep in (dist.iter_dependencies() or ())
  199. }
  200. # Create a set to remove duplicate packages, and cast it to a list
  201. # to keep the return type consistent with get_outdated and
  202. # get_uptodate
  203. return list({pkg for pkg in packages if pkg.canonical_name not in dep_keys})
  204. def iter_packages_latest_infos(
  205. self, packages: _ProcessedDists, options: Values
  206. ) -> Generator[_DistWithLatestInfo, None, None]:
  207. with self._build_session(options) as session:
  208. finder = self._build_package_finder(options, session)
  209. def latest_info(
  210. dist: _DistWithLatestInfo,
  211. ) -> _DistWithLatestInfo | None:
  212. all_candidates = finder.find_all_candidates(dist.canonical_name)
  213. if not options.pre:
  214. # Remove prereleases
  215. all_candidates = [
  216. candidate
  217. for candidate in all_candidates
  218. if not candidate.version.is_prerelease
  219. ]
  220. evaluator = finder.make_candidate_evaluator(
  221. project_name=dist.canonical_name,
  222. )
  223. best_candidate = evaluator.sort_best_candidate(all_candidates)
  224. if best_candidate is None:
  225. return None
  226. remote_version = best_candidate.version
  227. if best_candidate.link.is_wheel:
  228. typ = "wheel"
  229. else:
  230. typ = "sdist"
  231. dist.latest_version = remote_version
  232. dist.latest_filetype = typ
  233. return dist
  234. for dist in map(latest_info, packages):
  235. if dist is not None:
  236. yield dist
  237. def output_package_listing(
  238. self, packages: _ProcessedDists, options: Values
  239. ) -> None:
  240. packages = sorted(
  241. packages,
  242. key=lambda dist: dist.canonical_name,
  243. )
  244. if options.list_format == "columns" and packages:
  245. data, header = format_for_columns(packages, options)
  246. self.output_package_listing_columns(data, header)
  247. elif options.list_format == "freeze":
  248. for dist in packages:
  249. try:
  250. req_string = f"{dist.raw_name}=={dist.version}"
  251. except InvalidVersion:
  252. req_string = f"{dist.raw_name}==={dist.raw_version}"
  253. if options.verbose >= 1:
  254. write_output("%s (%s)", req_string, dist.location)
  255. else:
  256. write_output(req_string)
  257. elif options.list_format == "json":
  258. write_output(format_for_json(packages, options))
  259. def output_package_listing_columns(
  260. self, data: list[list[str]], header: list[str]
  261. ) -> None:
  262. # insert the header first: we need to know the size of column names
  263. if len(data) > 0:
  264. data.insert(0, header)
  265. pkg_strings, sizes = tabulate(data)
  266. # Create and add a separator.
  267. if len(data) > 0:
  268. pkg_strings.insert(1, " ".join("-" * x for x in sizes))
  269. for val in pkg_strings:
  270. write_output(val)
  271. def format_for_columns(
  272. pkgs: _ProcessedDists, options: Values
  273. ) -> tuple[list[list[str]], list[str]]:
  274. """
  275. Convert the package data into something usable
  276. by output_package_listing_columns.
  277. """
  278. header = ["Package", "Version"]
  279. running_outdated = options.outdated
  280. if running_outdated:
  281. header.extend(["Latest", "Type"])
  282. def wheel_build_tag(dist: BaseDistribution) -> str | None:
  283. try:
  284. wheel_file = dist.read_text("WHEEL")
  285. except FileNotFoundError:
  286. return None
  287. return Parser().parsestr(wheel_file).get("Build")
  288. build_tags = [wheel_build_tag(p) for p in pkgs]
  289. has_build_tags = any(build_tags)
  290. if has_build_tags:
  291. header.append("Build")
  292. if options.verbose >= 1:
  293. header.append("Location")
  294. if options.verbose >= 1:
  295. header.append("Installer")
  296. has_editables = any(x.editable for x in pkgs)
  297. if has_editables:
  298. header.append("Editable project location")
  299. data = []
  300. for i, proj in enumerate(pkgs):
  301. # if we're working on the 'outdated' list, separate out the
  302. # latest_version and type
  303. row = [proj.raw_name, proj.raw_version]
  304. if running_outdated:
  305. row.append(str(proj.latest_version))
  306. row.append(proj.latest_filetype)
  307. if has_build_tags:
  308. row.append(build_tags[i] or "")
  309. if has_editables:
  310. row.append(proj.editable_project_location or "")
  311. if options.verbose >= 1:
  312. row.append(proj.location or "")
  313. if options.verbose >= 1:
  314. row.append(proj.installer)
  315. data.append(row)
  316. return data, header
  317. def format_for_json(packages: _ProcessedDists, options: Values) -> str:
  318. data = []
  319. for dist in packages:
  320. try:
  321. version = str(dist.version)
  322. except InvalidVersion:
  323. version = dist.raw_version
  324. info = {
  325. "name": dist.raw_name,
  326. "version": version,
  327. }
  328. if options.verbose >= 1:
  329. info["location"] = dist.location or ""
  330. info["installer"] = dist.installer
  331. if options.outdated:
  332. info["latest_version"] = str(dist.latest_version)
  333. info["latest_filetype"] = dist.latest_filetype
  334. editable_project_location = dist.editable_project_location
  335. if editable_project_location:
  336. info["editable_project_location"] = editable_project_location
  337. data.append(info)
  338. return json.dumps(data)