Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove direct pkg_resources usages in req_install and req_uninstall #10390

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 3 additions & 60 deletions src/pip/_internal/commands/show.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import csv
import logging
import pathlib
from optparse import Values
from typing import Iterator, List, NamedTuple, Optional, Tuple
from typing import Iterator, List, NamedTuple, Optional

from pip._vendor.packaging.utils import canonicalize_name

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


def _convert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str:
"""Convert a legacy installed-files.txt path into modern RECORD path.

The legacy format stores paths relative to the info directory, while the
modern format stores paths relative to the package root, e.g. the
site-packages directory.

:param entry: Path parts of the installed-files.txt entry.
:param info: Path parts of the egg-info directory relative to package root.
:returns: The converted entry.

For best compatibility with symlinks, this does not use ``abspath()`` or
``Path.resolve()``, but tries to work with path parts:

1. While ``entry`` starts with ``..``, remove the equal amounts of parts
from ``info``; if ``info`` is empty, start appending ``..`` instead.
2. Join the two directly.
"""
while entry and entry[0] == "..":
if not info or info[-1] == "..":
info += ("..",)
else:
info = info[:-1]
entry = entry[1:]
return str(pathlib.Path(*info, *entry))


def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
"""
Gather details from installed distributions. Print distribution name,
Expand All @@ -121,34 +92,6 @@ def _get_requiring_packages(current_dist: BaseDistribution) -> List[str]:
in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
]

def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]:
try:
text = dist.read_text("RECORD")
except FileNotFoundError:
return None
# This extra Path-str cast normalizes entries.
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))

def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
try:
text = dist.read_text("installed-files.txt")
except FileNotFoundError:
return None
paths = (p for p in text.splitlines(keepends=False) if p)
root = dist.location
info = dist.info_directory
if root is None or info is None:
return paths
try:
info_rel = pathlib.Path(info).relative_to(root)
except ValueError: # info is not relative to root.
return paths
if not info_rel.parts: # info *is* root.
return paths
return (
_convert_legacy_entry(pathlib.Path(p).parts, info_rel.parts) for p in paths
)

for query_name in query_names:
try:
dist = installed[query_name]
Expand All @@ -161,11 +104,11 @@ def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
except FileNotFoundError:
entry_points = []

files_iter = _files_from_record(dist) or _files_from_legacy(dist)
files_iter = dist.iter_files()
if files_iter is None:
files: Optional[List[str]] = None
else:
files = sorted(files_iter)
files = sorted(str(f) for f in files_iter)

metadata = dist.metadata

Expand Down
4 changes: 1 addition & 3 deletions src/pip/_internal/distributions/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ class SourceDistribution(AbstractDistribution):
"""

def get_metadata_distribution(self) -> BaseDistribution:
from pip._internal.metadata.pkg_resources import Distribution as _Dist

return _Dist(self.req.get_dist())
return self.req.get_dist()

def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
Expand Down
4 changes: 2 additions & 2 deletions src/pip/_internal/distributions/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pip._internal.metadata import (
BaseDistribution,
FilesystemWheel,
get_wheel_distribution,
get_distribution_for_wheel,
)


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

def prepare_distribution_metadata(
self, finder: PackageFinder, build_isolation: bool
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/index/package_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ def find_requirement(

installed_version: Optional[_BaseVersion] = None
if req.satisfied_by is not None:
installed_version = parse_version(req.satisfied_by.version)
installed_version = req.satisfied_by.version

def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
# This repeated parse_version and str() conversion is needed to
Expand Down
13 changes: 12 additions & 1 deletion src/pip/_internal/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
return Environment.from_paths(paths)


def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
def get_distribution_for_wheel(wheel: Wheel, canonical_name: str) -> BaseDistribution:
"""Get the representation of the specified wheel's distribution metadata.

This returns a Distribution instance from the chosen backend based on
Expand All @@ -49,3 +49,14 @@ def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistributio
from .pkg_resources import Distribution

return Distribution.from_wheel(wheel, canonical_name)


def get_distribution_for_info_directory(directory_path: str) -> BaseDistribution:
"""Get the specified info directory's distribution representation.

The directory should be an on-disk ``NAME-VERSION.dist-info`` or
``NAME.egg-info`` directory.
"""
from .pkg_resources import Distribution

return Distribution.from_info_directory(directory_path)
91 changes: 89 additions & 2 deletions src/pip/_internal/metadata/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import csv
import email.message
import json
import logging
import pathlib
import re
import zipfile
from typing import (
Expand All @@ -12,6 +14,7 @@
Iterator,
List,
Optional,
Tuple,
Union,
)

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


def _convert_legacy_file(entry: Tuple[str, ...], info: Tuple[str, ...]) -> pathlib.Path:
"""Convert a legacy installed-files.txt path into modern RECORD path.

The legacy format stores paths relative to the info directory, while the
modern format stores paths relative to the package root, e.g. the
site-packages directory.

:param entry: Path parts of the installed-files.txt entry.
:param info: Path parts of the egg-info directory relative to package root.
:returns: The converted entry.

For best compatibility with symlinks, this does not use ``abspath()`` or
``Path.resolve()``, but tries to work with path parts:

1. While ``entry`` starts with ``..``, remove the equal amounts of parts
from ``info``; if ``info`` is empty, start appending ``..`` instead.
2. Join the two directly.
"""
while entry and entry[0] == "..":
if not info or info[-1] == "..":
info += ("..",)
else:
info = info[:-1]
entry = entry[1:]
return pathlib.Path(*info, *entry)


class BaseEntryPoint(Protocol):
@property
def name(self) -> str:
Expand Down Expand Up @@ -127,6 +157,17 @@ def direct_url(self) -> Optional[DirectUrl]:
def installer(self) -> str:
raise NotImplementedError()

@property
def egg_link(self) -> Optional[str]:
"""Location of the ``.egg-link`` for this distribution.

If there's not a matching file, None is returned. Note that finding this
file does not necessarily mean the currently-installed distribution is
editable since the ``.egg-link`` can still be shadowed by a
non-editable installation located in front of it in ``sys.path``.
"""
raise NotImplementedError()

@property
def editable(self) -> bool:
raise NotImplementedError()
Expand All @@ -146,11 +187,20 @@ def in_site_packages(self) -> bool:
def read_text(self, name: str) -> str:
"""Read a file in the .dist-info (or .egg-info) directory.

Should raise ``FileNotFoundError`` if ``name`` does not exist in the
metadata directory.
:raises FileNotFoundError: ``name`` does not exist in the info directory.
"""
raise NotImplementedError()

def iterdir(self, name: str) -> Iterable[pathlib.PurePosixPath]:
"""Iterate through a directory in the info directory.

Each item is a path relative to the info directory.

:raises FileNotFoundError: ``name`` does not exist in the info directory.
:raises NotADirectoryError: ``name`` exists in the info directory, but
is not a directory.
"""

def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
raise NotImplementedError()

Expand Down Expand Up @@ -206,6 +256,43 @@ def iter_provided_extras(self) -> Iterable[str]:
"""
raise NotImplementedError()

def _iter_files_from_legacy(self) -> Optional[Iterator[pathlib.Path]]:
try:
text = self.read_text("installed-files.txt")
except FileNotFoundError:
return None
paths = (pathlib.Path(p) for p in text.splitlines(keepends=False) if p)
root = self.location
info = self.info_directory
if root is None or info is None:
return paths
try:
rel = pathlib.Path(info).relative_to(root)
except ValueError: # info is not relative to root.
return paths
if not rel.parts: # info *is* root.
return paths
return (_convert_legacy_file(p.parts, rel.parts) for p in paths)

def _iter_files_from_record(self) -> Optional[Iterator[pathlib.Path]]:
try:
text = self.read_text("RECORD")
except FileNotFoundError:
return None
return (pathlib.Path(row[0]) for row in csv.reader(text.splitlines()))

def iter_files(self) -> Optional[Iterator[pathlib.Path]]:
"""Files in the distribution's record.

For modern .dist-info distributions, this is the files listed in the
``RECORD`` file. All entries are paths relative to this distribution's
``location``.

Note that this can be None for unmanagable distributions, e.g. an
installation performed by distutils or a foreign package manager.
"""
return self._iter_files_from_record() or self._iter_files_from_legacy()


class BaseEnvironment:
"""An environment containing distributions to introspect."""
Expand Down
37 changes: 37 additions & 0 deletions src/pip/_internal/metadata/pkg_resources.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import email.message
import logging
import os
import pathlib
from typing import (
TYPE_CHECKING,
Collection,
Expand Down Expand Up @@ -49,6 +51,25 @@ def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
dist = pkg_resources_distribution_for_wheel(zf, name, wheel.location)
return cls(dist)

@classmethod
def from_info_directory(cls, path: str) -> "Distribution":
dist_dir = path.rstrip(os.sep)

# Build a PathMetadata object, from path to metadata. :wink:
base_dir, dist_dir_name = os.path.split(dist_dir)
metadata = pkg_resources.PathMetadata(base_dir, dist_dir)

# Determine the correct Distribution object type.
if dist_dir.endswith(".egg-info"):
dist_cls = pkg_resources.Distribution
dist_name = os.path.splitext(dist_dir_name)[0]
else:
assert dist_dir.endswith(".dist-info")
dist_cls = pkg_resources.DistInfoDistribution
dist_name = os.path.splitext(dist_dir_name)[0].split("-", 1)[0]

return cls(dist_cls(base_dir, project_name=dist_name, metadata=metadata))

@property
def location(self) -> Optional[str]:
return self._dist.location
Expand All @@ -63,12 +84,19 @@ def canonical_name(self) -> "NormalizedName":

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

@property
def installer(self) -> str:
return get_installer(self._dist)

@property
def egg_link(self) -> Optional[str]:
return misc.egg_link_path(self._dist)

@property
def editable(self) -> bool:
return misc.dist_is_editable(self._dist)
Expand All @@ -90,6 +118,15 @@ def read_text(self, name: str) -> str:
raise FileNotFoundError(name)
return self._dist.get_metadata(name)

def iterdir(self, name: str) -> Iterable[pathlib.PurePosixPath]:
if not self._dist.has_metadata(name):
raise FileNotFoundError(name)
if not self._dist.metadata_isdir(name):
raise NotADirectoryError(name)
return (
pathlib.PurePosixPath(name, n) for n in self._dist.metadata_listdir(name)
)

def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
for group, entries in self._dist.get_entry_map().items():
for name, entry_point in entries.items():
Expand Down
8 changes: 6 additions & 2 deletions src/pip/_internal/network/lazy_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response

from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
from pip._internal.metadata import (
BaseDistribution,
MemoryWheel,
get_distribution_for_wheel,
)
from pip._internal.network.session import PipSession
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks

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


class LazyZipOverHTTP:
Expand Down
Loading