Skip to content

Commit 6f1c5dc

Browse files
committed
Remove pkg_resources usages from 'pip freeze'
1 parent 19389df commit 6f1c5dc

File tree

3 files changed

+85
-72
lines changed

3 files changed

+85
-72
lines changed

news/10157.process.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
``pip list`` and ``pip show`` no longer normalize underscore (``_``) in
2-
distribution names to dash (``-``). This is done as a part of the refactoring to
3-
prepare for the migration to ``importlib.metadata``.
1+
``pip freeze``, ``pip list``, and ``pip show`` no longer normalize underscore
2+
(``_``) in distribution names to dash (``-``). This is a side effect of the
3+
migration to ``importlib.metadata``, since the underscore-dash normalization
4+
behavior is non-standard and specific to setuptools. This should not affect
5+
other parts of pip (for example, when feeding the ``pip freeze`` result back
6+
into ``pip install``) since pip internally performs standard PEP 503
7+
normalization independently to setuptools.

src/pip/_internal/operations/freeze.py

Lines changed: 75 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,32 @@
77
Iterable,
88
Iterator,
99
List,
10+
NamedTuple,
1011
Optional,
1112
Set,
12-
Tuple,
1313
Union,
1414
)
1515

16+
from pip._vendor.packaging.requirements import Requirement
1617
from pip._vendor.packaging.utils import canonicalize_name
17-
from pip._vendor.pkg_resources import Distribution, Requirement, RequirementParseError
18+
from pip._vendor.packaging.version import Version
1819

1920
from pip._internal.exceptions import BadCommand, InstallationError
21+
from pip._internal.metadata import BaseDistribution, get_environment
2022
from pip._internal.req.constructors import (
2123
install_req_from_editable,
2224
install_req_from_line,
2325
)
2426
from pip._internal.req.req_file import COMMENT_RE
25-
from pip._internal.utils.direct_url_helpers import (
26-
direct_url_as_pep440_direct_reference,
27-
dist_get_direct_url,
28-
)
29-
from pip._internal.utils.misc import dist_is_editable, get_installed_distributions
27+
from pip._internal.utils.direct_url_helpers import direct_url_as_pep440_direct_reference
3028

3129
logger = logging.getLogger(__name__)
3230

33-
RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]]
31+
32+
class _EditableInfo(NamedTuple):
33+
requirement: Optional[str]
34+
editable: bool
35+
comments: List[str]
3436

3537

3638
def freeze(
@@ -45,24 +47,13 @@ def freeze(
4547
# type: (...) -> Iterator[str]
4648
installations = {} # type: Dict[str, FrozenRequirement]
4749

48-
for dist in get_installed_distributions(
49-
local_only=local_only,
50-
skip=(),
51-
user_only=user_only,
52-
paths=paths
53-
):
54-
try:
55-
req = FrozenRequirement.from_dist(dist)
56-
except RequirementParseError as exc:
57-
# We include dist rather than dist.project_name because the
58-
# dist string includes more information, like the version and
59-
# location. We also include the exception message to aid
60-
# troubleshooting.
61-
logger.warning(
62-
'Could not generate requirement for distribution %r: %s',
63-
dist, exc
64-
)
65-
continue
50+
dists = get_environment(paths).iter_installed_distributions(
51+
local_only=local_only,
52+
skip=(),
53+
user_only=user_only,
54+
)
55+
for dist in dists:
56+
req = FrozenRequirement.from_dist(dist)
6657
if exclude_editable and req.editable:
6758
continue
6859
installations[req.canonical_name] = req
@@ -160,49 +151,68 @@ def freeze(
160151
yield str(installation).rstrip()
161152

162153

163-
def get_requirement_info(dist):
164-
# type: (Distribution) -> RequirementInfo
154+
def _format_as_name_version(dist: BaseDistribution) -> str:
155+
if isinstance(dist.version, Version):
156+
return f"{dist.raw_name}=={dist.version}"
157+
return f"{dist.raw_name}==={dist.version}"
158+
159+
160+
def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
165161
"""
166162
Compute and return values (req, editable, comments) for use in
167163
FrozenRequirement.from_dist().
168164
"""
169-
if not dist_is_editable(dist):
170-
return (None, False, [])
165+
if not dist.editable:
166+
return _EditableInfo(requirement=None, editable=False, comments=[])
167+
if dist.location is None:
168+
display = _format_as_name_version(dist)
169+
logger.warning("Editable requirement not found on disk: %s", display)
170+
return _EditableInfo(
171+
requirement=None,
172+
editable=True,
173+
comments=[f"# Editable install not found ({display})"],
174+
)
171175

172176
location = os.path.normcase(os.path.abspath(dist.location))
173177

174178
from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs
179+
175180
vcs_backend = vcs.get_backend_for_dir(location)
176181

177182
if vcs_backend is None:
178-
req = dist.as_requirement()
183+
display = _format_as_name_version(dist)
179184
logger.debug(
180-
'No VCS found for editable requirement "%s" in: %r', req,
185+
'No VCS found for editable requirement "%s" in: %r', display,
181186
location,
182187
)
183-
comments = [
184-
f'# Editable install with no version control ({req})'
185-
]
186-
return (location, True, comments)
188+
return _EditableInfo(
189+
requirement=location,
190+
editable=True,
191+
comments=[f'# Editable install with no version control ({display})'],
192+
)
193+
194+
vcs_name = type(vcs_backend).__name__
187195

188196
try:
189-
req = vcs_backend.get_src_requirement(location, dist.project_name)
197+
req = vcs_backend.get_src_requirement(location, dist.raw_name)
190198
except RemoteNotFoundError:
191-
req = dist.as_requirement()
192-
comments = [
193-
'# Editable {} install with no remote ({})'.format(
194-
type(vcs_backend).__name__, req,
195-
)
196-
]
197-
return (location, True, comments)
199+
display = _format_as_name_version(dist)
200+
return _EditableInfo(
201+
requirement=location,
202+
editable=True,
203+
comments=[f'# Editable {vcs_name} install with no remote ({display})'],
204+
)
198205
except RemoteNotValidError as ex:
199-
req = dist.as_requirement()
200-
comments = [
201-
f"# Editable {type(vcs_backend).__name__} install ({req}) with "
202-
f"either a deleted local remote or invalid URI:",
203-
f"# '{ex.url}'",
204-
]
205-
return (location, True, comments)
206+
display = _format_as_name_version(dist)
207+
return _EditableInfo(
208+
requirement=location,
209+
editable=True,
210+
comments=[
211+
f"# Editable {vcs_name} install ({display}) with either a deleted "
212+
f"local remote or invalid URI:",
213+
f"# '{ex.url}'",
214+
],
215+
)
206216

207217
except BadCommand:
208218
logger.warning(
@@ -211,22 +221,23 @@ def get_requirement_info(dist):
211221
location,
212222
vcs_backend.name,
213223
)
214-
return (None, True, [])
224+
return _EditableInfo(requirement=None, editable=True, comments=[])
215225

216226
except InstallationError as exc:
217227
logger.warning(
218228
"Error when trying to get requirement for VCS system %s, "
219229
"falling back to uneditable format", exc
220230
)
221231
else:
222-
return (req, True, [])
232+
return _EditableInfo(requirement=req, editable=True, comments=[])
223233

224-
logger.warning(
225-
'Could not determine repository location of %s', location
226-
)
227-
comments = ['## !! Could not determine repository location']
234+
logger.warning('Could not determine repository location of %s', location)
228235

229-
return (None, False, comments)
236+
return _EditableInfo(
237+
requirement=None,
238+
editable=False,
239+
comments=['## !! Could not determine repository location'],
240+
)
230241

231242

232243
class FrozenRequirement:
@@ -239,25 +250,24 @@ def __init__(self, name, req, editable, comments=()):
239250
self.comments = comments
240251

241252
@classmethod
242-
def from_dist(cls, dist):
243-
# type: (Distribution) -> FrozenRequirement
253+
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
244254
# TODO `get_requirement_info` is taking care of editable requirements.
245255
# TODO This should be refactored when we will add detection of
246256
# editable that provide .dist-info metadata.
247-
req, editable, comments = get_requirement_info(dist)
257+
req, editable, comments = _get_editable_info(dist)
248258
if req is None and not editable:
249259
# if PEP 610 metadata is present, attempt to use it
250-
direct_url = dist_get_direct_url(dist)
260+
direct_url = dist.direct_url
251261
if direct_url:
252262
req = direct_url_as_pep440_direct_reference(
253-
direct_url, dist.project_name
263+
direct_url, dist.raw_name
254264
)
255265
comments = []
256266
if req is None:
257267
# name==version requirement
258-
req = dist.as_requirement()
268+
req = _format_as_name_version(dist)
259269

260-
return cls(dist.project_name, req, editable, comments=comments)
270+
return cls(dist.raw_name, req, editable, comments=comments)
261271

262272
def __str__(self):
263273
# type: () -> str

tests/functional/test_freeze.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import pytest
88
from pip._vendor.packaging.utils import canonicalize_name
9-
from pip._vendor.pkg_resources import safe_name
109

1110
from tests.lib import (
1211
_create_test_package,
@@ -88,9 +87,9 @@ def test_exclude_and_normalization(script, tmpdir):
8887
name="Normalizable_Name", version="1.0").save_to_dir(tmpdir)
8988
script.pip("install", "--no-index", req_path)
9089
result = script.pip("freeze")
91-
assert "Normalizable-Name" in result.stdout
90+
assert "Normalizable_Name" in result.stdout
9291
result = script.pip("freeze", "--exclude", "normalizablE-namE")
93-
assert "Normalizable-Name" not in result.stdout
92+
assert "Normalizable_Name" not in result.stdout
9493

9594

9695
def test_freeze_multiple_exclude_with_all(script, with_wheel):
@@ -136,7 +135,7 @@ def fake_install(pkgname, dest):
136135
# Check all valid names are in the output.
137136
output_lines = {line.strip() for line in result.stdout.splitlines()}
138137
for name in valid_pkgnames:
139-
assert f"{safe_name(name)}==1.0" in output_lines
138+
assert f"{name}==1.0" in output_lines
140139

141140
# Check all invalid names are excluded from the output.
142141
canonical_invalid_names = {canonicalize_name(n) for n in invalid_pkgnames}

0 commit comments

Comments
 (0)