Voice et bot modif
This commit is contained in:
parent
189d56026b
commit
7333a22bcd
10774 changed files with 634644 additions and 933308 deletions
|
|
@ -1 +1,2 @@
|
|||
"""A package that contains models that represent entities."""
|
||||
"""A package that contains models that represent entities.
|
||||
"""
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,25 +1,30 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from pip._vendor.packaging.version import Version
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.utils.models import KeyBasedCompareMixin
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class InstallationCandidate:
|
||||
class InstallationCandidate(KeyBasedCompareMixin):
|
||||
"""Represents a potential "candidate" for installation."""
|
||||
|
||||
__slots__ = ["name", "version", "link"]
|
||||
|
||||
name: str
|
||||
version: Version
|
||||
link: Link
|
||||
|
||||
def __init__(self, name: str, version: str, link: Link) -> None:
|
||||
object.__setattr__(self, "name", name)
|
||||
object.__setattr__(self, "version", parse_version(version))
|
||||
object.__setattr__(self, "link", link)
|
||||
self.name = name
|
||||
self.version = parse_version(version)
|
||||
self.link = link
|
||||
|
||||
super().__init__(
|
||||
key=(self.name, self.version, self.link),
|
||||
defining_class=InstallationCandidate,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<InstallationCandidate({!r}, {!r}, {!r})>".format(
|
||||
self.name,
|
||||
self.version,
|
||||
self.link,
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name!r} candidate (version {self.version} at {self.link})"
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
"""PEP 610"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
""" PEP 610 """
|
||||
import json
|
||||
import re
|
||||
import urllib.parse
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, ClassVar, TypeVar, Union
|
||||
from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union
|
||||
|
||||
__all__ = [
|
||||
"DirectUrl",
|
||||
|
|
@ -28,8 +23,8 @@ class DirectUrlValidationError(Exception):
|
|||
|
||||
|
||||
def _get(
|
||||
d: dict[str, Any], expected_type: type[T], key: str, default: T | None = None
|
||||
) -> T | None:
|
||||
d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
|
||||
) -> Optional[T]:
|
||||
"""Get value from dictionary and verify expected type."""
|
||||
if key not in d:
|
||||
return default
|
||||
|
|
@ -42,7 +37,7 @@ def _get(
|
|||
|
||||
|
||||
def _get_required(
|
||||
d: dict[str, Any], expected_type: type[T], key: str, default: T | None = None
|
||||
d: Dict[str, Any], expected_type: Type[T], key: str, default: Optional[T] = None
|
||||
) -> T:
|
||||
value = _get(d, expected_type, key, default)
|
||||
if value is None:
|
||||
|
|
@ -50,7 +45,7 @@ def _get_required(
|
|||
return value
|
||||
|
||||
|
||||
def _exactly_one_of(infos: Iterable[InfoType | None]) -> InfoType:
|
||||
def _exactly_one_of(infos: Iterable[Optional["InfoType"]]) -> "InfoType":
|
||||
infos = [info for info in infos if info is not None]
|
||||
if not infos:
|
||||
raise DirectUrlValidationError(
|
||||
|
|
@ -64,21 +59,26 @@ def _exactly_one_of(infos: Iterable[InfoType | None]) -> InfoType:
|
|||
return infos[0]
|
||||
|
||||
|
||||
def _filter_none(**kwargs: Any) -> dict[str, Any]:
|
||||
def _filter_none(**kwargs: Any) -> Dict[str, Any]:
|
||||
"""Make dict excluding None values."""
|
||||
return {k: v for k, v in kwargs.items() if v is not None}
|
||||
|
||||
|
||||
@dataclass
|
||||
class VcsInfo:
|
||||
name: ClassVar = "vcs_info"
|
||||
name = "vcs_info"
|
||||
|
||||
vcs: str
|
||||
commit_id: str
|
||||
requested_revision: str | None = None
|
||||
def __init__(
|
||||
self,
|
||||
vcs: str,
|
||||
commit_id: str,
|
||||
requested_revision: Optional[str] = None,
|
||||
) -> None:
|
||||
self.vcs = vcs
|
||||
self.requested_revision = requested_revision
|
||||
self.commit_id = commit_id
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: dict[str, Any] | None) -> VcsInfo | None:
|
||||
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
|
||||
if d is None:
|
||||
return None
|
||||
return cls(
|
||||
|
|
@ -87,7 +87,7 @@ class VcsInfo:
|
|||
requested_revision=_get(d, str, "requested_revision"),
|
||||
)
|
||||
|
||||
def _to_dict(self) -> dict[str, Any]:
|
||||
def _to_dict(self) -> Dict[str, Any]:
|
||||
return _filter_none(
|
||||
vcs=self.vcs,
|
||||
requested_revision=self.requested_revision,
|
||||
|
|
@ -100,19 +100,19 @@ class ArchiveInfo:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
hash: str | None = None,
|
||||
hashes: dict[str, str] | None = None,
|
||||
hash: Optional[str] = None,
|
||||
hashes: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
# set hashes before hash, since the hash setter will further populate hashes
|
||||
self.hashes = hashes
|
||||
self.hash = hash
|
||||
|
||||
@property
|
||||
def hash(self) -> str | None:
|
||||
def hash(self) -> Optional[str]:
|
||||
return self._hash
|
||||
|
||||
@hash.setter
|
||||
def hash(self, value: str | None) -> None:
|
||||
def hash(self, value: Optional[str]) -> None:
|
||||
if value is not None:
|
||||
# Auto-populate the hashes key to upgrade to the new format automatically.
|
||||
# We don't back-populate the legacy hash key from hashes.
|
||||
|
|
@ -130,39 +130,47 @@ class ArchiveInfo:
|
|||
self._hash = value
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: dict[str, Any] | None) -> ArchiveInfo | None:
|
||||
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["ArchiveInfo"]:
|
||||
if d is None:
|
||||
return None
|
||||
return cls(hash=_get(d, str, "hash"), hashes=_get(d, dict, "hashes"))
|
||||
|
||||
def _to_dict(self) -> dict[str, Any]:
|
||||
def _to_dict(self) -> Dict[str, Any]:
|
||||
return _filter_none(hash=self.hash, hashes=self.hashes)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirInfo:
|
||||
name: ClassVar = "dir_info"
|
||||
name = "dir_info"
|
||||
|
||||
editable: bool = False
|
||||
def __init__(
|
||||
self,
|
||||
editable: bool = False,
|
||||
) -> None:
|
||||
self.editable = editable
|
||||
|
||||
@classmethod
|
||||
def _from_dict(cls, d: dict[str, Any] | None) -> DirInfo | None:
|
||||
def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["DirInfo"]:
|
||||
if d is None:
|
||||
return None
|
||||
return cls(editable=_get_required(d, bool, "editable", default=False))
|
||||
|
||||
def _to_dict(self) -> dict[str, Any]:
|
||||
def _to_dict(self) -> Dict[str, Any]:
|
||||
return _filter_none(editable=self.editable or None)
|
||||
|
||||
|
||||
InfoType = Union[ArchiveInfo, DirInfo, VcsInfo]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DirectUrl:
|
||||
url: str
|
||||
info: InfoType
|
||||
subdirectory: str | None = None
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
info: InfoType,
|
||||
subdirectory: Optional[str] = None,
|
||||
) -> None:
|
||||
self.url = url
|
||||
self.info = info
|
||||
self.subdirectory = subdirectory
|
||||
|
||||
def _remove_auth_from_netloc(self, netloc: str) -> str:
|
||||
if "@" not in netloc:
|
||||
|
|
@ -195,7 +203,7 @@ class DirectUrl:
|
|||
self.from_dict(self.to_dict())
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict[str, Any]) -> DirectUrl:
|
||||
def from_dict(cls, d: Dict[str, Any]) -> "DirectUrl":
|
||||
return DirectUrl(
|
||||
url=_get_required(d, str, "url"),
|
||||
subdirectory=_get(d, str, "subdirectory"),
|
||||
|
|
@ -208,7 +216,7 @@ class DirectUrl:
|
|||
),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
res = _filter_none(
|
||||
url=self.redacted_url,
|
||||
subdirectory=self.subdirectory,
|
||||
|
|
@ -217,7 +225,7 @@ class DirectUrl:
|
|||
return res
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, s: str) -> DirectUrl:
|
||||
def from_json(cls, s: str) -> "DirectUrl":
|
||||
return cls.from_dict(json.loads(s))
|
||||
|
||||
def to_json(self) -> str:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from __future__ import annotations
|
||||
from typing import FrozenSet, Optional, Set
|
||||
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
|
|
@ -12,8 +12,8 @@ class FormatControl:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
no_binary: set[str] | None = None,
|
||||
only_binary: set[str] | None = None,
|
||||
no_binary: Optional[Set[str]] = None,
|
||||
only_binary: Optional[Set[str]] = None,
|
||||
) -> None:
|
||||
if no_binary is None:
|
||||
no_binary = set()
|
||||
|
|
@ -36,7 +36,7 @@ class FormatControl:
|
|||
return f"{self.__class__.__name__}({self.no_binary}, {self.only_binary})"
|
||||
|
||||
@staticmethod
|
||||
def handle_mutual_excludes(value: str, target: set[str], other: set[str]) -> None:
|
||||
def handle_mutual_excludes(value: str, target: Set[str], other: Set[str]) -> None:
|
||||
if value.startswith("-"):
|
||||
raise CommandError(
|
||||
"--no-binary / --only-binary option requires 1 argument."
|
||||
|
|
@ -58,7 +58,7 @@ class FormatControl:
|
|||
other.discard(name)
|
||||
target.add(name)
|
||||
|
||||
def get_allowed_formats(self, canonical_name: str) -> frozenset[str]:
|
||||
def get_allowed_formats(self, canonical_name: str) -> FrozenSet[str]:
|
||||
result = {"binary", "source"}
|
||||
if canonical_name in self.only_binary:
|
||||
result.discard("source")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from collections.abc import Sequence
|
||||
from typing import Any
|
||||
from typing import Any, Dict, Sequence
|
||||
|
||||
from pip._vendor.packaging.markers import default_environment
|
||||
|
||||
|
|
@ -12,7 +11,7 @@ class InstallationReport:
|
|||
self._install_requirements = install_requirements
|
||||
|
||||
@classmethod
|
||||
def _install_req_to_dict(cls, ireq: InstallRequirement) -> dict[str, Any]:
|
||||
def _install_req_to_dict(cls, ireq: InstallRequirement) -> Dict[str, Any]:
|
||||
assert ireq.download_info, f"No download_info for {ireq}"
|
||||
res = {
|
||||
# PEP 610 json for the download URL. download_info.archive_info.hashes may
|
||||
|
|
@ -40,7 +39,7 @@ class InstallationReport:
|
|||
res["requested_extras"] = sorted(ireq.extras)
|
||||
return res
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"version": "1",
|
||||
"pip_version": __version__,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import logging
|
||||
|
|
@ -7,12 +5,17 @@ import os
|
|||
import posixpath
|
||||
import re
|
||||
import urllib.parse
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pip._internal.utils.deprecation import deprecated
|
||||
|
|
@ -24,6 +27,7 @@ from pip._internal.utils.misc import (
|
|||
split_auth_from_netloc,
|
||||
splitext,
|
||||
)
|
||||
from pip._internal.utils.models import KeyBasedCompareMixin
|
||||
from pip._internal.utils.urls import path_to_url, url_to_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -66,8 +70,8 @@ class LinkHash:
|
|||
assert self.name in _SUPPORTED_HASHES
|
||||
|
||||
@classmethod
|
||||
@functools.cache
|
||||
def find_hash_url_fragment(cls, url: str) -> LinkHash | None:
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def find_hash_url_fragment(cls, url: str) -> Optional["LinkHash"]:
|
||||
"""Search a string for a checksum algorithm name and encoded output value."""
|
||||
match = cls._hash_url_fragment_re.search(url)
|
||||
if match is None:
|
||||
|
|
@ -75,14 +79,14 @@ class LinkHash:
|
|||
name, value = match.groups()
|
||||
return cls(name=name, value=value)
|
||||
|
||||
def as_dict(self) -> dict[str, str]:
|
||||
def as_dict(self) -> Dict[str, str]:
|
||||
return {self.name: self.value}
|
||||
|
||||
def as_hashes(self) -> Hashes:
|
||||
"""Return a Hashes instance which checks only for the current hash."""
|
||||
return Hashes({self.name: [self.value]})
|
||||
|
||||
def is_hash_allowed(self, hashes: Hashes | None) -> bool:
|
||||
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
|
||||
"""
|
||||
Return True if the current hash is allowed by `hashes`.
|
||||
"""
|
||||
|
|
@ -95,14 +99,14 @@ class LinkHash:
|
|||
class MetadataFile:
|
||||
"""Information about a core metadata file associated with a distribution."""
|
||||
|
||||
hashes: dict[str, str] | None
|
||||
hashes: Optional[Dict[str, str]]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.hashes is not None:
|
||||
assert all(name in _SUPPORTED_HASHES for name in self.hashes)
|
||||
|
||||
|
||||
def supported_hashes(hashes: dict[str, str] | None) -> dict[str, str] | None:
|
||||
def supported_hashes(hashes: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
|
||||
# Remove any unsupported hash types from the mapping. If this leaves no
|
||||
# supported hashes, return None
|
||||
if hashes is None:
|
||||
|
|
@ -131,11 +135,7 @@ def _clean_file_url_path(part: str) -> str:
|
|||
# should not be quoted. On Linux where drive letters do not
|
||||
# exist, the colon should be quoted. We rely on urllib.request
|
||||
# to do the right thing here.
|
||||
ret = urllib.request.pathname2url(urllib.request.url2pathname(part))
|
||||
if ret.startswith("///"):
|
||||
# Remove any URL authority section, leaving only the URL path.
|
||||
ret = ret.removeprefix("//")
|
||||
return ret
|
||||
return urllib.request.pathname2url(urllib.request.url2pathname(part))
|
||||
|
||||
|
||||
# percent-encoded: /
|
||||
|
|
@ -171,37 +171,20 @@ def _ensure_quoted_url(url: str) -> str:
|
|||
and without double-quoting other characters.
|
||||
"""
|
||||
# Split the URL into parts according to the general structure
|
||||
# `scheme://netloc/path?query#fragment`.
|
||||
result = urllib.parse.urlsplit(url)
|
||||
# `scheme://netloc/path;parameters?query#fragment`.
|
||||
result = urllib.parse.urlparse(url)
|
||||
# If the netloc is empty, then the URL refers to a local filesystem path.
|
||||
is_local_path = not result.netloc
|
||||
path = _clean_url_path(result.path, is_local_path=is_local_path)
|
||||
# Temporarily replace scheme with file to ensure the URL generated by
|
||||
# urlunsplit() contains an empty netloc (file://) as per RFC 1738.
|
||||
ret = urllib.parse.urlunsplit(result._replace(scheme="file", path=path))
|
||||
ret = result.scheme + ret[4:] # Restore original scheme.
|
||||
return ret
|
||||
return urllib.parse.urlunparse(result._replace(path=path))
|
||||
|
||||
|
||||
def _absolute_link_url(base_url: str, url: str) -> str:
|
||||
"""
|
||||
A faster implementation of urllib.parse.urljoin with a shortcut
|
||||
for absolute http/https URLs.
|
||||
"""
|
||||
if url.startswith(("https://", "http://")):
|
||||
return url
|
||||
else:
|
||||
return urllib.parse.urljoin(base_url, url)
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class Link:
|
||||
class Link(KeyBasedCompareMixin):
|
||||
"""Represents a parsed link from a Package Index's simple URL"""
|
||||
|
||||
__slots__ = [
|
||||
"_parsed_url",
|
||||
"_url",
|
||||
"_path",
|
||||
"_hashes",
|
||||
"comes_from",
|
||||
"requires_python",
|
||||
|
|
@ -214,12 +197,12 @@ class Link:
|
|||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
comes_from: str | IndexContent | None = None,
|
||||
requires_python: str | None = None,
|
||||
yanked_reason: str | None = None,
|
||||
metadata_file_data: MetadataFile | None = None,
|
||||
comes_from: Optional[Union[str, "IndexContent"]] = None,
|
||||
requires_python: Optional[str] = None,
|
||||
yanked_reason: Optional[str] = None,
|
||||
metadata_file_data: Optional[MetadataFile] = None,
|
||||
cache_link_parsing: bool = True,
|
||||
hashes: Mapping[str, str] | None = None,
|
||||
hashes: Optional[Mapping[str, str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param url: url of the resource pointed to (href of the link)
|
||||
|
|
@ -258,8 +241,6 @@ class Link:
|
|||
# Store the url as a private attribute to prevent accidentally
|
||||
# trying to set a new value.
|
||||
self._url = url
|
||||
# The .path property is hot, so calculate its value ahead of time.
|
||||
self._path = urllib.parse.unquote(self._parsed_url.path)
|
||||
|
||||
link_hash = LinkHash.find_hash_url_fragment(url)
|
||||
hashes_from_link = {} if link_hash is None else link_hash.as_dict()
|
||||
|
|
@ -273,15 +254,17 @@ class Link:
|
|||
self.yanked_reason = yanked_reason
|
||||
self.metadata_file_data = metadata_file_data
|
||||
|
||||
super().__init__(key=url, defining_class=Link)
|
||||
|
||||
self.cache_link_parsing = cache_link_parsing
|
||||
self.egg_fragment = self._egg_fragment()
|
||||
|
||||
@classmethod
|
||||
def from_json(
|
||||
cls,
|
||||
file_data: dict[str, Any],
|
||||
file_data: Dict[str, Any],
|
||||
page_url: str,
|
||||
) -> Link | None:
|
||||
) -> Optional["Link"]:
|
||||
"""
|
||||
Convert an pypi json document from a simple repository page into a Link.
|
||||
"""
|
||||
|
|
@ -289,7 +272,7 @@ class Link:
|
|||
if file_url is None:
|
||||
return None
|
||||
|
||||
url = _ensure_quoted_url(_absolute_link_url(page_url, file_url))
|
||||
url = _ensure_quoted_url(urllib.parse.urljoin(page_url, file_url))
|
||||
pyrequire = file_data.get("requires-python")
|
||||
yanked_reason = file_data.get("yanked")
|
||||
hashes = file_data.get("hashes", {})
|
||||
|
|
@ -330,10 +313,10 @@ class Link:
|
|||
@classmethod
|
||||
def from_element(
|
||||
cls,
|
||||
anchor_attribs: dict[str, str | None],
|
||||
anchor_attribs: Dict[str, Optional[str]],
|
||||
page_url: str,
|
||||
base_url: str,
|
||||
) -> Link | None:
|
||||
) -> Optional["Link"]:
|
||||
"""
|
||||
Convert an anchor element's attributes in a simple repository page to a Link.
|
||||
"""
|
||||
|
|
@ -341,7 +324,7 @@ class Link:
|
|||
if not href:
|
||||
return None
|
||||
|
||||
url = _ensure_quoted_url(_absolute_link_url(base_url, href))
|
||||
url = _ensure_quoted_url(urllib.parse.urljoin(base_url, href))
|
||||
pyrequire = anchor_attribs.get("data-requires-python")
|
||||
yanked_reason = anchor_attribs.get("data-yanked")
|
||||
|
||||
|
|
@ -385,34 +368,17 @@ class Link:
|
|||
else:
|
||||
rp = ""
|
||||
if self.comes_from:
|
||||
return f"{self.redacted_url} (from {self.comes_from}){rp}"
|
||||
return f"{redact_auth_from_url(self._url)} (from {self.comes_from}){rp}"
|
||||
else:
|
||||
return self.redacted_url
|
||||
return redact_auth_from_url(str(self._url))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Link {self}>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.url)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if not isinstance(other, Link):
|
||||
return NotImplemented
|
||||
return self.url == other.url
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
if not isinstance(other, Link):
|
||||
return NotImplemented
|
||||
return self.url < other.url
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
return self._url
|
||||
|
||||
@property
|
||||
def redacted_url(self) -> str:
|
||||
return redact_auth_from_url(self.url)
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
path = self.path.rstrip("/")
|
||||
|
|
@ -444,9 +410,9 @@ class Link:
|
|||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
return self._path
|
||||
return urllib.parse.unquote(self._parsed_url.path)
|
||||
|
||||
def splitext(self) -> tuple[str, str]:
|
||||
def splitext(self) -> Tuple[str, str]:
|
||||
return splitext(posixpath.basename(self.path.rstrip("/")))
|
||||
|
||||
@property
|
||||
|
|
@ -465,7 +431,7 @@ class Link:
|
|||
r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE
|
||||
)
|
||||
|
||||
def _egg_fragment(self) -> str | None:
|
||||
def _egg_fragment(self) -> Optional[str]:
|
||||
match = self._egg_fragment_re.search(self._url)
|
||||
if not match:
|
||||
return None
|
||||
|
|
@ -475,10 +441,10 @@ class Link:
|
|||
project_name = match.group(1)
|
||||
if not self._project_name_re.match(project_name):
|
||||
deprecated(
|
||||
reason=f"{self} contains an egg fragment with a non-PEP 508 name.",
|
||||
reason=f"{self} contains an egg fragment with a non-PEP 508 name",
|
||||
replacement="to use the req @ url syntax, and remove the egg fragment",
|
||||
gone_in="26.0",
|
||||
issue=13157,
|
||||
gone_in="25.0",
|
||||
issue=11617,
|
||||
)
|
||||
|
||||
return project_name
|
||||
|
|
@ -486,13 +452,13 @@ class Link:
|
|||
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
|
||||
|
||||
@property
|
||||
def subdirectory_fragment(self) -> str | None:
|
||||
def subdirectory_fragment(self) -> Optional[str]:
|
||||
match = self._subdirectory_fragment_re.search(self._url)
|
||||
if not match:
|
||||
return None
|
||||
return match.group(1)
|
||||
|
||||
def metadata_link(self) -> Link | None:
|
||||
def metadata_link(self) -> Optional["Link"]:
|
||||
"""Return a link to the associated core metadata file (if any)."""
|
||||
if self.metadata_file_data is None:
|
||||
return None
|
||||
|
|
@ -505,11 +471,11 @@ class Link:
|
|||
return Hashes({k: [v] for k, v in self._hashes.items()})
|
||||
|
||||
@property
|
||||
def hash(self) -> str | None:
|
||||
def hash(self) -> Optional[str]:
|
||||
return next(iter(self._hashes.values()), None)
|
||||
|
||||
@property
|
||||
def hash_name(self) -> str | None:
|
||||
def hash_name(self) -> Optional[str]:
|
||||
return next(iter(self._hashes), None)
|
||||
|
||||
@property
|
||||
|
|
@ -541,7 +507,7 @@ class Link:
|
|||
def has_hash(self) -> bool:
|
||||
return bool(self._hashes)
|
||||
|
||||
def is_hash_allowed(self, hashes: Hashes | None) -> bool:
|
||||
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
|
||||
"""
|
||||
Return True if the link has a hash and it is allowed by `hashes`.
|
||||
"""
|
||||
|
|
@ -577,9 +543,9 @@ class _CleanResult(NamedTuple):
|
|||
"""
|
||||
|
||||
parsed: urllib.parse.SplitResult
|
||||
query: dict[str, list[str]]
|
||||
query: Dict[str, List[str]]
|
||||
subdirectory: str
|
||||
hashes: dict[str, str]
|
||||
hashes: Dict[str, str]
|
||||
|
||||
|
||||
def _clean_link(link: Link) -> _CleanResult:
|
||||
|
|
@ -608,6 +574,6 @@ def _clean_link(link: Link) -> _CleanResult:
|
|||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def links_equivalent(link1: Link, link2: Link) -> bool:
|
||||
return _clean_link(link1) == _clean_link(link2)
|
||||
|
|
|
|||
|
|
@ -1,188 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pip._vendor import tomli_w
|
||||
|
||||
from pip._internal.models.direct_url import ArchiveInfo, DirInfo, VcsInfo
|
||||
from pip._internal.models.link import Link
|
||||
from pip._internal.req.req_install import InstallRequirement
|
||||
from pip._internal.utils.urls import url_to_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Self
|
||||
|
||||
PYLOCK_FILE_NAME_RE = re.compile(r"^pylock\.([^.]+)\.toml$")
|
||||
|
||||
|
||||
def is_valid_pylock_file_name(path: Path) -> bool:
|
||||
return path.name == "pylock.toml" or bool(re.match(PYLOCK_FILE_NAME_RE, path.name))
|
||||
|
||||
|
||||
def _toml_dict_factory(data: list[tuple[str, Any]]) -> dict[str, Any]:
|
||||
return {key.replace("_", "-"): value for key, value in data if value is not None}
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageVcs:
|
||||
type: str
|
||||
url: str | None
|
||||
# (not supported) path: Optional[str]
|
||||
requested_revision: str | None
|
||||
commit_id: str
|
||||
subdirectory: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageDirectory:
|
||||
path: str
|
||||
editable: bool | None
|
||||
subdirectory: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageArchive:
|
||||
url: str | None
|
||||
# (not supported) path: Optional[str]
|
||||
# (not supported) size: Optional[int]
|
||||
# (not supported) upload_time: Optional[datetime]
|
||||
hashes: dict[str, str]
|
||||
subdirectory: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageSdist:
|
||||
name: str
|
||||
# (not supported) upload_time: Optional[datetime]
|
||||
url: str | None
|
||||
# (not supported) path: Optional[str]
|
||||
# (not supported) size: Optional[int]
|
||||
hashes: dict[str, str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageWheel:
|
||||
name: str
|
||||
# (not supported) upload_time: Optional[datetime]
|
||||
url: str | None
|
||||
# (not supported) path: Optional[str]
|
||||
# (not supported) size: Optional[int]
|
||||
hashes: dict[str, str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Package:
|
||||
name: str
|
||||
version: str | None = None
|
||||
# (not supported) marker: Optional[str]
|
||||
# (not supported) requires_python: Optional[str]
|
||||
# (not supported) dependencies
|
||||
vcs: PackageVcs | None = None
|
||||
directory: PackageDirectory | None = None
|
||||
archive: PackageArchive | None = None
|
||||
# (not supported) index: Optional[str]
|
||||
sdist: PackageSdist | None = None
|
||||
wheels: list[PackageWheel] | None = None
|
||||
# (not supported) attestation_identities: Optional[List[Dict[str, Any]]]
|
||||
# (not supported) tool: Optional[Dict[str, Any]]
|
||||
|
||||
@classmethod
|
||||
def from_install_requirement(cls, ireq: InstallRequirement, base_dir: Path) -> Self:
|
||||
base_dir = base_dir.resolve()
|
||||
dist = ireq.get_dist()
|
||||
download_info = ireq.download_info
|
||||
assert download_info
|
||||
package = cls(name=dist.canonical_name)
|
||||
if ireq.is_direct:
|
||||
if isinstance(download_info.info, VcsInfo):
|
||||
package.vcs = PackageVcs(
|
||||
type=download_info.info.vcs,
|
||||
url=download_info.url,
|
||||
requested_revision=download_info.info.requested_revision,
|
||||
commit_id=download_info.info.commit_id,
|
||||
subdirectory=download_info.subdirectory,
|
||||
)
|
||||
elif isinstance(download_info.info, DirInfo):
|
||||
package.directory = PackageDirectory(
|
||||
path=(
|
||||
Path(url_to_path(download_info.url))
|
||||
.resolve()
|
||||
.relative_to(base_dir)
|
||||
.as_posix()
|
||||
),
|
||||
editable=(
|
||||
download_info.info.editable
|
||||
if download_info.info.editable
|
||||
else None
|
||||
),
|
||||
subdirectory=download_info.subdirectory,
|
||||
)
|
||||
elif isinstance(download_info.info, ArchiveInfo):
|
||||
if not download_info.info.hashes:
|
||||
raise NotImplementedError()
|
||||
package.archive = PackageArchive(
|
||||
url=download_info.url,
|
||||
hashes=download_info.info.hashes,
|
||||
subdirectory=download_info.subdirectory,
|
||||
)
|
||||
else:
|
||||
# should never happen
|
||||
raise NotImplementedError()
|
||||
else:
|
||||
package.version = str(dist.version)
|
||||
if isinstance(download_info.info, ArchiveInfo):
|
||||
if not download_info.info.hashes:
|
||||
raise NotImplementedError()
|
||||
link = Link(download_info.url)
|
||||
if link.is_wheel:
|
||||
package.wheels = [
|
||||
PackageWheel(
|
||||
name=link.filename,
|
||||
url=download_info.url,
|
||||
hashes=download_info.info.hashes,
|
||||
)
|
||||
]
|
||||
else:
|
||||
package.sdist = PackageSdist(
|
||||
name=link.filename,
|
||||
url=download_info.url,
|
||||
hashes=download_info.info.hashes,
|
||||
)
|
||||
else:
|
||||
# should never happen
|
||||
raise NotImplementedError()
|
||||
return package
|
||||
|
||||
|
||||
@dataclass
|
||||
class Pylock:
|
||||
lock_version: str = "1.0"
|
||||
# (not supported) environments: Optional[List[str]]
|
||||
# (not supported) requires_python: Optional[str]
|
||||
# (not supported) extras: List[str] = []
|
||||
# (not supported) dependency_groups: List[str] = []
|
||||
created_by: str = "pip"
|
||||
packages: list[Package] = dataclasses.field(default_factory=list)
|
||||
# (not supported) tool: Optional[Dict[str, Any]]
|
||||
|
||||
def as_toml(self) -> str:
|
||||
return tomli_w.dumps(dataclasses.asdict(self, dict_factory=_toml_dict_factory))
|
||||
|
||||
@classmethod
|
||||
def from_install_requirements(
|
||||
cls, install_requirements: Iterable[InstallRequirement], base_dir: Path
|
||||
) -> Self:
|
||||
return cls(
|
||||
packages=sorted(
|
||||
(
|
||||
Package.from_install_requirement(ireq, base_dir)
|
||||
for ireq in install_requirements
|
||||
),
|
||||
key=lambda p: p.name,
|
||||
)
|
||||
)
|
||||
|
|
@ -5,12 +5,10 @@ For a general overview of available schemes and their context, see
|
|||
https://docs.python.org/3/install/index.html#alternate-installation.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
SCHEME_KEYS = ["platlib", "purelib", "headers", "scripts", "data"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Scheme:
|
||||
"""A Scheme holds paths which are used as the base directories for
|
||||
artifacts associated with a Python package.
|
||||
|
|
@ -18,8 +16,16 @@ class Scheme:
|
|||
|
||||
__slots__ = SCHEME_KEYS
|
||||
|
||||
platlib: str
|
||||
purelib: str
|
||||
headers: str
|
||||
scripts: str
|
||||
data: str
|
||||
def __init__(
|
||||
self,
|
||||
platlib: str,
|
||||
purelib: str,
|
||||
headers: str,
|
||||
scripts: str,
|
||||
data: str,
|
||||
) -> None:
|
||||
self.platlib = platlib
|
||||
self.purelib = purelib
|
||||
self.headers = headers
|
||||
self.scripts = scripts
|
||||
self.data = data
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import os
|
||||
import posixpath
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
|
||||
|
|
@ -14,23 +14,19 @@ from pip._internal.utils.misc import normalize_path, redact_auth_from_url
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SearchScope:
|
||||
|
||||
"""
|
||||
Encapsulates the locations that pip is configured to search.
|
||||
"""
|
||||
|
||||
__slots__ = ["find_links", "index_urls", "no_index"]
|
||||
|
||||
find_links: list[str]
|
||||
index_urls: list[str]
|
||||
no_index: bool
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
find_links: list[str],
|
||||
index_urls: list[str],
|
||||
find_links: List[str],
|
||||
index_urls: List[str],
|
||||
no_index: bool,
|
||||
) -> "SearchScope":
|
||||
"""
|
||||
|
|
@ -41,7 +37,7 @@ class SearchScope:
|
|||
# it and if it exists, use the normalized version.
|
||||
# This is deliberately conservative - it might be fine just to
|
||||
# blindly normalize anything starting with a ~...
|
||||
built_find_links: list[str] = []
|
||||
built_find_links: List[str] = []
|
||||
for link in find_links:
|
||||
if link.startswith("~"):
|
||||
new_link = normalize_path(link)
|
||||
|
|
@ -68,6 +64,16 @@ class SearchScope:
|
|||
no_index=no_index,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
find_links: List[str],
|
||||
index_urls: List[str],
|
||||
no_index: bool,
|
||||
) -> None:
|
||||
self.find_links = find_links
|
||||
self.index_urls = index_urls
|
||||
self.no_index = no_index
|
||||
|
||||
def get_formatted_locations(self) -> str:
|
||||
lines = []
|
||||
redacted_index_urls = []
|
||||
|
|
@ -103,7 +109,7 @@ class SearchScope:
|
|||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_index_urls_locations(self, project_name: str) -> list[str]:
|
||||
def get_index_urls_locations(self, project_name: str) -> List[str]:
|
||||
"""Returns the locations found via self.index_urls
|
||||
|
||||
Checks the url_name on the main (first in the list) index and
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
from pip._internal.models.format_control import FormatControl
|
||||
|
||||
|
||||
# TODO: This needs Python 3.10's improved slots support for dataclasses
|
||||
# to be converted into a dataclass.
|
||||
class SelectionPreferences:
|
||||
"""
|
||||
Encapsulates the candidate selection preferences for downloading
|
||||
|
|
@ -27,9 +25,9 @@ class SelectionPreferences:
|
|||
self,
|
||||
allow_yanked: bool,
|
||||
allow_all_prereleases: bool = False,
|
||||
format_control: FormatControl | None = None,
|
||||
format_control: Optional[FormatControl] = None,
|
||||
prefer_binary: bool = False,
|
||||
ignore_requires_python: bool | None = None,
|
||||
ignore_requires_python: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""Create a SelectionPreferences object.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import List, Optional, Set, Tuple
|
||||
|
||||
from pip._vendor.packaging.tags import Tag
|
||||
|
||||
|
|
@ -9,6 +8,7 @@ from pip._internal.utils.misc import normalize_version_info
|
|||
|
||||
|
||||
class TargetPython:
|
||||
|
||||
"""
|
||||
Encapsulates the properties of a Python interpreter one is targeting
|
||||
for a package install, download, etc.
|
||||
|
|
@ -27,10 +27,10 @@ class TargetPython:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
platforms: list[str] | None = None,
|
||||
py_version_info: tuple[int, ...] | None = None,
|
||||
abis: list[str] | None = None,
|
||||
implementation: str | None = None,
|
||||
platforms: Optional[List[str]] = None,
|
||||
py_version_info: Optional[Tuple[int, ...]] = None,
|
||||
abis: Optional[List[str]] = None,
|
||||
implementation: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param platforms: A list of strings or None. If None, searches for
|
||||
|
|
@ -63,8 +63,8 @@ class TargetPython:
|
|||
self.py_version_info = py_version_info
|
||||
|
||||
# This is used to cache the return value of get_(un)sorted_tags.
|
||||
self._valid_tags: list[Tag] | None = None
|
||||
self._valid_tags_set: set[Tag] | None = None
|
||||
self._valid_tags: Optional[List[Tag]] = None
|
||||
self._valid_tags_set: Optional[Set[Tag]] = None
|
||||
|
||||
def format_given(self) -> str:
|
||||
"""
|
||||
|
|
@ -86,7 +86,7 @@ class TargetPython:
|
|||
f"{key}={value!r}" for key, value in key_values if value is not None
|
||||
)
|
||||
|
||||
def get_sorted_tags(self) -> list[Tag]:
|
||||
def get_sorted_tags(self) -> List[Tag]:
|
||||
"""
|
||||
Return the supported PEP 425 tags to check wheel candidates against.
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ class TargetPython:
|
|||
|
||||
return self._valid_tags
|
||||
|
||||
def get_unsorted_tags(self) -> set[Tag]:
|
||||
def get_unsorted_tags(self) -> Set[Tag]:
|
||||
"""Exactly the same as get_sorted_tags, but returns a set.
|
||||
|
||||
This is important for performance.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,10 @@
|
|||
"""Represents a wheel file and provides access to the various parts of the
|
||||
name that have meaning.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
import re
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
from pip._vendor.packaging.tags import Tag
|
||||
from pip._vendor.packaging.utils import (
|
||||
InvalidWheelFilename as _PackagingInvalidWheelFilename,
|
||||
)
|
||||
from pip._vendor.packaging.utils import parse_wheel_filename
|
||||
|
||||
from pip._internal.exceptions import InvalidWheelFilename
|
||||
|
||||
|
|
@ -18,22 +12,40 @@ from pip._internal.exceptions import InvalidWheelFilename
|
|||
class Wheel:
|
||||
"""A wheel file"""
|
||||
|
||||
wheel_file_re = re.compile(
|
||||
r"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?))
|
||||
((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\s-]+?)
|
||||
\.whl|\.dist-info)$""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
def __init__(self, filename: str) -> None:
|
||||
"""
|
||||
:raises InvalidWheelFilename: when the filename is invalid for a wheel
|
||||
"""
|
||||
wheel_info = self.wheel_file_re.match(filename)
|
||||
if not wheel_info:
|
||||
raise InvalidWheelFilename(f"{filename} is not a valid wheel filename.")
|
||||
self.filename = filename
|
||||
self.name = wheel_info.group("name").replace("_", "-")
|
||||
# we'll assume "_" means "-" due to wheel naming scheme
|
||||
# (https://github.com/pypa/pip/issues/1150)
|
||||
self.version = wheel_info.group("ver").replace("_", "-")
|
||||
self.build_tag = wheel_info.group("build")
|
||||
self.pyversions = wheel_info.group("pyver").split(".")
|
||||
self.abis = wheel_info.group("abi").split(".")
|
||||
self.plats = wheel_info.group("plat").split(".")
|
||||
|
||||
try:
|
||||
wheel_info = parse_wheel_filename(filename)
|
||||
except _PackagingInvalidWheelFilename as e:
|
||||
raise InvalidWheelFilename(e.args[0]) from None
|
||||
# All the tag combinations from this file
|
||||
self.file_tags = {
|
||||
Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats
|
||||
}
|
||||
|
||||
self.name, _version, self.build_tag, self.file_tags = wheel_info
|
||||
self.version = str(_version)
|
||||
|
||||
def get_formatted_file_tags(self) -> list[str]:
|
||||
def get_formatted_file_tags(self) -> List[str]:
|
||||
"""Return the wheel's tags as a sorted list of strings."""
|
||||
return sorted(str(tag) for tag in self.file_tags)
|
||||
|
||||
def support_index_min(self, tags: list[Tag]) -> int:
|
||||
def support_index_min(self, tags: List[Tag]) -> int:
|
||||
"""Return the lowest index that one of the wheel's file_tag combinations
|
||||
achieves in the given list of supported tags.
|
||||
|
||||
|
|
@ -52,7 +64,7 @@ class Wheel:
|
|||
raise ValueError()
|
||||
|
||||
def find_most_preferred_tag(
|
||||
self, tags: list[Tag], tag_to_priority: dict[Tag, int]
|
||||
self, tags: List[Tag], tag_to_priority: Dict[Tag, int]
|
||||
) -> int:
|
||||
"""Return the priority of the most preferred tag that one of the wheel's file
|
||||
tag combinations achieves in the given list of supported tags using the given
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue