list.py 11 KB

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