Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -1,18 +1,17 @@
from __future__ import annotations
import contextlib
import functools
import os
import sys
from typing import TYPE_CHECKING, Literal, Protocol, cast
from typing import TYPE_CHECKING, List, Optional, Type, cast
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.misc import strtobool
from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
if TYPE_CHECKING:
from pip._vendor.packaging.utils import NormalizedName
from typing import Literal, Protocol
else:
Protocol = object
__all__ = [
"BaseDistribution",
@ -31,75 +30,37 @@ 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_resources`` otherwise. Up to Python 3.13, This can be
overridden by a couple of ways:
``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, for Python <3.14.
* On Python 3.11, 3.12 and 3.13, 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*).
On Python 3.14+, the ``pkg_resources`` backend cannot be used.
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*).
"""
if sys.version_info >= (3, 14):
# On Python >=3.14 we only support importlib.metadata.
return True
with contextlib.suppress(KeyError, ValueError):
# On Python <3.14, if the environment variable is set, we obey what it says.
return bool(strtobool(os.environ["_PIP_USE_IMPORTLIB_METADATA"]))
if sys.version_info < (3, 11):
# On Python <3.11, we always use pkg_resources, unless the environment
# variable was set.
return False
# On Python 3.11, 3.12 and 3.13, we check if the global constant is set.
import importlib.metadata
return bool(getattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA", True))
def _emit_pkg_resources_deprecation_if_needed() -> None:
if sys.version_info < (3, 11):
# All pip versions supporting Python<=3.11 will support pkg_resources,
# and pkg_resources is the default for these, so let's not bother users.
return
import importlib.metadata
if hasattr(importlib.metadata, "_PIP_USE_IMPORTLIB_METADATA"):
# The Python distributor has set the global constant, so we don't
# warn, since it is not a user decision.
return
# The user has decided to use pkg_resources, so we warn.
deprecated(
reason="Using the pkg_resources metadata backend is deprecated.",
replacement=(
"to use the default importlib.metadata backend, "
"by unsetting the _PIP_USE_IMPORTLIB_METADATA environment variable"
),
gone_in="26.3",
issue=13317,
)
class Backend(Protocol):
NAME: Literal["importlib", "pkg_resources"]
Distribution: type[BaseDistribution]
Environment: type[BaseEnvironment]
NAME: 'Literal["importlib", "pkg_resources"]'
Distribution: Type[BaseDistribution]
Environment: Type[BaseEnvironment]
@functools.cache
@functools.lru_cache(maxsize=None)
def select_backend() -> Backend:
if _should_use_importlib_metadata():
from . import importlib
return cast(Backend, importlib)
_emit_pkg_resources_deprecation_if_needed()
from . import pkg_resources
return cast(Backend, pkg_resources)
@ -110,12 +71,12 @@ def get_default_environment() -> BaseEnvironment:
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 across calls.
to share instance state accorss calls.
"""
return select_backend().Environment.default()
def get_environment(paths: list[str] | None) -> BaseEnvironment:
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
@ -134,9 +95,7 @@ def get_directory_distribution(directory: str) -> BaseDistribution:
return select_backend().Distribution.from_directory(directory)
def get_wheel_distribution(
wheel: Wheel, canonical_name: NormalizedName
) -> BaseDistribution:
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

View file

@ -1,9 +1,8 @@
# Extracted from https://github.com/pfmoore/pkg_metadata
from __future__ import annotations
from email.header import Header, decode_header, make_header
from email.message import Message
from typing import Any, cast
from typing import Any, Dict, List, Union
METADATA_FIELDS = [
# Name, Multiple-Use
@ -24,8 +23,6 @@ METADATA_FIELDS = [
("Maintainer", False),
("Maintainer-email", False),
("License", False),
("License-Expression", False),
("License-File", True),
("Classifier", True),
("Requires-Dist", True),
("Requires-Python", False),
@ -41,10 +38,10 @@ def json_name(field: str) -> str:
return field.lower().replace("-", "_")
def msg_to_json(msg: Message) -> dict[str, Any]:
def msg_to_json(msg: Message) -> Dict[str, Any]:
"""Convert a Message object into a JSON-compatible dictionary."""
def sanitise_header(h: Header | str) -> str:
def sanitise_header(h: Union[Header, str]) -> str:
if isinstance(h, Header):
chunks = []
for bytes, encoding in decode_header(h):
@ -66,7 +63,7 @@ def msg_to_json(msg: Message) -> dict[str, Any]:
continue
key = json_name(field)
if multi:
value: str | list[str] = [
value: Union[str, List[str]] = [
sanitise_header(v) for v in msg.get_all(field) # type: ignore
]
else:
@ -80,7 +77,7 @@ def msg_to_json(msg: Message) -> dict[str, Any]:
value = value.split()
result[key] = value
payload = cast(str, msg.get_payload())
payload = msg.get_payload()
if payload:
result["description"] = payload

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import csv
import email.message
import functools
@ -8,19 +6,26 @@ import logging
import pathlib
import re
import zipfile
from collections.abc import Collection, Container, Iterable, Iterator
from typing import (
IO,
TYPE_CHECKING,
Any,
Collection,
Container,
Dict,
Iterable,
Iterator,
List,
NamedTuple,
Protocol,
Optional,
Tuple,
Union,
)
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.packaging.version import Version
from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.exceptions import NoneMetadataError
from pip._internal.locations import site_packages, user_site
@ -36,6 +41,13 @@ from pip._internal.utils.urls import url_to_path
from ._json import msg_to_json
if TYPE_CHECKING:
from typing import Protocol
else:
Protocol = object
DistributionVersion = Union[LegacyVersion, Version]
InfoPath = Union[str, pathlib.PurePath]
logger = logging.getLogger(__name__)
@ -56,8 +68,8 @@ class BaseEntryPoint(Protocol):
def _convert_installed_files_path(
entry: tuple[str, ...],
info: tuple[str, ...],
entry: Tuple[str, ...],
info: Tuple[str, ...],
) -> str:
"""Convert a legacy installed-files.txt path into modern RECORD path.
@ -93,7 +105,7 @@ class RequiresEntry(NamedTuple):
class BaseDistribution(Protocol):
@classmethod
def from_directory(cls, directory: str) -> BaseDistribution:
def from_directory(cls, directory: str) -> "BaseDistribution":
"""Load the distribution from a metadata directory.
:param directory: Path to a metadata directory, e.g. ``.dist-info``.
@ -106,7 +118,7 @@ class BaseDistribution(Protocol):
metadata_contents: bytes,
filename: str,
project_name: str,
) -> BaseDistribution:
) -> "BaseDistribution":
"""Load the distribution from the contents of a METADATA file.
This is used to implement PEP 658 by generating a "shallow" dist object that can
@ -119,7 +131,7 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@classmethod
def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
"""Load the distribution from a given wheel.
:param wheel: A concrete wheel definition.
@ -133,13 +145,13 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
def __repr__(self) -> str:
return f"{self.raw_name} {self.raw_version} ({self.location})"
return f"{self.raw_name} {self.version} ({self.location})"
def __str__(self) -> str:
return f"{self.raw_name} {self.raw_version}"
return f"{self.raw_name} {self.version}"
@property
def location(self) -> str | None:
def location(self) -> Optional[str]:
"""Where the distribution is loaded from.
A string value is not necessarily a filesystem path, since distributions
@ -153,7 +165,7 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def editable_project_location(self) -> str | None:
def editable_project_location(self) -> Optional[str]:
"""The project location for editable distributions.
This is the directory where pyproject.toml or setup.py is located.
@ -175,7 +187,7 @@ class BaseDistribution(Protocol):
return None
@property
def installed_location(self) -> str | None:
def installed_location(self) -> Optional[str]:
"""The distribution's "installed" location.
This should generally be a ``site-packages`` directory. This is
@ -188,7 +200,7 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def info_location(self) -> str | None:
def info_location(self) -> Optional[str]:
"""Location of the .[egg|dist]-info directory or file.
Similarly to ``location``, a string value is not necessarily a
@ -226,9 +238,7 @@ class BaseDistribution(Protocol):
location = self.location
if not location:
return False
# XXX if the distribution is a zipped egg, location has a trailing /
# so we resort to pathlib.Path to check the suffix in a reliable way.
return pathlib.Path(location).suffix == ".egg"
return location.endswith(".egg")
@property
def installed_with_setuptools_egg_info(self) -> bool:
@ -269,11 +279,7 @@ class BaseDistribution(Protocol):
raise NotImplementedError()
@property
def version(self) -> Version:
raise NotImplementedError()
@property
def raw_version(self) -> str:
def version(self) -> DistributionVersion:
raise NotImplementedError()
@property
@ -285,7 +291,7 @@ class BaseDistribution(Protocol):
return self.raw_name.replace("-", "_")
@property
def direct_url(self) -> DirectUrl | None:
def direct_url(self) -> Optional[DirectUrl]:
"""Obtain a DirectUrl from this distribution.
Returns None if the distribution has no `direct_url.json` metadata,
@ -379,7 +385,15 @@ class BaseDistribution(Protocol):
def _metadata_impl(self) -> email.message.Message:
raise NotImplementedError()
@functools.cached_property
@functools.lru_cache(maxsize=1)
def _metadata_cached(self) -> email.message.Message:
# When we drop python 3.7 support, move this to the metadata property and use
# functools.cached_property instead of lru_cache.
metadata = self._metadata_impl()
self._add_egg_info_requires(metadata)
return metadata
@property
def metadata(self) -> email.message.Message:
"""Metadata of distribution parsed from e.g. METADATA or PKG-INFO.
@ -388,12 +402,10 @@ class BaseDistribution(Protocol):
:raises NoneMetadataError: If the metadata file is available, but does
not contain valid metadata.
"""
metadata = self._metadata_impl()
self._add_egg_info_requires(metadata)
return metadata
return self._metadata_cached()
@property
def metadata_dict(self) -> dict[str, Any]:
def metadata_dict(self) -> Dict[str, Any]:
"""PEP 566 compliant JSON-serializable representation of METADATA or PKG-INFO.
This should return an empty dict if the metadata file is unavailable.
@ -404,7 +416,7 @@ class BaseDistribution(Protocol):
return msg_to_json(self.metadata)
@property
def metadata_version(self) -> str | None:
def metadata_version(self) -> Optional[str]:
"""Value of "Metadata-Version:" in distribution metadata, if available."""
return self.metadata.get("Metadata-Version")
@ -442,23 +454,28 @@ class BaseDistribution(Protocol):
"""
raise NotImplementedError()
def iter_raw_dependencies(self) -> Iterable[str]:
"""Raw Requires-Dist metadata."""
return self.metadata.get_all("Requires-Dist", [])
def iter_provided_extras(self) -> Iterable[NormalizedName]:
def iter_provided_extras(self) -> Iterable[str]:
"""Extras provided by this distribution.
For modern .dist-info distributions, this is the collection of
"Provides-Extra:" entries in distribution metadata.
The return value of this function is expected to be normalised names,
per PEP 685, with the returned value being handled appropriately by
`iter_dependencies`.
The return value of this function is not particularly useful other than
display purposes due to backward compatibility issues and the extra
names being poorly normalized prior to PEP 685. If you want to perform
logic operations on extras, use :func:`is_extra_provided` instead.
"""
raise NotImplementedError()
def _iter_declared_entries_from_record(self) -> Iterator[str] | None:
def is_extra_provided(self, extra: str) -> bool:
"""Check whether an extra is provided by this distribution.
This is needed mostly for compatibility issues with pkg_resources not
following the extra normalization rules defined in PEP 685.
"""
raise NotImplementedError()
def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
try:
text = self.read_text("RECORD")
except FileNotFoundError:
@ -466,7 +483,7 @@ class BaseDistribution(Protocol):
# This extra Path-str cast normalizes entries.
return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
def _iter_declared_entries_from_legacy(self) -> Iterator[str] | None:
def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
try:
text = self.read_text("installed-files.txt")
except FileNotFoundError:
@ -487,7 +504,7 @@ class BaseDistribution(Protocol):
for p in paths
)
def iter_declared_entries(self) -> Iterator[str] | None:
def iter_declared_entries(self) -> Optional[Iterator[str]]:
"""Iterate through file entries declared in this distribution.
For modern .dist-info distributions, this is the files listed in the
@ -580,14 +597,14 @@ class BaseEnvironment:
"""An environment containing distributions to introspect."""
@classmethod
def default(cls) -> BaseEnvironment:
def default(cls) -> "BaseEnvironment":
raise NotImplementedError()
@classmethod
def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:
def from_paths(cls, paths: Optional[List[str]]) -> "BaseEnvironment":
raise NotImplementedError()
def get_distribution(self, name: str) -> BaseDistribution | None:
def get_distribution(self, name: str) -> Optional["BaseDistribution"]:
"""Given a requirement name, return the installed distributions.
The name may not be normalized. The implementation must canonicalize
@ -595,7 +612,7 @@ class BaseEnvironment:
"""
raise NotImplementedError()
def _iter_distributions(self) -> Iterator[BaseDistribution]:
def _iter_distributions(self) -> Iterator["BaseDistribution"]:
"""Iterate through installed distributions.
This function should be implemented by subclass, but never called

View file

@ -1,10 +1,5 @@
from __future__ import annotations
import importlib.metadata
import os
from typing import Any, Protocol, cast
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from typing import Any, Optional, Protocol, cast
class BadMetadata(ValueError):
@ -32,11 +27,11 @@ class BasePath(Protocol):
raise NotImplementedError()
@property
def parent(self) -> BasePath:
def parent(self) -> "BasePath":
raise NotImplementedError()
def get_info_location(d: importlib.metadata.Distribution) -> BasePath | None:
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
@ -48,40 +43,13 @@ def get_info_location(d: importlib.metadata.Distribution) -> BasePath | None:
return getattr(d, "_path", None)
def parse_name_and_version_from_info_directory(
dist: importlib.metadata.Distribution,
) -> tuple[str | None, str | None]:
"""Get a name and version from the metadata directory name.
This is much faster than reading distribution metadata.
"""
info_location = get_info_location(dist)
if info_location is None:
return None, None
stem, suffix = os.path.splitext(info_location.name)
if suffix == ".dist-info":
name, sep, version = stem.partition("-")
if sep:
return name, version
if suffix == ".egg-info":
name = stem.split("-", 1)[0]
return name, None
return None, None
def get_dist_canonical_name(dist: importlib.metadata.Distribution) -> NormalizedName:
"""Get the distribution's normalized name.
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.
"""
if name := parse_name_and_version_from_info_directory(dist)[0]:
return canonicalize_name(name)
name = cast(Any, dist).name
if not isinstance(name, str):
raise BadMetadata(dist, reason="invalid metadata entry 'name'")
return canonicalize_name(name)
return name

View file

@ -1,38 +1,36 @@
from __future__ import annotations
import email.message
import importlib.metadata
import os
import pathlib
import zipfile
from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence
from os import PathLike
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 Version
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 get_requirement
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
from ._compat import (
BadMetadata,
BasePath,
get_dist_canonical_name,
parse_name_and_version_from_info_directory,
)
from ._compat import BasePath, get_dist_name
class WheelDistribution(importlib.metadata.Distribution):
@ -60,7 +58,7 @@ class WheelDistribution(importlib.metadata.Distribution):
zf: zipfile.ZipFile,
name: str,
location: str,
) -> WheelDistribution:
) -> "WheelDistribution":
info_dir, _ = parse_wheel(zf, name)
paths = (
(name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
@ -80,7 +78,7 @@ class WheelDistribution(importlib.metadata.Distribution):
return iter(self._files)
raise FileNotFoundError(path)
def read_text(self, filename: str) -> str | None:
def read_text(self, filename: str) -> Optional[str]:
try:
data = self._files[pathlib.PurePosixPath(filename)]
except KeyError:
@ -93,18 +91,13 @@ class WheelDistribution(importlib.metadata.Distribution):
raise UnsupportedWheel(error)
return text
def locate_file(self, path: str | PathLike[str]) -> pathlib.Path:
# This method doesn't make sense for our in-memory wheel, but the API
# requires us to define it.
raise NotImplementedError
class Distribution(BaseDistribution):
def __init__(
self,
dist: importlib.metadata.Distribution,
info_location: BasePath | None,
installed_location: BasePath | None,
info_location: Optional[BasePath],
installed_location: Optional[BasePath],
) -> None:
self._dist = dist
self._info_location = info_location
@ -140,44 +133,48 @@ class Distribution(BaseDistribution):
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) -> str | None:
def location(self) -> Optional[str]:
if self._info_location is None:
return None
return str(self._info_location.parent)
@property
def info_location(self) -> str | None:
def info_location(self) -> Optional[str]:
if self._info_location is None:
return None
return str(self._info_location)
@property
def installed_location(self) -> str | None:
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:
return get_dist_canonical_name(self._dist)
name = self._get_dist_name_from_location() or get_dist_name(self._dist)
return canonicalize_name(name)
@property
def version(self) -> Version:
try:
version = (
parse_name_and_version_from_info_directory(self._dist)[1]
or self._dist.version
)
return parse_version(version)
except TypeError:
raise BadMetadata(self._dist, reason="invalid metadata entry `version`")
@property
def raw_version(self) -> str:
return self._dist.version
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
@ -198,7 +195,7 @@ class Distribution(BaseDistribution):
return content
def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
# importlib.metadata's EntryPoint structure satisfies BaseEntryPoint.
# importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
return self._dist.entry_points
def _metadata_impl(self) -> email.message.Message:
@ -209,18 +206,19 @@ class Distribution(BaseDistribution):
# until upstream can improve the protocol. (python/cpython#94952)
return cast(email.message.Message, self._dist.metadata)
def iter_provided_extras(self) -> Iterable[NormalizedName]:
return [
canonicalize_name(extra)
for extra in self.metadata.get_all("Provides-Extra", [])
]
def iter_provided_extras(self) -> Iterable[str]:
return self.metadata.get_all("Provides-Extra", [])
def is_extra_provided(self, extra: str) -> bool:
return any(
canonicalize_name(provided_extra) == canonicalize_name(extra)
for provided_extra in self.metadata.get_all("Provides-Extra", [])
)
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
contexts: Sequence[dict[str, str]] = [{"extra": e} for e in extras]
contexts: Sequence[Dict[str, str]] = [{"extra": e} for e in extras]
for req_string in self.metadata.get_all("Requires-Dist", []):
# strip() because email.message.Message.get_all() may return a leading \n
# in case a long header was wrapped.
req = get_requirement(req_string.strip())
req = Requirement(req_string)
if not req.marker:
yield req
elif not extras and req.marker.evaluate({"extra": ""}):

View file

@ -1,25 +1,21 @@
from __future__ import annotations
import functools
import importlib.metadata
import logging
import os
import pathlib
import sys
import zipfile
from collections.abc import Iterator, Sequence
from typing import Optional
import zipimport
from typing import Iterator, List, Optional, Sequence, Set, Tuple
from pip._vendor.packaging.utils import (
InvalidWheelFilename,
NormalizedName,
canonicalize_name,
parse_wheel_filename,
)
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_canonical_name, get_info_location
from ._compat import BadMetadata, BasePath, get_dist_name, get_info_location
from ._dists import Distribution
logger = logging.getLogger(__name__)
@ -30,9 +26,7 @@ def _looks_like_wheel(location: str) -> bool:
return False
if not os.path.isfile(location):
return False
try:
parse_wheel_filename(os.path.basename(location))
except InvalidWheelFilename:
if not Wheel.wheel_file_re.match(os.path.basename(location)):
return False
return zipfile.is_zipfile(location)
@ -50,10 +44,10 @@ class _DistributionFinder:
installations as well. It's useful feature, after all.
"""
FoundResult = tuple[importlib.metadata.Distribution, Optional[BasePath]]
FoundResult = Tuple[importlib.metadata.Distribution, Optional[BasePath]]
def __init__(self) -> None:
self._found_names: set[NormalizedName] = set()
self._found_names: Set[NormalizedName] = set()
def _find_impl(self, location: str) -> Iterator[FoundResult]:
"""Find distributions in a location."""
@ -67,13 +61,14 @@ class _DistributionFinder:
for dist in importlib.metadata.distributions(path=[location]):
info_location = get_info_location(dist)
try:
name = get_dist_canonical_name(dist)
raw_name = get_dist_name(dist)
except BadMetadata as e:
logger.warning("Skipping %s due to %s", info_location, e.reason)
continue
if name in self._found_names:
normalized_name = canonicalize_name(raw_name)
if normalized_name in self._found_names:
continue
self._found_names.add(name)
self._found_names.add(normalized_name)
yield dist, info_location
def find(self, location: str) -> Iterator[BaseDistribution]:
@ -83,12 +78,12 @@ class _DistributionFinder:
"""
for dist, info_location in self._find_impl(location):
if info_location is None:
installed_location: BasePath | None = None
installed_location: Optional[BasePath] = None
else:
installed_location = info_location.parent
yield Distribution(dist, info_location, installed_location)
def find_legacy_editables(self, location: str) -> Iterator[BaseDistribution]:
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
@ -112,6 +107,54 @@ class _DistributionFinder:
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="24.3",
issue=12330,
)
class Environment(BaseEnvironment):
def __init__(self, paths: Sequence[str]) -> None:
@ -122,7 +165,7 @@ class Environment(BaseEnvironment):
return cls(sys.path)
@classmethod
def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:
def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
if paths is None:
return cls(sys.path)
return cls(paths)
@ -131,13 +174,16 @@ class Environment(BaseEnvironment):
finder = _DistributionFinder()
for location in self._paths:
yield from finder.find(location)
yield from finder.find_legacy_editables(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) -> BaseDistribution | None:
canonical_name = canonicalize_name(name)
def get_distribution(self, name: str) -> Optional[BaseDistribution]:
matches = (
distribution
for distribution in self.iter_all_distributions()
if distribution.canonical_name == canonical_name
if distribution.canonical_name == canonicalize_name(name)
)
return next(matches, None)

View file

@ -1,19 +1,13 @@
from __future__ import annotations
import email.message
import email.parser
import logging
import os
import zipfile
from collections.abc import Collection, Iterable, Iterator, Mapping
from typing import (
NamedTuple,
)
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 Version
from pip._vendor.packaging.version import parse as parse_version
from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
@ -25,6 +19,7 @@ from .base import (
BaseDistribution,
BaseEntryPoint,
BaseEnvironment,
DistributionVersion,
InfoPath,
Wheel,
)
@ -70,7 +65,7 @@ class InMemoryMetadata:
def metadata_isdir(self, name: str) -> bool:
return False
def metadata_listdir(self, name: str) -> list[str]:
def metadata_listdir(self, name: str) -> List[str]:
return []
def run_script(self, script_name: str, namespace: str) -> None:
@ -80,18 +75,6 @@ class InMemoryMetadata:
class Distribution(BaseDistribution):
def __init__(self, dist: pkg_resources.Distribution) -> None:
self._dist = dist
# This is populated lazily, to avoid loading metadata for all possible
# distributions eagerly.
self.__extra_mapping: Mapping[NormalizedName, str] | None = None
@property
def _extra_mapping(self) -> Mapping[NormalizedName, str]:
if self.__extra_mapping is None:
self.__extra_mapping = {
canonicalize_name(extra): extra for extra in self._dist.extras
}
return self.__extra_mapping
@classmethod
def from_directory(cls, directory: str) -> BaseDistribution:
@ -152,11 +135,11 @@ class Distribution(BaseDistribution):
return cls(dist)
@property
def location(self) -> str | None:
def location(self) -> Optional[str]:
return self._dist.location
@property
def installed_location(self) -> str | None:
def installed_location(self) -> Optional[str]:
egg_link = egg_link_path_from_location(self.raw_name)
if egg_link:
location = egg_link
@ -167,7 +150,7 @@ class Distribution(BaseDistribution):
return normalize_path(location)
@property
def info_location(self) -> str | None:
def info_location(self) -> Optional[str]:
return self._dist.egg_info
@property
@ -185,13 +168,9 @@ class Distribution(BaseDistribution):
return canonicalize_name(self._dist.project_name)
@property
def version(self) -> Version:
def version(self) -> DistributionVersion:
return parse_version(self._dist.version)
@property
def raw_version(self) -> str:
return self._dist.version
def is_file(self, path: InfoPath) -> bool:
return self._dist.has_metadata(str(path))
@ -236,15 +215,16 @@ class Distribution(BaseDistribution):
return feed_parser.close()
def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
if extras:
relevant_extras = set(self._extra_mapping) & set(
map(canonicalize_name, extras)
)
extras = [self._extra_mapping[extra] for extra in relevant_extras]
if extras: # pkg_resources raises on invalid extras, so we sanitize.
extras = frozenset(pkg_resources.safe_extra(e) for e in extras)
extras = extras.intersection(self._dist.extras)
return self._dist.requires(extras)
def iter_provided_extras(self) -> Iterable[NormalizedName]:
return self._extra_mapping.keys()
def iter_provided_extras(self) -> Iterable[str]:
return self._dist.extras
def is_extra_provided(self, extra: str) -> bool:
return pkg_resources.safe_extra(extra) in self._dist.extras
class Environment(BaseEnvironment):
@ -256,14 +236,14 @@ class Environment(BaseEnvironment):
return cls(pkg_resources.working_set)
@classmethod
def from_paths(cls, paths: list[str] | None) -> BaseEnvironment:
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) -> BaseDistribution | None:
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
@ -275,7 +255,7 @@ class Environment(BaseEnvironment):
return dist
return None
def get_distribution(self, name: str) -> BaseDistribution | 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: