You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

list.py 9.9KB

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