list.py 12 KB

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