list.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. # The following comment should be removed at some point in the future.
  2. # mypy: disallow-untyped-defs=False
  3. from __future__ import absolute_import
  4. import json
  5. import logging
  6. from pip._vendor import six
  7. from pip._vendor.six.moves import zip_longest
  8. from pip._internal.cli import cmdoptions
  9. from pip._internal.cli.req_command import IndexGroupCommand
  10. from pip._internal.exceptions import CommandError
  11. from pip._internal.index import PackageFinder
  12. from pip._internal.models.selection_prefs import SelectionPreferences
  13. from pip._internal.self_outdated_check import make_link_collector
  14. from pip._internal.utils.misc import (
  15. dist_is_editable,
  16. get_installed_distributions,
  17. write_output,
  18. )
  19. from pip._internal.utils.packaging import get_installer
  20. logger = logging.getLogger(__name__)
  21. class ListCommand(IndexGroupCommand):
  22. """
  23. List installed packages, including editables.
  24. Packages are listed in a case-insensitive sorted order.
  25. """
  26. usage = """
  27. %prog [options]"""
  28. def __init__(self, *args, **kw):
  29. super(ListCommand, self).__init__(*args, **kw)
  30. cmd_opts = self.cmd_opts
  31. cmd_opts.add_option(
  32. '-o', '--outdated',
  33. action='store_true',
  34. default=False,
  35. help='List outdated packages')
  36. cmd_opts.add_option(
  37. '-u', '--uptodate',
  38. action='store_true',
  39. default=False,
  40. help='List uptodate packages')
  41. cmd_opts.add_option(
  42. '-e', '--editable',
  43. action='store_true',
  44. default=False,
  45. help='List editable projects.')
  46. cmd_opts.add_option(
  47. '-l', '--local',
  48. action='store_true',
  49. default=False,
  50. help=('If in a virtualenv that has global access, do not list '
  51. 'globally-installed packages.'),
  52. )
  53. self.cmd_opts.add_option(
  54. '--user',
  55. dest='user',
  56. action='store_true',
  57. default=False,
  58. help='Only output packages installed in user-site.')
  59. cmd_opts.add_option(cmdoptions.list_path())
  60. cmd_opts.add_option(
  61. '--pre',
  62. action='store_true',
  63. default=False,
  64. help=("Include pre-release and development versions. By default, "
  65. "pip only finds stable versions."),
  66. )
  67. cmd_opts.add_option(
  68. '--format',
  69. action='store',
  70. dest='list_format',
  71. default="columns",
  72. choices=('columns', 'freeze', 'json'),
  73. help="Select the output format among: columns (default), freeze, "
  74. "or json",
  75. )
  76. cmd_opts.add_option(
  77. '--not-required',
  78. action='store_true',
  79. dest='not_required',
  80. help="List packages that are not dependencies of "
  81. "installed packages.",
  82. )
  83. cmd_opts.add_option(
  84. '--exclude-editable',
  85. action='store_false',
  86. dest='include_editable',
  87. help='Exclude editable package from output.',
  88. )
  89. cmd_opts.add_option(
  90. '--include-editable',
  91. action='store_true',
  92. dest='include_editable',
  93. help='Include editable package from output.',
  94. default=True,
  95. )
  96. index_opts = cmdoptions.make_option_group(
  97. cmdoptions.index_group, self.parser
  98. )
  99. self.parser.insert_option_group(0, index_opts)
  100. self.parser.insert_option_group(0, cmd_opts)
  101. def _build_package_finder(self, options, session):
  102. """
  103. Create a package finder appropriate to this list command.
  104. """
  105. link_collector = make_link_collector(session, options=options)
  106. # Pass allow_yanked=False to ignore yanked versions.
  107. selection_prefs = SelectionPreferences(
  108. allow_yanked=False,
  109. allow_all_prereleases=options.pre,
  110. )
  111. return PackageFinder.create(
  112. link_collector=link_collector,
  113. selection_prefs=selection_prefs,
  114. )
  115. def run(self, options, args):
  116. if options.outdated and options.uptodate:
  117. raise CommandError(
  118. "Options --outdated and --uptodate cannot be combined.")
  119. cmdoptions.check_list_path_option(options)
  120. packages = get_installed_distributions(
  121. local_only=options.local,
  122. user_only=options.user,
  123. editables_only=options.editable,
  124. include_editables=options.include_editable,
  125. paths=options.path,
  126. )
  127. # get_not_required must be called firstly in order to find and
  128. # filter out all dependencies correctly. Otherwise a package
  129. # can't be identified as requirement because some parent packages
  130. # could be filtered out before.
  131. if options.not_required:
  132. packages = self.get_not_required(packages, options)
  133. if options.outdated:
  134. packages = self.get_outdated(packages, options)
  135. elif options.uptodate:
  136. packages = self.get_uptodate(packages, options)
  137. self.output_package_listing(packages, options)
  138. def get_outdated(self, packages, options):
  139. return [
  140. dist for dist in self.iter_packages_latest_infos(packages, options)
  141. if dist.latest_version > dist.parsed_version
  142. ]
  143. def get_uptodate(self, packages, options):
  144. return [
  145. dist for dist in self.iter_packages_latest_infos(packages, options)
  146. if dist.latest_version == dist.parsed_version
  147. ]
  148. def get_not_required(self, packages, options):
  149. dep_keys = set()
  150. for dist in packages:
  151. dep_keys.update(requirement.key for requirement in dist.requires())
  152. return {pkg for pkg in packages if pkg.key not in dep_keys}
  153. def iter_packages_latest_infos(self, packages, options):
  154. with self._build_session(options) as session:
  155. finder = self._build_package_finder(options, session)
  156. for dist in packages:
  157. typ = 'unknown'
  158. all_candidates = finder.find_all_candidates(dist.key)
  159. if not options.pre:
  160. # Remove prereleases
  161. all_candidates = [candidate for candidate in all_candidates
  162. if not candidate.version.is_prerelease]
  163. evaluator = finder.make_candidate_evaluator(
  164. project_name=dist.project_name,
  165. )
  166. best_candidate = evaluator.sort_best_candidate(all_candidates)
  167. if best_candidate is None:
  168. continue
  169. remote_version = best_candidate.version
  170. if best_candidate.link.is_wheel:
  171. typ = 'wheel'
  172. else:
  173. typ = 'sdist'
  174. # This is dirty but makes the rest of the code much cleaner
  175. dist.latest_version = remote_version
  176. dist.latest_filetype = typ
  177. yield dist
  178. def output_package_listing(self, packages, options):
  179. packages = sorted(
  180. packages,
  181. key=lambda dist: dist.project_name.lower(),
  182. )
  183. if options.list_format == 'columns' and packages:
  184. data, header = format_for_columns(packages, options)
  185. self.output_package_listing_columns(data, header)
  186. elif options.list_format == 'freeze':
  187. for dist in packages:
  188. if options.verbose >= 1:
  189. write_output("%s==%s (%s)", dist.project_name,
  190. dist.version, dist.location)
  191. else:
  192. write_output("%s==%s", dist.project_name, dist.version)
  193. elif options.list_format == 'json':
  194. write_output(format_for_json(packages, options))
  195. def output_package_listing_columns(self, data, header):
  196. # insert the header first: we need to know the size of column names
  197. if len(data) > 0:
  198. data.insert(0, header)
  199. pkg_strings, sizes = tabulate(data)
  200. # Create and add a separator.
  201. if len(data) > 0:
  202. pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes)))
  203. for val in pkg_strings:
  204. write_output(val)
  205. def tabulate(vals):
  206. # From pfmoore on GitHub:
  207. # https://github.com/pypa/pip/issues/3651#issuecomment-216932564
  208. assert len(vals) > 0
  209. sizes = [0] * max(len(x) for x in vals)
  210. for row in vals:
  211. sizes = [max(s, len(str(c))) for s, c in zip_longest(sizes, row)]
  212. result = []
  213. for row in vals:
  214. display = " ".join([str(c).ljust(s) if c is not None else ''
  215. for s, c in zip_longest(sizes, row)])
  216. result.append(display)
  217. return result, sizes
  218. def format_for_columns(pkgs, options):
  219. """
  220. Convert the package data into something usable
  221. by output_package_listing_columns.
  222. """
  223. running_outdated = options.outdated
  224. # Adjust the header for the `pip list --outdated` case.
  225. if running_outdated:
  226. header = ["Package", "Version", "Latest", "Type"]
  227. else:
  228. header = ["Package", "Version"]
  229. data = []
  230. if options.verbose >= 1 or any(dist_is_editable(x) for x in pkgs):
  231. header.append("Location")
  232. if options.verbose >= 1:
  233. header.append("Installer")
  234. for proj in pkgs:
  235. # if we're working on the 'outdated' list, separate out the
  236. # latest_version and type
  237. row = [proj.project_name, proj.version]
  238. if running_outdated:
  239. row.append(proj.latest_version)
  240. row.append(proj.latest_filetype)
  241. if options.verbose >= 1 or dist_is_editable(proj):
  242. row.append(proj.location)
  243. if options.verbose >= 1:
  244. row.append(get_installer(proj))
  245. data.append(row)
  246. return data, header
  247. def format_for_json(packages, options):
  248. data = []
  249. for dist in packages:
  250. info = {
  251. 'name': dist.project_name,
  252. 'version': six.text_type(dist.version),
  253. }
  254. if options.verbose >= 1:
  255. info['location'] = dist.location
  256. info['installer'] = get_installer(dist)
  257. if options.outdated:
  258. info['latest_version'] = six.text_type(dist.latest_version)
  259. info['latest_filetype'] = dist.latest_filetype
  260. data.append(info)
  261. return json.dumps(data)