Skip to content

Commit

Permalink
Remove pkg_resources usages from 'pip freeze'
Browse files Browse the repository at this point in the history
  • Loading branch information
uranusjr committed Jul 13, 2021
1 parent 8aa0dcb commit b03156f
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 69 deletions.
10 changes: 7 additions & 3 deletions news/10157.process.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
``pip list`` and ``pip show`` no longer normalize underscore (``_``) in
distribution names to dash (``-``). This is done as a part of the refactoring to
prepare for the migration to ``importlib.metadata``.
``pip freeze``, ``pip list``, and ``pip show`` no longer normalize underscore
(``_``) in distribution names to dash (``-``). This is a side effect of the
migration to ``importlib.metadata``, since the underscore-dash normalization
behavior is non-standard and specific to setuptools. This should not affect
other parts of pip (for example, when feeding the ``pip freeze`` result back
into ``pip install``) since pip internally performs standard PEP 503
normalization independently to setuptools.
129 changes: 67 additions & 62 deletions src/pip/_internal/operations/freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@
Iterable,
Iterator,
List,
NamedTuple,
Optional,
Set,
Tuple,
Union,
)

from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.pkg_resources import Distribution, Requirement, RequirementParseError
from pip._vendor.packaging.version import Version

from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.metadata import BaseDistribution, get_environment
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
)
from pip._internal.req.req_file import COMMENT_RE
from pip._internal.utils.direct_url_helpers import (
direct_url_as_pep440_direct_reference,
dist_get_direct_url,
)
from pip._internal.utils.misc import dist_is_editable, get_installed_distributions

logger = logging.getLogger(__name__)

RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]]

class _EditableInfo(NamedTuple):
requirement: Optional[str]
editable: bool
comments: List[str]


def freeze(
Expand All @@ -48,26 +52,16 @@ def freeze(

for link in find_links:
yield f'-f {link}'

installations = {} # type: Dict[str, FrozenRequirement]

for dist in get_installed_distributions(
local_only=local_only,
skip=(),
user_only=user_only,
paths=paths
):
try:
req = FrozenRequirement.from_dist(dist)
except RequirementParseError as exc:
# We include dist rather than dist.project_name because the
# dist string includes more information, like the version and
# location. We also include the exception message to aid
# troubleshooting.
logger.warning(
'Could not generate requirement for distribution %r: %s',
dist, exc
)
continue
dists = get_environment(paths).iter_installed_distributions(
local_only=local_only,
skip=(),
user_only=user_only,
)
for dist in dists:
req = FrozenRequirement.from_dist(dist)
if exclude_editable and req.editable:
continue
installations[req.canonical_name] = req
Expand Down Expand Up @@ -165,49 +159,60 @@ def freeze(
yield str(installation).rstrip()


def get_requirement_info(dist):
# type: (Distribution) -> RequirementInfo
def _format_as_name_version(dist: BaseDistribution) -> str:
if isinstance(dist.version, Version):
return f"{dist.raw_name}=={dist.version}"
return f"{dist.raw_name}==={dist.version}"


def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
"""
Compute and return values (req, editable, comments) for use in
FrozenRequirement.from_dist().
"""
if not dist_is_editable(dist):
return (None, False, [])
if not dist.editable:
return _EditableInfo(requirement=None, editable=False, comments=[])

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

from pip._internal.vcs import RemoteNotFoundError, RemoteNotValidError, vcs

vcs_backend = vcs.get_backend_for_dir(location)

if vcs_backend is None:
req = dist.as_requirement()
display = _format_as_name_version(dist)
logger.debug(
'No VCS found for editable requirement "%s" in: %r', req,
'No VCS found for editable requirement "%s" in: %r', display,
location,
)
comments = [
f'# Editable install with no version control ({req})'
]
return (location, True, comments)
return _EditableInfo(
requirement=location,
editable=True,
comments=[f'# Editable install with no version control ({display})'],
)

vcs_name = type(vcs_backend).__name__

try:
req = vcs_backend.get_src_requirement(location, dist.project_name)
req = vcs_backend.get_src_requirement(location, dist.raw_name)
except RemoteNotFoundError:
req = dist.as_requirement()
comments = [
'# Editable {} install with no remote ({})'.format(
type(vcs_backend).__name__, req,
)
]
return (location, True, comments)
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
editable=True,
comments=[f'# Editable {vcs_name} install with no remote ({display})'],
)
except RemoteNotValidError as ex:
req = dist.as_requirement()
comments = [
f"# Editable {type(vcs_backend).__name__} install ({req}) with "
f"either a deleted local remote or invalid URI:",
f"# '{ex.url}'",
]
return (location, True, comments)
display = _format_as_name_version(dist)
return _EditableInfo(
requirement=location,
editable=True,
comments=[
f"# Editable {vcs_name} install ({display}) with either a deleted "
f"local remote or invalid URI:",
f"# '{ex.url}'",
],
)

except BadCommand:
logger.warning(
Expand All @@ -216,22 +221,23 @@ def get_requirement_info(dist):
location,
vcs_backend.name,
)
return (None, True, [])
return _EditableInfo(requirement=None, editable=True, comments=[])

except InstallationError as exc:
logger.warning(
"Error when trying to get requirement for VCS system %s, "
"falling back to uneditable format", exc
)
else:
return (req, True, [])
return _EditableInfo(requirement=req, editable=True, comments=[])

logger.warning(
'Could not determine repository location of %s', location
)
comments = ['## !! Could not determine repository location']
logger.warning('Could not determine repository location of %s', location)

return (None, False, comments)
return _EditableInfo(
requirement=None,
editable=False,
comments=['## !! Could not determine repository location'],
)


class FrozenRequirement:
Expand All @@ -244,25 +250,24 @@ def __init__(self, name, req, editable, comments=()):
self.comments = comments

@classmethod
def from_dist(cls, dist):
# type: (Distribution) -> FrozenRequirement
def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
# TODO `get_requirement_info` is taking care of editable requirements.
# TODO This should be refactored when we will add detection of
# editable that provide .dist-info metadata.
req, editable, comments = get_requirement_info(dist)
req, editable, comments = _get_editable_info(dist)
if req is None and not editable:
# if PEP 610 metadata is present, attempt to use it
direct_url = dist_get_direct_url(dist)
direct_url = dist.direct_url
if direct_url:
req = direct_url_as_pep440_direct_reference(
direct_url, dist.project_name
direct_url, dist.raw_name
)
comments = []
if req is None:
# name==version requirement
req = dist.as_requirement()
req = _format_as_name_version(dist)

return cls(dist.project_name, req, editable, comments=comments)
return cls(dist.raw_name, req, editable, comments=comments)

def __str__(self):
# type: () -> str
Expand Down
7 changes: 3 additions & 4 deletions tests/functional/test_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import pytest
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.pkg_resources import safe_name

from tests.lib import (
_create_test_package,
Expand Down Expand Up @@ -88,9 +87,9 @@ def test_exclude_and_normalization(script, tmpdir):
name="Normalizable_Name", version="1.0").save_to_dir(tmpdir)
script.pip("install", "--no-index", req_path)
result = script.pip("freeze")
assert "Normalizable-Name" in result.stdout
assert "Normalizable_Name" in result.stdout
result = script.pip("freeze", "--exclude", "normalizablE-namE")
assert "Normalizable-Name" not in result.stdout
assert "Normalizable_Name" not in result.stdout


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

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

0 comments on commit b03156f

Please sign in to comment.