Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": ""}):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue