list.py 12 KB

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