Skip to content

Commit 033576f

Browse files
committed
Remove direct pkg_resources usages in req_install
This also by extension converts req_uninstall since the two are very much coupled together.
1 parent 135faab commit 033576f

File tree

17 files changed

+295
-275
lines changed

17 files changed

+295
-275
lines changed

src/pip/_internal/commands/show.py

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import csv
21
import logging
3-
import pathlib
42
from optparse import Values
5-
from typing import Iterator, List, NamedTuple, Optional, Tuple
3+
from typing import Iterator, List, NamedTuple, Optional
64

75
from pip._vendor.packaging.utils import canonicalize_name
86

@@ -69,33 +67,6 @@ class _PackageInfo(NamedTuple):
6967
files: Optional[List[str]]
7068

7169

72-
def _convert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str:
73-
"""Convert a legacy installed-files.txt path into modern RECORD path.
74-
75-
The legacy format stores paths relative to the info directory, while the
76-
modern format stores paths relative to the package root, e.g. the
77-
site-packages directory.
78-
79-
:param entry: Path parts of the installed-files.txt entry.
80-
:param info: Path parts of the egg-info directory relative to package root.
81-
:returns: The converted entry.
82-
83-
For best compatibility with symlinks, this does not use ``abspath()`` or
84-
``Path.resolve()``, but tries to work with path parts:
85-
86-
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
87-
from ``info``; if ``info`` is empty, start appending ``..`` instead.
88-
2. Join the two directly.
89-
"""
90-
while entry and entry[0] == "..":
91-
if not info or info[-1] == "..":
92-
info += ("..",)
93-
else:
94-
info = info[:-1]
95-
entry = entry[1:]
96-
return str(pathlib.Path(*info, *entry))
97-
98-
9970
def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
10071
"""
10172
Gather details from installed distributions. Print distribution name,
@@ -121,34 +92,6 @@ def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]:
12192
in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
12293
]
12394

124-
def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]:
125-
try:
126-
text = dist.read_text("RECORD")
127-
except FileNotFoundError:
128-
return None
129-
# This extra Path-str cast normalizes entries.
130-
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
131-
132-
def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
133-
try:
134-
text = dist.read_text("installed-files.txt")
135-
except FileNotFoundError:
136-
return None
137-
paths = (p for p in text.splitlines(keepends=False) if p)
138-
root = dist.location
139-
info = dist.info_directory
140-
if root is None or info is None:
141-
return paths
142-
try:
143-
info_rel = pathlib.Path(info).relative_to(root)
144-
except ValueError: # info is not relative to root.
145-
return paths
146-
if not info_rel.parts: # info *is* root.
147-
return paths
148-
return (
149-
_convert_legacy_entry(pathlib.Path(p).parts, info_rel.parts) for p in paths
150-
)
151-
15295
for query_name in query_names:
15396
try:
15497
dist = installed[query_name]
@@ -161,11 +104,11 @@ def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
161104
except FileNotFoundError:
162105
entry_points = []
163106

164-
files_iter = _files_from_record(dist) or _files_from_legacy(dist)
107+
files_iter = dist.iter_files()
165108
if files_iter is None:
166109
files: Optional[List[str]] = None
167110
else:
168-
files = sorted(files_iter)
111+
files = sorted(str(f) for f in files_iter)
169112

170113
metadata = dist.metadata
171114

src/pip/_internal/distributions/sdist.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ class SourceDistribution(AbstractDistribution):
1919
"""
2020

2121
def get_metadata_distribution(self) -> BaseDistribution:
22-
from pip._internal.metadata.pkg_resources import Distribution as _Dist
23-
24-
return _Dist(self.req.get_dist())
22+
return self.req.get_dist()
2523

2624
def prepare_distribution_metadata(
2725
self, finder: PackageFinder, build_isolation: bool

src/pip/_internal/distributions/wheel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pip._internal.metadata import (
66
BaseDistribution,
77
FilesystemWheel,
8-
get_wheel_distribution,
8+
get_distribution_for_wheel,
99
)
1010

1111

@@ -23,7 +23,7 @@ def get_metadata_distribution(self) -> BaseDistribution:
2323
assert self.req.local_file_path, "Set as part of preparation during download"
2424
assert self.req.name, "Wheels are never unnamed"
2525
wheel = FilesystemWheel(self.req.local_file_path)
26-
return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
26+
return get_distribution_for_wheel(wheel, canonicalize_name(self.req.name))
2727

2828
def prepare_distribution_metadata(
2929
self, finder: PackageFinder, build_isolation: bool

src/pip/_internal/index/package_finder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ def find_requirement(
881881

882882
installed_version: Optional[_BaseVersion] = None
883883
if req.satisfied_by is not None:
884-
installed_version = parse_version(req.satisfied_by.version)
884+
installed_version = req.satisfied_by.version
885885

886886
def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
887887
# This repeated parse_version and str() conversion is needed to

src/pip/_internal/metadata/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
3838
return Environment.from_paths(paths)
3939

4040

41-
def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
41+
def get_distribution_for_wheel(wheel: Wheel, canonical_name: str) -> BaseDistribution:
4242
"""Get the representation of the specified wheel's distribution metadata.
4343
4444
This returns a Distribution instance from the chosen backend based on
@@ -49,3 +49,14 @@ def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistributio
4949
from .pkg_resources import Distribution
5050

5151
return Distribution.from_wheel(wheel, canonical_name)
52+
53+
54+
def get_distribution_for_info_directory(directory_path: str) -> BaseDistribution:
55+
"""Get the specified info directory's distribution representation.
56+
57+
The directory should be an on-disk ``NAME-VERSION.dist-info`` or
58+
``NAME.egg-info`` directory.
59+
"""
60+
from .pkg_resources import Distribution
61+
62+
return Distribution.from_info_directory(directory_path)

src/pip/_internal/metadata/base.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import csv
12
import email.message
23
import json
34
import logging
5+
import pathlib
46
import re
57
import zipfile
68
from typing import (
@@ -12,6 +14,7 @@
1214
Iterator,
1315
List,
1416
Optional,
17+
Tuple,
1518
Union,
1619
)
1720

@@ -38,6 +41,33 @@
3841
logger = logging.getLogger(__name__)
3942

4043

44+
def _convert_legacy_file(entry: Tuple[str, ...], info: Tuple[str, ...]) -> pathlib.Path:
45+
"""Convert a legacy installed-files.txt path into modern RECORD path.
46+
47+
The legacy format stores paths relative to the info directory, while the
48+
modern format stores paths relative to the package root, e.g. the
49+
site-packages directory.
50+
51+
:param entry: Path parts of the installed-files.txt entry.
52+
:param info: Path parts of the egg-info directory relative to package root.
53+
:returns: The converted entry.
54+
55+
For best compatibility with symlinks, this does not use ``abspath()`` or
56+
``Path.resolve()``, but tries to work with path parts:
57+
58+
1. While ``entry`` starts with ``..``, remove the equal amounts of parts
59+
from ``info``; if ``info`` is empty, start appending ``..`` instead.
60+
2. Join the two directly.
61+
"""
62+
while entry and entry[0] == "..":
63+
if not info or info[-1] == "..":
64+
info += ("..",)
65+
else:
66+
info = info[:-1]
67+
entry = entry[1:]
68+
return pathlib.Path(*info, *entry)
69+
70+
4171
class BaseEntryPoint(Protocol):
4272
@property
4373
def name(self) -> str:
@@ -127,6 +157,17 @@ def direct_url(self) -> Optional[DirectUrl]:
127157
def installer(self) -> str:
128158
raise NotImplementedError()
129159

160+
@property
161+
def egg_link(self) -> Optional[str]:
162+
"""Location of the ``.egg-link`` for this distribution.
163+
164+
If there's not a matching file, None is returned. Note that finding this
165+
file does not necessarily mean the currently-installed distribution is
166+
editable since the ``.egg-link`` can still be shadowed by a
167+
non-editable installation located in front of it in ``sys.path``.
168+
"""
169+
raise NotImplementedError()
170+
130171
@property
131172
def editable(self) -> bool:
132173
raise NotImplementedError()
@@ -146,11 +187,20 @@ def in_site_packages(self) -> bool:
146187
def read_text(self, name: str) -> str:
147188
"""Read a file in the .dist-info (or .egg-info) directory.
148189
149-
Should raise ``FileNotFoundError`` if ``name`` does not exist in the
150-
metadata directory.
190+
:raises FileNotFoundError: ``name`` does not exist in the info directory.
151191
"""
152192
raise NotImplementedError()
153193

194+
def iterdir(self, name: str) -> Iterable[pathlib.PurePosixPath]:
195+
"""Iterate through a directory in the info directory.
196+
197+
Each item is a path relative to the info directory.
198+
199+
:raises FileNotFoundError: ``name`` does not exist in the info directory.
200+
:raises NotADirectoryError: ``name`` exists in the info directory, but
201+
is not a directory.
202+
"""
203+
154204
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
155205
raise NotImplementedError()
156206

@@ -206,6 +256,43 @@ def iter_provided_extras(self) -> Iterable[str]:
206256
"""
207257
raise NotImplementedError()
208258

259+
def _iter_files_from_legacy(self) -> Optional[Iterator[pathlib.Path]]:
260+
try:
261+
text = self.read_text("installed-files.txt")
262+
except FileNotFoundError:
263+
return None
264+
paths = (pathlib.Path(p) for p in text.splitlines(keepends=False) if p)
265+
root = self.location
266+
info = self.info_directory
267+
if root is None or info is None:
268+
return paths
269+
try:
270+
rel = pathlib.Path(info).relative_to(root)
271+
except ValueError: # info is not relative to root.
272+
return paths
273+
if not rel.parts: # info *is* root.
274+
return paths
275+
return (_convert_legacy_file(p.parts, rel.parts) for p in paths)
276+
277+
def _iter_files_from_record(self) -> Optional[Iterator[pathlib.Path]]:
278+
try:
279+
text = self.read_text("RECORD")
280+
except FileNotFoundError:
281+
return None
282+
return (pathlib.Path(row[0]) for row in csv.reader(text.splitlines()))
283+
284+
def iter_files(self) -> Optional[Iterator[pathlib.Path]]:
285+
"""Files in the distribution's record.
286+
287+
For modern .dist-info distributions, this is the files listed in the
288+
``RECORD`` file. All entries are paths relative to this distribution's
289+
``location``.
290+
291+
Note that this can be None for unmanagable distributions, e.g. an
292+
installation performed by distutils or a foreign package manager.
293+
"""
294+
return self._iter_files_from_record() or self._iter_files_from_legacy()
295+
209296

210297
class BaseEnvironment:
211298
"""An environment containing distributions to introspect."""

src/pip/_internal/metadata/pkg_resources.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import email.message
22
import logging
3+
import os
4+
import pathlib
35
from typing import (
46
TYPE_CHECKING,
57
Collection,
@@ -49,6 +51,25 @@ def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
4951
dist = pkg_resources_distribution_for_wheel(zf, name, wheel.location)
5052
return cls(dist)
5153

54+
@classmethod
55+
def from_info_directory(cls, path: str) -> "Distribution":
56+
dist_dir = path.rstrip(os.sep)
57+
58+
# Build a PathMetadata object, from path to metadata. :wink:
59+
base_dir, dist_dir_name = os.path.split(dist_dir)
60+
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
61+
62+
# Determine the correct Distribution object type.
63+
if dist_dir.endswith(".egg-info"):
64+
dist_cls = pkg_resources.Distribution
65+
dist_name = os.path.splitext(dist_dir_name)[0]
66+
else:
67+
assert dist_dir.endswith(".dist-info")
68+
dist_cls = pkg_resources.DistInfoDistribution
69+
dist_name = os.path.splitext(dist_dir_name)[0].split("-", 1)[0]
70+
71+
return cls(dist_cls(base_dir, project_name=dist_name, metadata=metadata))
72+
5273
@property
5374
def location(self) -> Optional[str]:
5475
return self._dist.location
@@ -63,12 +84,19 @@ def canonical_name(self) -> "NormalizedName":
6384

6485
@property
6586
def version(self) -> DistributionVersion:
87+
# pkg_resouces may contain a different copy of packaging.version from
88+
# pip in if the downstream distributor does a poor job debundling pip.
89+
# We avoid parsed_version and use our vendored packaging instead.
6690
return parse_version(self._dist.version)
6791

6892
@property
6993
def installer(self) -> str:
7094
return get_installer(self._dist)
7195

96+
@property
97+
def egg_link(self) -> Optional[str]:
98+
return misc.egg_link_path(self._dist)
99+
72100
@property
73101
def editable(self) -> bool:
74102
return misc.dist_is_editable(self._dist)
@@ -90,6 +118,15 @@ def read_text(self, name: str) -> str:
90118
raise FileNotFoundError(name)
91119
return self._dist.get_metadata(name)
92120

121+
def iterdir(self, name: str) -> Iterable[pathlib.PurePosixPath]:
122+
if not self._dist.has_metadata(name):
123+
raise FileNotFoundError(name)
124+
if not self._dist.metadata_isdir(name):
125+
raise NotADirectoryError(name)
126+
return (
127+
pathlib.PurePosixPath(name, n) for n in self._dist.metadata_listdir(name)
128+
)
129+
93130
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
94131
for group, entries in self._dist.get_entry_map().items():
95132
for name, entry_point in entries.items():

src/pip/_internal/network/lazy_wheel.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
from pip._vendor.packaging.utils import canonicalize_name
1212
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
1313

14-
from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
14+
from pip._internal.metadata import (
15+
BaseDistribution,
16+
MemoryWheel,
17+
get_distribution_for_wheel,
18+
)
1519
from pip._internal.network.session import PipSession
1620
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
1721

@@ -34,7 +38,7 @@ def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistrib
3438
wheel = MemoryWheel(zf.name, zf) # type: ignore
3539
# After context manager exit, wheel.name
3640
# is an invalid file by intention.
37-
return get_wheel_distribution(wheel, canonicalize_name(name))
41+
return get_distribution_for_wheel(wheel, canonicalize_name(name))
3842

3943

4044
class LazyZipOverHTTP:

0 commit comments

Comments
 (0)