first commit
This commit is contained in:
127
venv/Lib/site-packages/pip/_internal/metadata/__init__.py
Normal file
127
venv/Lib/site-packages/pip/_internal/metadata/__init__.py
Normal file
@@ -0,0 +1,127 @@
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, List, Optional, Type, cast
|
||||
|
||||
from pip._internal.utils.misc import strtobool
|
||||
|
||||
from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Protocol
|
||||
else:
|
||||
Protocol = object
|
||||
|
||||
__all__ = [
|
||||
"BaseDistribution",
|
||||
"BaseEnvironment",
|
||||
"FilesystemWheel",
|
||||
"MemoryWheel",
|
||||
"Wheel",
|
||||
"get_default_environment",
|
||||
"get_environment",
|
||||
"get_wheel_distribution",
|
||||
"select_backend",
|
||||
]
|
||||
|
||||
|
||||
def _should_use_importlib_metadata() -> bool:
|
||||
"""Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
|
||||
|
||||
By default, pip uses ``importlib.metadata`` on Python 3.11+, and
|
||||
``pkg_resourcess`` otherwise. This can be overridden by a couple of ways:
|
||||
|
||||
* If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
|
||||
dictates whether ``importlib.metadata`` is used, regardless of Python
|
||||
version.
|
||||
* On Python 3.11+, Python distributors can patch ``importlib.metadata``
|
||||
to add a global constant ``_PIP_USE_IMPORTLIB_METADATA = False``. This
|
||||
makes pip use ``pkg_resources`` (unless the user set the aforementioned
|
||||
environment variable to *True*).
|
||||
"""
|
||||
with contextlib.suppress(KeyError, ValueError):
|
||||
return bool(strtobool(os.environ["_PIP_USE_IMPORTLIB_METADATA"]))
|
||||
if sys.version_info < (3, 11):
|
||||
return False
|
||||
import importlib.metadata
|
||||
|
||||
return bool(getattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA", True))
|
||||
|
||||
|
||||
class Backend(Protocol):
|
||||
Distribution: Type[BaseDistribution]
|
||||
Environment: Type[BaseEnvironment]
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def select_backend() -> Backend:
|
||||
if _should_use_importlib_metadata():
|
||||
from . import importlib
|
||||
|
||||
return cast(Backend, importlib)
|
||||
from . import pkg_resources
|
||||
|
||||
return cast(Backend, pkg_resources)
|
||||
|
||||
|
||||
def get_default_environment() -> BaseEnvironment:
|
||||
"""Get the default representation for the current environment.
|
||||
|
||||
This returns an Environment instance from the chosen backend. The default
|
||||
Environment instance should be built from ``sys.path`` and may use caching
|
||||
to share instance state accorss calls.
|
||||
"""
|
||||
return select_backend().Environment.default()
|
||||
|
||||
|
||||
def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
|
||||
"""Get a representation of the environment specified by ``paths``.
|
||||
|
||||
This returns an Environment instance from the chosen backend based on the
|
||||
given import paths. The backend must build a fresh instance representing
|
||||
the state of installed distributions when this function is called.
|
||||
"""
|
||||
return select_backend().Environment.from_paths(paths)
|
||||
|
||||
|
||||
def get_directory_distribution(directory: str) -> BaseDistribution:
|
||||
"""Get the distribution metadata representation in the specified directory.
|
||||
|
||||
This returns a Distribution instance from the chosen backend based on
|
||||
the given on-disk ``.dist-info`` directory.
|
||||
"""
|
||||
return select_backend().Distribution.from_directory(directory)
|
||||
|
||||
|
||||
def get_wheel_distribution(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
|
||||
the given wheel's ``.dist-info`` directory.
|
||||
|
||||
:param canonical_name: Normalized project name of the given wheel.
|
||||
"""
|
||||
return select_backend().Distribution.from_wheel(wheel, canonical_name)
|
||||
|
||||
|
||||
def get_metadata_distribution(
|
||||
metadata_contents: bytes,
|
||||
filename: str,
|
||||
canonical_name: str,
|
||||
) -> BaseDistribution:
|
||||
"""Get the dist representation of the specified METADATA file contents.
|
||||
|
||||
This returns a Distribution instance from the chosen backend sourced from the data
|
||||
in `metadata_contents`.
|
||||
|
||||
:param metadata_contents: Contents of a METADATA file within a dist, or one served
|
||||
via PEP 658.
|
||||
:param filename: Filename for the dist this metadata represents.
|
||||
:param canonical_name: Normalized project name of the given dist.
|
||||
"""
|
||||
return select_backend().Distribution.from_metadata_file_contents(
|
||||
metadata_contents,
|
||||
filename,
|
||||
canonical_name,
|
||||
)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
84
venv/Lib/site-packages/pip/_internal/metadata/_json.py
Normal file
84
venv/Lib/site-packages/pip/_internal/metadata/_json.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# Extracted from https://github.com/pfmoore/pkg_metadata
|
||||
|
||||
from email.header import Header, decode_header, make_header
|
||||
from email.message import Message
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
METADATA_FIELDS = [
|
||||
# Name, Multiple-Use
|
||||
("Metadata-Version", False),
|
||||
("Name", False),
|
||||
("Version", False),
|
||||
("Dynamic", True),
|
||||
("Platform", True),
|
||||
("Supported-Platform", True),
|
||||
("Summary", False),
|
||||
("Description", False),
|
||||
("Description-Content-Type", False),
|
||||
("Keywords", False),
|
||||
("Home-page", False),
|
||||
("Download-URL", False),
|
||||
("Author", False),
|
||||
("Author-email", False),
|
||||
("Maintainer", False),
|
||||
("Maintainer-email", False),
|
||||
("License", False),
|
||||
("Classifier", True),
|
||||
("Requires-Dist", True),
|
||||
("Requires-Python", False),
|
||||
("Requires-External", True),
|
||||
("Project-URL", True),
|
||||
("Provides-Extra", True),
|
||||
("Provides-Dist", True),
|
||||
("Obsoletes-Dist", True),
|
||||
]
|
||||
|
||||
|
||||
def json_name(field: str) -> str:
|
||||
return field.lower().replace("-", "_")
|
||||
|
||||
|
||||
def msg_to_json(msg: Message) -> Dict[str, Any]:
|
||||
"""Convert a Message object into a JSON-compatible dictionary."""
|
||||
|
||||
def sanitise_header(h: Union[Header, str]) -> str:
|
||||
if isinstance(h, Header):
|
||||
chunks = []
|
||||
for bytes, encoding in decode_header(h):
|
||||
if encoding == "unknown-8bit":
|
||||
try:
|
||||
# See if UTF-8 works
|
||||
bytes.decode("utf-8")
|
||||
encoding = "utf-8"
|
||||
except UnicodeDecodeError:
|
||||
# If not, latin1 at least won't fail
|
||||
encoding = "latin1"
|
||||
chunks.append((bytes, encoding))
|
||||
return str(make_header(chunks))
|
||||
return str(h)
|
||||
|
||||
result = {}
|
||||
for field, multi in METADATA_FIELDS:
|
||||
if field not in msg:
|
||||
continue
|
||||
key = json_name(field)
|
||||
if multi:
|
||||
value: Union[str, List[str]] = [
|
||||
sanitise_header(v) for v in msg.get_all(field)
|
||||
]
|
||||
else:
|
||||
value = sanitise_header(msg.get(field))
|
||||
if key == "keywords":
|
||||
# Accept both comma-separated and space-separated
|
||||
# forms, for better compatibility with old data.
|
||||
if "," in value:
|
||||
value = [v.strip() for v in value.split(",")]
|
||||
else:
|
||||
value = value.split()
|
||||
result[key] = value
|
||||
|
||||
payload = msg.get_payload()
|
||||
if payload:
|
||||
result["description"] = payload
|
||||
|
||||
return result
|
688
venv/Lib/site-packages/pip/_internal/metadata/base.py
Normal file
688
venv/Lib/site-packages/pip/_internal/metadata/base.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
||||
from ._dists import Distribution
|
||||
from ._envs import Environment
|
||||
|
||||
__all__ = ["Distribution", "Environment"]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
import importlib.metadata
|
||||
from typing import Any, Optional, Protocol, cast
|
||||
|
||||
|
||||
class BadMetadata(ValueError):
|
||||
def __init__(self, dist: importlib.metadata.Distribution, *, reason: str) -> None:
|
||||
self.dist = dist
|
||||
self.reason = reason
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Bad metadata in {self.dist} ({self.reason})"
|
||||
|
||||
|
||||
class BasePath(Protocol):
|
||||
"""A protocol that various path objects conform.
|
||||
|
||||
This exists because importlib.metadata uses both ``pathlib.Path`` and
|
||||
``zipfile.Path``, and we need a common base for type hints (Union does not
|
||||
work well since ``zipfile.Path`` is too new for our linter setup).
|
||||
|
||||
This does not mean to be exhaustive, but only contains things that present
|
||||
in both classes *that we need*.
|
||||
"""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def parent(self) -> "BasePath":
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def get_info_location(d: importlib.metadata.Distribution) -> Optional[BasePath]:
|
||||
"""Find the path to the distribution's metadata directory.
|
||||
|
||||
HACK: This relies on importlib.metadata's private ``_path`` attribute. Not
|
||||
all distributions exist on disk, so importlib.metadata is correct to not
|
||||
expose the attribute as public. But pip's code base is old and not as clean,
|
||||
so we do this to avoid having to rewrite too many things. Hopefully we can
|
||||
eliminate this some day.
|
||||
"""
|
||||
return getattr(d, "_path", None)
|
||||
|
||||
|
||||
def get_dist_name(dist: importlib.metadata.Distribution) -> str:
|
||||
"""Get the distribution's project name.
|
||||
|
||||
The ``name`` attribute is only available in Python 3.10 or later. We are
|
||||
targeting exactly that, but Mypy does not know this.
|
||||
"""
|
||||
name = cast(Any, dist).name
|
||||
if not isinstance(name, str):
|
||||
raise BadMetadata(dist, reason="invalid metadata entry 'name'")
|
||||
return name
|
@@ -0,0 +1,224 @@
|
||||
import email.message
|
||||
import importlib.metadata
|
||||
import os
|
||||
import pathlib
|
||||
import zipfile
|
||||
from typing import (
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
cast,
|
||||
)
|
||||
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
|
||||
from pip._internal.exceptions import InvalidWheel, UnsupportedWheel
|
||||
from pip._internal.metadata.base import (
|
||||
BaseDistribution,
|
||||
BaseEntryPoint,
|
||||
DistributionVersion,
|
||||
InfoPath,
|
||||
Wheel,
|
||||
)
|
||||
from pip._internal.utils.misc import normalize_path
|
||||
from pip._internal.utils.packaging import safe_extra
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
|
||||
|
||||
from ._compat import BasePath, get_dist_name
|
||||
|
||||
|
||||
class WheelDistribution(importlib.metadata.Distribution):
|
||||
"""An ``importlib.metadata.Distribution`` read from a wheel.
|
||||
|
||||
Although ``importlib.metadata.PathDistribution`` accepts ``zipfile.Path``,
|
||||
its implementation is too "lazy" for pip's needs (we can't keep the ZipFile
|
||||
handle open for the entire lifetime of the distribution object).
|
||||
|
||||
This implementation eagerly reads the entire metadata directory into the
|
||||
memory instead, and operates from that.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
files: Mapping[pathlib.PurePosixPath, bytes],
|
||||
info_location: pathlib.PurePosixPath,
|
||||
) -> None:
|
||||
self._files = files
|
||||
self.info_location = info_location
|
||||
|
||||
@classmethod
|
||||
def from_zipfile(
|
||||
cls,
|
||||
zf: zipfile.ZipFile,
|
||||
name: str,
|
||||
location: str,
|
||||
) -> "WheelDistribution":
|
||||
info_dir, _ = parse_wheel(zf, name)
|
||||
paths = (
|
||||
(name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
|
||||
for name in zf.namelist()
|
||||
if name.startswith(f"{info_dir}/")
|
||||
)
|
||||
files = {
|
||||
relpath: read_wheel_metadata_file(zf, fullpath)
|
||||
for fullpath, relpath in paths
|
||||
}
|
||||
info_location = pathlib.PurePosixPath(location, info_dir)
|
||||
return cls(files, info_location)
|
||||
|
||||
def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
|
||||
# Only allow iterating through the metadata directory.
|
||||
if pathlib.PurePosixPath(str(path)) in self._files:
|
||||
return iter(self._files)
|
||||
raise FileNotFoundError(path)
|
||||
|
||||
def read_text(self, filename: str) -> Optional[str]:
|
||||
try:
|
||||
data = self._files[pathlib.PurePosixPath(filename)]
|
||||
except KeyError:
|
||||
return None
|
||||
try:
|
||||
text = data.decode("utf-8")
|
||||
except UnicodeDecodeError as e:
|
||||
wheel = self.info_location.parent
|
||||
error = f"Error decoding metadata for {wheel}: {e} in {filename} file"
|
||||
raise UnsupportedWheel(error)
|
||||
return text
|
||||
|
||||
|
||||
class Distribution(BaseDistribution):
|
||||
def __init__(
|
||||
self,
|
||||
dist: importlib.metadata.Distribution,
|
||||
info_location: Optional[BasePath],
|
||||
installed_location: Optional[BasePath],
|
||||
) -> None:
|
||||
self._dist = dist
|
||||
self._info_location = info_location
|
||||
self._installed_location = installed_location
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, directory: str) -> BaseDistribution:
|
||||
info_location = pathlib.Path(directory)
|
||||
dist = importlib.metadata.Distribution.at(info_location)
|
||||
return cls(dist, info_location, info_location.parent)
|
||||
|
||||
@classmethod
|
||||
def from_metadata_file_contents(
|
||||
cls,
|
||||
metadata_contents: bytes,
|
||||
filename: str,
|
||||
project_name: str,
|
||||
) -> BaseDistribution:
|
||||
# Generate temp dir to contain the metadata file, and write the file contents.
|
||||
temp_dir = pathlib.Path(
|
||||
TempDirectory(kind="metadata", globally_managed=True).path
|
||||
)
|
||||
metadata_path = temp_dir / "METADATA"
|
||||
metadata_path.write_bytes(metadata_contents)
|
||||
# Construct dist pointing to the newly created directory.
|
||||
dist = importlib.metadata.Distribution.at(metadata_path.parent)
|
||||
return cls(dist, metadata_path.parent, None)
|
||||
|
||||
@classmethod
|
||||
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
||||
try:
|
||||
with wheel.as_zipfile() as zf:
|
||||
dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
|
||||
except zipfile.BadZipFile as e:
|
||||
raise InvalidWheel(wheel.location, name) from e
|
||||
except UnsupportedWheel as e:
|
||||
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
|
||||
return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
|
||||
|
||||
@property
|
||||
def location(self) -> Optional[str]:
|
||||
if self._info_location is None:
|
||||
return None
|
||||
return str(self._info_location.parent)
|
||||
|
||||
@property
|
||||
def info_location(self) -> Optional[str]:
|
||||
if self._info_location is None:
|
||||
return None
|
||||
return str(self._info_location)
|
||||
|
||||
@property
|
||||
def installed_location(self) -> Optional[str]:
|
||||
if self._installed_location is None:
|
||||
return None
|
||||
return normalize_path(str(self._installed_location))
|
||||
|
||||
def _get_dist_name_from_location(self) -> Optional[str]:
|
||||
"""Try to get the name from the metadata directory name.
|
||||
|
||||
This is much faster than reading metadata.
|
||||
"""
|
||||
if self._info_location is None:
|
||||
return None
|
||||
stem, suffix = os.path.splitext(self._info_location.name)
|
||||
if suffix not in (".dist-info", ".egg-info"):
|
||||
return None
|
||||
return stem.split("-", 1)[0]
|
||||
|
||||
@property
|
||||
def canonical_name(self) -> NormalizedName:
|
||||
name = self._get_dist_name_from_location() or get_dist_name(self._dist)
|
||||
return canonicalize_name(name)
|
||||
|
||||
@property
|
||||
def version(self) -> DistributionVersion:
|
||||
return parse_version(self._dist.version)
|
||||
|
||||
def is_file(self, path: InfoPath) -> bool:
|
||||
return self._dist.read_text(str(path)) is not None
|
||||
|
||||
def iter_distutils_script_names(self) -> Iterator[str]:
|
||||
# A distutils installation is always "flat" (not in e.g. egg form), so
|
||||
# if this distribution's info location is NOT a pathlib.Path (but e.g.
|
||||
# zipfile.Path), it can never contain any distutils scripts.
|
||||
if not isinstance(self._info_location, pathlib.Path):
|
||||
return
|
||||
for child in self._info_location.joinpath("scripts").iterdir():
|
||||
yield child.name
|
||||
|
||||
def read_text(self, path: InfoPath) -> str:
|
||||
content = self._dist.read_text(str(path))
|
||||
if content is None:
|
||||
raise FileNotFoundError(path)
|
||||
return content
|
||||
|
||||
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
||||
# importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
|
||||
return self._dist.entry_points
|
||||
|
||||
def _metadata_impl(self) -> email.message.Message:
|
||||
# From Python 3.10+, importlib.metadata declares PackageMetadata as the
|
||||
# return type. This protocol is unfortunately a disaster now and misses
|
||||
# a ton of fields that we need, including get() and get_payload(). We
|
||||
# rely on the implementation that the object is actually a Message now,
|
||||
# until upstream can improve the protocol. (python/cpython#94952)
|
||||
return cast(email.message.Message, self._dist.metadata)
|
||||
|
||||
def iter_provided_extras(self) -> Iterable[str]:
|
||||
return (
|
||||
safe_extra(extra) for extra in self.metadata.get_all("Provides-Extra", [])
|
||||
)
|
||||
|
||||
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
||||
contexts: Sequence[Dict[str, str]] = [{"extra": safe_extra(e)} for e in extras]
|
||||
for req_string in self.metadata.get_all("Requires-Dist", []):
|
||||
req = Requirement(req_string)
|
||||
if not req.marker:
|
||||
yield req
|
||||
elif not extras and req.marker.evaluate({"extra": ""}):
|
||||
yield req
|
||||
elif any(req.marker.evaluate(context) for context in contexts):
|
||||
yield req
|
188
venv/Lib/site-packages/pip/_internal/metadata/importlib/_envs.py
Normal file
188
venv/Lib/site-packages/pip/_internal/metadata/importlib/_envs.py
Normal file
@@ -0,0 +1,188 @@
|
||||
import functools
|
||||
import importlib.metadata
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
import zipfile
|
||||
import zipimport
|
||||
from typing import Iterator, List, Optional, Sequence, Set, Tuple
|
||||
|
||||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
|
||||
from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
|
||||
from pip._internal.models.wheel import Wheel
|
||||
from pip._internal.utils.deprecation import deprecated
|
||||
from pip._internal.utils.filetypes import WHEEL_EXTENSION
|
||||
|
||||
from ._compat import BadMetadata, BasePath, get_dist_name, get_info_location
|
||||
from ._dists import Distribution
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _looks_like_wheel(location: str) -> bool:
|
||||
if not location.endswith(WHEEL_EXTENSION):
|
||||
return False
|
||||
if not os.path.isfile(location):
|
||||
return False
|
||||
if not Wheel.wheel_file_re.match(os.path.basename(location)):
|
||||
return False
|
||||
return zipfile.is_zipfile(location)
|
||||
|
||||
|
||||
class _DistributionFinder:
|
||||
"""Finder to locate distributions.
|
||||
|
||||
The main purpose of this class is to memoize found distributions' names, so
|
||||
only one distribution is returned for each package name. At lot of pip code
|
||||
assumes this (because it is setuptools's behavior), and not doing the same
|
||||
can potentially cause a distribution in lower precedence path to override a
|
||||
higher precedence one if the caller is not careful.
|
||||
|
||||
Eventually we probably want to make it possible to see lower precedence
|
||||
installations as well. It's useful feature, after all.
|
||||
"""
|
||||
|
||||
FoundResult = Tuple[importlib.metadata.Distribution, Optional[BasePath]]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._found_names: Set[NormalizedName] = set()
|
||||
|
||||
def _find_impl(self, location: str) -> Iterator[FoundResult]:
|
||||
"""Find distributions in a location."""
|
||||
# Skip looking inside a wheel. Since a package inside a wheel is not
|
||||
# always valid (due to .data directories etc.), its .dist-info entry
|
||||
# should not be considered an installed distribution.
|
||||
if _looks_like_wheel(location):
|
||||
return
|
||||
# To know exactly where we find a distribution, we have to feed in the
|
||||
# paths one by one, instead of dumping the list to importlib.metadata.
|
||||
for dist in importlib.metadata.distributions(path=[location]):
|
||||
info_location = get_info_location(dist)
|
||||
try:
|
||||
raw_name = get_dist_name(dist)
|
||||
except BadMetadata as e:
|
||||
logger.warning("Skipping %s due to %s", info_location, e.reason)
|
||||
continue
|
||||
normalized_name = canonicalize_name(raw_name)
|
||||
if normalized_name in self._found_names:
|
||||
continue
|
||||
self._found_names.add(normalized_name)
|
||||
yield dist, info_location
|
||||
|
||||
def find(self, location: str) -> Iterator[BaseDistribution]:
|
||||
"""Find distributions in a location.
|
||||
|
||||
The path can be either a directory, or a ZIP archive.
|
||||
"""
|
||||
for dist, info_location in self._find_impl(location):
|
||||
if info_location is None:
|
||||
installed_location: Optional[BasePath] = None
|
||||
else:
|
||||
installed_location = info_location.parent
|
||||
yield Distribution(dist, info_location, installed_location)
|
||||
|
||||
def find_linked(self, location: str) -> Iterator[BaseDistribution]:
|
||||
"""Read location in egg-link files and return distributions in there.
|
||||
|
||||
The path should be a directory; otherwise this returns nothing. This
|
||||
follows how setuptools does this for compatibility. The first non-empty
|
||||
line in the egg-link is read as a path (resolved against the egg-link's
|
||||
containing directory if relative). Distributions found at that linked
|
||||
location are returned.
|
||||
"""
|
||||
path = pathlib.Path(location)
|
||||
if not path.is_dir():
|
||||
return
|
||||
for child in path.iterdir():
|
||||
if child.suffix != ".egg-link":
|
||||
continue
|
||||
with child.open() as f:
|
||||
lines = (line.strip() for line in f)
|
||||
target_rel = next((line for line in lines if line), "")
|
||||
if not target_rel:
|
||||
continue
|
||||
target_location = str(path.joinpath(target_rel))
|
||||
for dist, info_location in self._find_impl(target_location):
|
||||
yield Distribution(dist, info_location, path)
|
||||
|
||||
def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
|
||||
from pip._vendor.pkg_resources import find_distributions
|
||||
|
||||
from pip._internal.metadata import pkg_resources as legacy
|
||||
|
||||
with os.scandir(location) as it:
|
||||
for entry in it:
|
||||
if not entry.name.endswith(".egg"):
|
||||
continue
|
||||
for dist in find_distributions(entry.path):
|
||||
yield legacy.Distribution(dist)
|
||||
|
||||
def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
|
||||
from pip._vendor.pkg_resources import find_eggs_in_zip
|
||||
|
||||
from pip._internal.metadata import pkg_resources as legacy
|
||||
|
||||
try:
|
||||
importer = zipimport.zipimporter(location)
|
||||
except zipimport.ZipImportError:
|
||||
return
|
||||
for dist in find_eggs_in_zip(importer, location):
|
||||
yield legacy.Distribution(dist)
|
||||
|
||||
def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
|
||||
"""Find eggs in a location.
|
||||
|
||||
This actually uses the old *pkg_resources* backend. We likely want to
|
||||
deprecate this so we can eventually remove the *pkg_resources*
|
||||
dependency entirely. Before that, this should first emit a deprecation
|
||||
warning for some versions when using the fallback since importing
|
||||
*pkg_resources* is slow for those who don't need it.
|
||||
"""
|
||||
if os.path.isdir(location):
|
||||
yield from self._find_eggs_in_dir(location)
|
||||
if zipfile.is_zipfile(location):
|
||||
yield from self._find_eggs_in_zip(location)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None) # Warn a distribution exactly once.
|
||||
def _emit_egg_deprecation(location: Optional[str]) -> None:
|
||||
deprecated(
|
||||
reason=f"Loading egg at {location} is deprecated.",
|
||||
replacement="to use pip for package installation.",
|
||||
gone_in="23.3",
|
||||
)
|
||||
|
||||
|
||||
class Environment(BaseEnvironment):
|
||||
def __init__(self, paths: Sequence[str]) -> None:
|
||||
self._paths = paths
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> BaseEnvironment:
|
||||
return cls(sys.path)
|
||||
|
||||
@classmethod
|
||||
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
|
||||
if paths is None:
|
||||
return cls(sys.path)
|
||||
return cls(paths)
|
||||
|
||||
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
||||
finder = _DistributionFinder()
|
||||
for location in self._paths:
|
||||
yield from finder.find(location)
|
||||
for dist in finder.find_eggs(location):
|
||||
_emit_egg_deprecation(dist.location)
|
||||
yield dist
|
||||
# This must go last because that's how pkg_resources tie-breaks.
|
||||
yield from finder.find_linked(location)
|
||||
|
||||
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
|
||||
matches = (
|
||||
distribution
|
||||
for distribution in self.iter_all_distributions()
|
||||
if distribution.canonical_name == canonicalize_name(name)
|
||||
)
|
||||
return next(matches, None)
|
270
venv/Lib/site-packages/pip/_internal/metadata/pkg_resources.py
Normal file
270
venv/Lib/site-packages/pip/_internal/metadata/pkg_resources.py
Normal file
@@ -0,0 +1,270 @@
|
||||
import email.message
|
||||
import email.parser
|
||||
import logging
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
|
||||
|
||||
from pip._vendor import pkg_resources
|
||||
from pip._vendor.packaging.requirements import Requirement
|
||||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
|
||||
from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
|
||||
from pip._internal.utils.egg_link import egg_link_path_from_location
|
||||
from pip._internal.utils.misc import display_path, normalize_path
|
||||
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
|
||||
|
||||
from .base import (
|
||||
BaseDistribution,
|
||||
BaseEntryPoint,
|
||||
BaseEnvironment,
|
||||
DistributionVersion,
|
||||
InfoPath,
|
||||
Wheel,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EntryPoint(NamedTuple):
|
||||
name: str
|
||||
value: str
|
||||
group: str
|
||||
|
||||
|
||||
class InMemoryMetadata:
|
||||
"""IMetadataProvider that reads metadata files from a dictionary.
|
||||
|
||||
This also maps metadata decoding exceptions to our internal exception type.
|
||||
"""
|
||||
|
||||
def __init__(self, metadata: Mapping[str, bytes], wheel_name: str) -> None:
|
||||
self._metadata = metadata
|
||||
self._wheel_name = wheel_name
|
||||
|
||||
def has_metadata(self, name: str) -> bool:
|
||||
return name in self._metadata
|
||||
|
||||
def get_metadata(self, name: str) -> str:
|
||||
try:
|
||||
return self._metadata[name].decode()
|
||||
except UnicodeDecodeError as e:
|
||||
# Augment the default error with the origin of the file.
|
||||
raise UnsupportedWheel(
|
||||
f"Error decoding metadata for {self._wheel_name}: {e} in {name} file"
|
||||
)
|
||||
|
||||
def get_metadata_lines(self, name: str) -> Iterable[str]:
|
||||
return pkg_resources.yield_lines(self.get_metadata(name))
|
||||
|
||||
def metadata_isdir(self, name: str) -> bool:
|
||||
return False
|
||||
|
||||
def metadata_listdir(self, name: str) -> List[str]:
|
||||
return []
|
||||
|
||||
def run_script(self, script_name: str, namespace: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class Distribution(BaseDistribution):
|
||||
def __init__(self, dist: pkg_resources.Distribution) -> None:
|
||||
self._dist = dist
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, directory: str) -> BaseDistribution:
|
||||
dist_dir = directory.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("-")[0]
|
||||
|
||||
dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
|
||||
return cls(dist)
|
||||
|
||||
@classmethod
|
||||
def from_metadata_file_contents(
|
||||
cls,
|
||||
metadata_contents: bytes,
|
||||
filename: str,
|
||||
project_name: str,
|
||||
) -> BaseDistribution:
|
||||
metadata_dict = {
|
||||
"METADATA": metadata_contents,
|
||||
}
|
||||
dist = pkg_resources.DistInfoDistribution(
|
||||
location=filename,
|
||||
metadata=InMemoryMetadata(metadata_dict, filename),
|
||||
project_name=project_name,
|
||||
)
|
||||
return cls(dist)
|
||||
|
||||
@classmethod
|
||||
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
|
||||
try:
|
||||
with wheel.as_zipfile() as zf:
|
||||
info_dir, _ = parse_wheel(zf, name)
|
||||
metadata_dict = {
|
||||
path.split("/", 1)[-1]: read_wheel_metadata_file(zf, path)
|
||||
for path in zf.namelist()
|
||||
if path.startswith(f"{info_dir}/")
|
||||
}
|
||||
except zipfile.BadZipFile as e:
|
||||
raise InvalidWheel(wheel.location, name) from e
|
||||
except UnsupportedWheel as e:
|
||||
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
|
||||
dist = pkg_resources.DistInfoDistribution(
|
||||
location=wheel.location,
|
||||
metadata=InMemoryMetadata(metadata_dict, wheel.location),
|
||||
project_name=name,
|
||||
)
|
||||
return cls(dist)
|
||||
|
||||
@property
|
||||
def location(self) -> Optional[str]:
|
||||
return self._dist.location
|
||||
|
||||
@property
|
||||
def installed_location(self) -> Optional[str]:
|
||||
egg_link = egg_link_path_from_location(self.raw_name)
|
||||
if egg_link:
|
||||
location = egg_link
|
||||
elif self.location:
|
||||
location = self.location
|
||||
else:
|
||||
return None
|
||||
return normalize_path(location)
|
||||
|
||||
@property
|
||||
def info_location(self) -> Optional[str]:
|
||||
return self._dist.egg_info
|
||||
|
||||
@property
|
||||
def installed_by_distutils(self) -> bool:
|
||||
# A distutils-installed distribution is provided by FileMetadata. This
|
||||
# provider has a "path" attribute not present anywhere else. Not the
|
||||
# best introspection logic, but pip has been doing this for a long time.
|
||||
try:
|
||||
return bool(self._dist._provider.path)
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
@property
|
||||
def canonical_name(self) -> NormalizedName:
|
||||
return canonicalize_name(self._dist.project_name)
|
||||
|
||||
@property
|
||||
def version(self) -> DistributionVersion:
|
||||
return parse_version(self._dist.version)
|
||||
|
||||
def is_file(self, path: InfoPath) -> bool:
|
||||
return self._dist.has_metadata(str(path))
|
||||
|
||||
def iter_distutils_script_names(self) -> Iterator[str]:
|
||||
yield from self._dist.metadata_listdir("scripts")
|
||||
|
||||
def read_text(self, path: InfoPath) -> str:
|
||||
name = str(path)
|
||||
if not self._dist.has_metadata(name):
|
||||
raise FileNotFoundError(name)
|
||||
content = self._dist.get_metadata(name)
|
||||
if content is None:
|
||||
raise NoneMetadataError(self, name)
|
||||
return content
|
||||
|
||||
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
|
||||
for group, entries in self._dist.get_entry_map().items():
|
||||
for name, entry_point in entries.items():
|
||||
name, _, value = str(entry_point).partition("=")
|
||||
yield EntryPoint(name=name.strip(), value=value.strip(), group=group)
|
||||
|
||||
def _metadata_impl(self) -> email.message.Message:
|
||||
"""
|
||||
:raises NoneMetadataError: if the distribution reports `has_metadata()`
|
||||
True but `get_metadata()` returns None.
|
||||
"""
|
||||
if isinstance(self._dist, pkg_resources.DistInfoDistribution):
|
||||
metadata_name = "METADATA"
|
||||
else:
|
||||
metadata_name = "PKG-INFO"
|
||||
try:
|
||||
metadata = self.read_text(metadata_name)
|
||||
except FileNotFoundError:
|
||||
if self.location:
|
||||
displaying_path = display_path(self.location)
|
||||
else:
|
||||
displaying_path = repr(self.location)
|
||||
logger.warning("No metadata found in %s", displaying_path)
|
||||
metadata = ""
|
||||
feed_parser = email.parser.FeedParser()
|
||||
feed_parser.feed(metadata)
|
||||
return feed_parser.close()
|
||||
|
||||
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
|
||||
if extras: # pkg_resources raises on invalid extras, so we sanitize.
|
||||
extras = frozenset(extras).intersection(self._dist.extras)
|
||||
return self._dist.requires(extras)
|
||||
|
||||
def iter_provided_extras(self) -> Iterable[str]:
|
||||
return self._dist.extras
|
||||
|
||||
|
||||
class Environment(BaseEnvironment):
|
||||
def __init__(self, ws: pkg_resources.WorkingSet) -> None:
|
||||
self._ws = ws
|
||||
|
||||
@classmethod
|
||||
def default(cls) -> BaseEnvironment:
|
||||
return cls(pkg_resources.working_set)
|
||||
|
||||
@classmethod
|
||||
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
|
||||
return cls(pkg_resources.WorkingSet(paths))
|
||||
|
||||
def _iter_distributions(self) -> Iterator[BaseDistribution]:
|
||||
for dist in self._ws:
|
||||
yield Distribution(dist)
|
||||
|
||||
def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
|
||||
"""Find a distribution matching the ``name`` in the environment.
|
||||
|
||||
This searches from *all* distributions available in the environment, to
|
||||
match the behavior of ``pkg_resources.get_distribution()``.
|
||||
"""
|
||||
canonical_name = canonicalize_name(name)
|
||||
for dist in self.iter_all_distributions():
|
||||
if dist.canonical_name == canonical_name:
|
||||
return dist
|
||||
return None
|
||||
|
||||
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
|
||||
# Search the distribution by looking through the working set.
|
||||
dist = self._search_distribution(name)
|
||||
if dist:
|
||||
return dist
|
||||
|
||||
# If distribution could not be found, call working_set.require to
|
||||
# update the working set, and try to find the distribution again.
|
||||
# This might happen for e.g. when you install a package twice, once
|
||||
# using setup.py develop and again using setup.py install. Now when
|
||||
# running pip uninstall twice, the package gets removed from the
|
||||
# working set in the first uninstall, so we have to populate the
|
||||
# working set again so that pip knows about it and the packages gets
|
||||
# picked up and is successfully uninstalled the second time too.
|
||||
try:
|
||||
# We didn't pass in any version specifiers, so this can never
|
||||
# raise pkg_resources.VersionConflict.
|
||||
self._ws.require(name)
|
||||
except pkg_resources.DistributionNotFound:
|
||||
return None
|
||||
return self._search_distribution(name)
|
Reference in New Issue
Block a user