Skip to content

Commit 5dcb6a9

Browse files
committed
Migrate 'pip list' to use metadata abstraction
1 parent 7cae5f2 commit 5dcb6a9

File tree

3 files changed

+99
-51
lines changed

3 files changed

+99
-51
lines changed

src/pip/_internal/commands/list.py

+59-47
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
11
import json
22
import logging
33
from optparse import Values
4-
from typing import Iterator, List, Set, Tuple
4+
from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence, Tuple, cast
55

6-
from pip._vendor.pkg_resources import Distribution
6+
from pip._vendor.packaging.utils import canonicalize_name
77

88
from pip._internal.cli import cmdoptions
99
from pip._internal.cli.req_command import IndexGroupCommand
1010
from pip._internal.cli.status_codes import SUCCESS
1111
from pip._internal.exceptions import CommandError
1212
from pip._internal.index.collector import LinkCollector
1313
from pip._internal.index.package_finder import PackageFinder
14+
from pip._internal.metadata import BaseDistribution, get_environment
1415
from pip._internal.models.selection_prefs import SelectionPreferences
1516
from pip._internal.network.session import PipSession
16-
from pip._internal.utils.compat import stdlib_pkgs
17-
from pip._internal.utils.misc import (
18-
dist_is_editable,
19-
get_installed_distributions,
20-
tabulate,
21-
write_output,
22-
)
23-
from pip._internal.utils.packaging import get_installer
17+
from pip._internal.utils.misc import stdlib_pkgs, tabulate, write_output
2418
from pip._internal.utils.parallel import map_multithread
2519

20+
if 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+
latest_version: DistributionVersion
30+
latest_filetype: str
31+
32+
_ProcessedDists = Sequence[_DistWithLatestInfo]
33+
34+
2635
logger = logging.getLogger(__name__)
2736

2837

@@ -145,14 +154,16 @@ def run(self, options, args):
145154
if options.excludes:
146155
skip.update(options.excludes)
147156

148-
packages = get_installed_distributions(
149-
local_only=options.local,
150-
user_only=options.user,
151-
editables_only=options.editable,
152-
include_editables=options.include_editable,
153-
paths=options.path,
154-
skip=skip,
155-
)
157+
packages: "_ProcessedDists" = [
158+
cast("_DistWithLatestInfo", d)
159+
for d in get_environment(options.path).iter_installed_distributions(
160+
local_only=options.local,
161+
user_only=options.user,
162+
editables_only=options.editable,
163+
include_editables=options.include_editable,
164+
skip=skip,
165+
)
166+
]
156167

157168
# get_not_required must be called firstly in order to find and
158169
# filter out all dependencies correctly. Otherwise a package
@@ -170,45 +181,47 @@ def run(self, options, args):
170181
return SUCCESS
171182

172183
def get_outdated(self, packages, options):
173-
# type: (List[Distribution], Values) -> List[Distribution]
184+
# type: (_ProcessedDists, Values) -> _ProcessedDists
174185
return [
175186
dist for dist in self.iter_packages_latest_infos(packages, options)
176-
if dist.latest_version > dist.parsed_version
187+
if dist.latest_version > dist.version
177188
]
178189

179190
def get_uptodate(self, packages, options):
180-
# type: (List[Distribution], Values) -> List[Distribution]
191+
# type: (_ProcessedDists, Values) -> _ProcessedDists
181192
return [
182193
dist for dist in self.iter_packages_latest_infos(packages, options)
183-
if dist.latest_version == dist.parsed_version
194+
if dist.latest_version == dist.version
184195
]
185196

186197
def get_not_required(self, packages, options):
187-
# type: (List[Distribution], Values) -> List[Distribution]
188-
dep_keys = set() # type: Set[Distribution]
189-
for dist in packages:
190-
dep_keys.update(requirement.key for requirement in dist.requires())
198+
# type: (_ProcessedDists, Values) -> _ProcessedDists
199+
dep_keys = {
200+
canonicalize_name(dep.name)
201+
for dist in packages
202+
for dep in dist.iter_dependencies()
203+
}
191204

192205
# Create a set to remove duplicate packages, and cast it to a list
193206
# to keep the return type consistent with get_outdated and
194207
# get_uptodate
195-
return list({pkg for pkg in packages if pkg.key not in dep_keys})
208+
return list({pkg for pkg in packages if pkg.canonical_name not in dep_keys})
196209

197210
def iter_packages_latest_infos(self, packages, options):
198-
# type: (List[Distribution], Values) -> Iterator[Distribution]
211+
# type: (_ProcessedDists, Values) -> Iterator[_DistWithLatestInfo]
199212
with self._build_session(options) as session:
200213
finder = self._build_package_finder(options, session)
201214

202215
def latest_info(dist):
203-
# type: (Distribution) -> Distribution
204-
all_candidates = finder.find_all_candidates(dist.key)
216+
# type: (_DistWithLatestInfo) -> Optional[_DistWithLatestInfo]
217+
all_candidates = finder.find_all_candidates(dist.canonical_name)
205218
if not options.pre:
206219
# Remove prereleases
207220
all_candidates = [candidate for candidate in all_candidates
208221
if not candidate.version.is_prerelease]
209222

210223
evaluator = finder.make_candidate_evaluator(
211-
project_name=dist.project_name,
224+
project_name=dist.canonical_name,
212225
)
213226
best_candidate = evaluator.sort_best_candidate(all_candidates)
214227
if best_candidate is None:
@@ -219,7 +232,6 @@ def latest_info(dist):
219232
typ = 'wheel'
220233
else:
221234
typ = 'sdist'
222-
# This is dirty but makes the rest of the code much cleaner
223235
dist.latest_version = remote_version
224236
dist.latest_filetype = typ
225237
return dist
@@ -229,21 +241,21 @@ def latest_info(dist):
229241
yield dist
230242

231243
def output_package_listing(self, packages, options):
232-
# type: (List[Distribution], Values) -> None
244+
# type: (_ProcessedDists, Values) -> None
233245
packages = sorted(
234246
packages,
235-
key=lambda dist: dist.project_name.lower(),
247+
key=lambda dist: dist.canonical_name,
236248
)
237249
if options.list_format == 'columns' and packages:
238250
data, header = format_for_columns(packages, options)
239251
self.output_package_listing_columns(data, header)
240252
elif options.list_format == 'freeze':
241253
for dist in packages:
242254
if options.verbose >= 1:
243-
write_output("%s==%s (%s)", dist.project_name,
255+
write_output("%s==%s (%s)", dist.canonical_name,
244256
dist.version, dist.location)
245257
else:
246-
write_output("%s==%s", dist.project_name, dist.version)
258+
write_output("%s==%s", dist.canonical_name, dist.version)
247259
elif options.list_format == 'json':
248260
write_output(format_for_json(packages, options))
249261

@@ -264,7 +276,7 @@ def output_package_listing_columns(self, data, header):
264276

265277

266278
def format_for_columns(pkgs, options):
267-
# type: (List[Distribution], Values) -> Tuple[List[List[str]], List[str]]
279+
# type: (_ProcessedDists, Values) -> Tuple[List[List[str]], List[str]]
268280
"""
269281
Convert the package data into something usable
270282
by output_package_listing_columns.
@@ -277,41 +289,41 @@ def format_for_columns(pkgs, options):
277289
header = ["Package", "Version"]
278290

279291
data = []
280-
if options.verbose >= 1 or any(dist_is_editable(x) for x in pkgs):
292+
if options.verbose >= 1 or any(x.editable for x in pkgs):
281293
header.append("Location")
282294
if options.verbose >= 1:
283295
header.append("Installer")
284296

285297
for proj in pkgs:
286298
# if we're working on the 'outdated' list, separate out the
287299
# latest_version and type
288-
row = [proj.project_name, proj.version]
300+
row = [proj.canonical_name, str(proj.version)]
289301

290302
if running_outdated:
291-
row.append(proj.latest_version)
303+
row.append(str(proj.latest_version))
292304
row.append(proj.latest_filetype)
293305

294-
if options.verbose >= 1 or dist_is_editable(proj):
295-
row.append(proj.location)
306+
if options.verbose >= 1 or proj.editable:
307+
row.append(proj.location or "")
296308
if options.verbose >= 1:
297-
row.append(get_installer(proj))
309+
row.append(proj.installer)
298310

299311
data.append(row)
300312

301313
return data, header
302314

303315

304316
def format_for_json(packages, options):
305-
# type: (List[Distribution], Values) -> str
317+
# type: (_ProcessedDists, Values) -> str
306318
data = []
307319
for dist in packages:
308320
info = {
309-
'name': dist.project_name,
321+
'name': dist.canonical_name,
310322
'version': str(dist.version),
311323
}
312324
if options.verbose >= 1:
313-
info['location'] = dist.location
314-
info['installer'] = get_installer(dist)
325+
info['location'] = dist.location or ""
326+
info['installer'] = dist.installer
315327
if options.outdated:
316328
info['latest_version'] = str(dist.latest_version)
317329
info['latest_filetype'] = dist.latest_filetype

src/pip/_internal/metadata/base.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,32 @@
11
import logging
22
import re
3-
from typing import Container, Iterator, List, Optional, Union
4-
3+
from typing import (
4+
TYPE_CHECKING,
5+
Collection,
6+
Container,
7+
Iterable,
8+
Iterator,
9+
List,
10+
Optional,
11+
Union,
12+
)
13+
14+
from pip._vendor.packaging.requirements import Requirement
515
from pip._vendor.packaging.version import LegacyVersion, Version
616

717
from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here.
818

19+
if TYPE_CHECKING:
20+
from typing import Protocol
21+
else:
22+
Protocol = object
23+
924
DistributionVersion = Union[LegacyVersion, Version]
1025

1126
logger = logging.getLogger(__name__)
1227

1328

14-
class BaseDistribution:
29+
class BaseDistribution(Protocol):
1530
@property
1631
def location(self) -> Optional[str]:
1732
"""Where the distribution is loaded from.
@@ -51,6 +66,10 @@ def local(self) -> bool:
5166
def in_usersite(self) -> bool:
5267
raise NotImplementedError()
5368

69+
def iter_dependencies(self, extras=()):
70+
# type: (Collection[str]) -> Iterable[Requirement]
71+
raise NotImplementedError()
72+
5473

5574
class BaseEnvironment:
5675
"""An environment containing distributions to introspect."""

src/pip/_internal/metadata/pkg_resources.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import logging
12
import zipfile
2-
from typing import Iterator, List, Optional
3+
from typing import Collection, Iterable, Iterator, List, Optional
34

45
from pip._vendor import pkg_resources
6+
from pip._vendor.packaging.requirements import Requirement
57
from pip._vendor.packaging.utils import canonicalize_name
68
from pip._vendor.packaging.version import parse as parse_version
79

@@ -11,6 +13,8 @@
1113

1214
from .base import BaseDistribution, BaseEnvironment, DistributionVersion
1315

16+
logger = logging.getLogger(__name__)
17+
1418

1519
class Distribution(BaseDistribution):
1620
def __init__(self, dist: pkg_resources.Distribution) -> None:
@@ -57,6 +61,19 @@ def local(self) -> bool:
5761
def in_usersite(self) -> bool:
5862
return misc.dist_in_usersite(self._dist)
5963

64+
def iter_dependencies(self, extras=()):
65+
# type: (Collection[str]) -> Iterable[Requirement]
66+
# pkg_resources raises on invalid extras, so we sanitize.
67+
requested_extras = set(extras)
68+
valid_extras = requested_extras & set(self._dist.extras)
69+
for invalid_extra in requested_extras ^ valid_extras:
70+
logger.warning(
71+
"Invalid extra %r for package %r discarded",
72+
invalid_extra,
73+
self.canonical_name,
74+
)
75+
return self._dist.requires(valid_extras)
76+
6077

6178
class Environment(BaseEnvironment):
6279
def __init__(self, ws: pkg_resources.WorkingSet) -> None:

0 commit comments

Comments
 (0)