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