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 @@
|
|||
"""Index interaction code"""
|
||||
"""Index interaction code
|
||||
"""
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2,8 +2,6 @@
|
|||
The main purpose of this module is to expose LinkCollector.collect_sources().
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import email.message
|
||||
import functools
|
||||
|
|
@ -13,14 +11,20 @@ import logging
|
|||
import os
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections.abc import Iterable, MutableMapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from html.parser import HTMLParser
|
||||
from optparse import Values
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
MutableMapping,
|
||||
NamedTuple,
|
||||
Protocol,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pip._vendor import requests
|
||||
|
|
@ -38,12 +42,17 @@ from pip._internal.vcs import vcs
|
|||
|
||||
from .sources import CandidatesFromPage, LinkSource, build_source
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Protocol
|
||||
else:
|
||||
Protocol = object
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ResponseHeaders = MutableMapping[str, str]
|
||||
|
||||
|
||||
def _match_vcs_scheme(url: str) -> str | None:
|
||||
def _match_vcs_scheme(url: str) -> Optional[str]:
|
||||
"""Look for VCS schemes in the URL.
|
||||
|
||||
Returns the matched VCS scheme, or None if there's no match.
|
||||
|
|
@ -168,7 +177,7 @@ def _get_simple_response(url: str, session: PipSession) -> Response:
|
|||
return resp
|
||||
|
||||
|
||||
def _get_encoding_from_headers(headers: ResponseHeaders) -> str | None:
|
||||
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
|
||||
"""Determine if we have any encoding information in our headers."""
|
||||
if headers and "Content-Type" in headers:
|
||||
m = email.message.Message()
|
||||
|
|
@ -180,7 +189,7 @@ def _get_encoding_from_headers(headers: ResponseHeaders) -> str | None:
|
|||
|
||||
|
||||
class CacheablePageContent:
|
||||
def __init__(self, page: IndexContent) -> None:
|
||||
def __init__(self, page: "IndexContent") -> None:
|
||||
assert page.cache_link_parsing
|
||||
self.page = page
|
||||
|
||||
|
|
@ -192,7 +201,8 @@ class CacheablePageContent:
|
|||
|
||||
|
||||
class ParseLinks(Protocol):
|
||||
def __call__(self, page: IndexContent) -> Iterable[Link]: ...
|
||||
def __call__(self, page: "IndexContent") -> Iterable[Link]:
|
||||
...
|
||||
|
||||
|
||||
def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
|
||||
|
|
@ -202,12 +212,12 @@ def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
|
|||
`page` has `page.cache_link_parsing == False`.
|
||||
"""
|
||||
|
||||
@functools.cache
|
||||
def wrapper(cacheable_page: CacheablePageContent) -> list[Link]:
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def wrapper(cacheable_page: CacheablePageContent) -> List[Link]:
|
||||
return list(fn(cacheable_page.page))
|
||||
|
||||
@functools.wraps(fn)
|
||||
def wrapper_wrapper(page: IndexContent) -> list[Link]:
|
||||
def wrapper_wrapper(page: "IndexContent") -> List[Link]:
|
||||
if page.cache_link_parsing:
|
||||
return wrapper(CacheablePageContent(page))
|
||||
return list(fn(page))
|
||||
|
|
@ -216,7 +226,7 @@ def with_cached_index_content(fn: ParseLinks) -> ParseLinks:
|
|||
|
||||
|
||||
@with_cached_index_content
|
||||
def parse_links(page: IndexContent) -> Iterable[Link]:
|
||||
def parse_links(page: "IndexContent") -> Iterable[Link]:
|
||||
"""
|
||||
Parse a Simple API's Index Content, and yield its anchor elements as Link objects.
|
||||
"""
|
||||
|
|
@ -244,22 +254,29 @@ def parse_links(page: IndexContent) -> Iterable[Link]:
|
|||
yield link
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IndexContent:
|
||||
"""Represents one response (or page), along with its URL.
|
||||
"""Represents one response (or page), along with its URL"""
|
||||
|
||||
:param encoding: the encoding to decode the given content.
|
||||
:param url: the URL from which the HTML was downloaded.
|
||||
:param cache_link_parsing: whether links parsed from this page's url
|
||||
should be cached. PyPI index urls should
|
||||
have this set to False, for example.
|
||||
"""
|
||||
|
||||
content: bytes
|
||||
content_type: str
|
||||
encoding: str | None
|
||||
url: str
|
||||
cache_link_parsing: bool = True
|
||||
def __init__(
|
||||
self,
|
||||
content: bytes,
|
||||
content_type: str,
|
||||
encoding: Optional[str],
|
||||
url: str,
|
||||
cache_link_parsing: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
:param encoding: the encoding to decode the given content.
|
||||
:param url: the URL from which the HTML was downloaded.
|
||||
:param cache_link_parsing: whether links parsed from this page's url
|
||||
should be cached. PyPI index urls should
|
||||
have this set to False, for example.
|
||||
"""
|
||||
self.content = content
|
||||
self.content_type = content_type
|
||||
self.encoding = encoding
|
||||
self.url = url
|
||||
self.cache_link_parsing = cache_link_parsing
|
||||
|
||||
def __str__(self) -> str:
|
||||
return redact_auth_from_url(self.url)
|
||||
|
|
@ -275,10 +292,10 @@ class HTMLLinkParser(HTMLParser):
|
|||
super().__init__(convert_charrefs=True)
|
||||
|
||||
self.url: str = url
|
||||
self.base_url: str | None = None
|
||||
self.anchors: list[dict[str, str | None]] = []
|
||||
self.base_url: Optional[str] = None
|
||||
self.anchors: List[Dict[str, Optional[str]]] = []
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
|
||||
if tag == "base" and self.base_url is None:
|
||||
href = self.get_href(attrs)
|
||||
if href is not None:
|
||||
|
|
@ -286,7 +303,7 @@ class HTMLLinkParser(HTMLParser):
|
|||
elif tag == "a":
|
||||
self.anchors.append(dict(attrs))
|
||||
|
||||
def get_href(self, attrs: list[tuple[str, str | None]]) -> str | None:
|
||||
def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
|
||||
for name, value in attrs:
|
||||
if name == "href":
|
||||
return value
|
||||
|
|
@ -295,8 +312,8 @@ class HTMLLinkParser(HTMLParser):
|
|||
|
||||
def _handle_get_simple_fail(
|
||||
link: Link,
|
||||
reason: str | Exception,
|
||||
meth: Callable[..., None] | None = None,
|
||||
reason: Union[str, Exception],
|
||||
meth: Optional[Callable[..., None]] = None,
|
||||
) -> None:
|
||||
if meth is None:
|
||||
meth = logger.debug
|
||||
|
|
@ -316,7 +333,7 @@ def _make_index_content(
|
|||
)
|
||||
|
||||
|
||||
def _get_index_content(link: Link, *, session: PipSession) -> IndexContent | None:
|
||||
def _get_index_content(link: Link, *, session: PipSession) -> Optional["IndexContent"]:
|
||||
url = link.url.split("#", 1)[0]
|
||||
|
||||
# Check for VCS schemes that do not support lookup as web pages.
|
||||
|
|
@ -378,11 +395,12 @@ def _get_index_content(link: Link, *, session: PipSession) -> IndexContent | Non
|
|||
|
||||
|
||||
class CollectedSources(NamedTuple):
|
||||
find_links: Sequence[LinkSource | None]
|
||||
index_urls: Sequence[LinkSource | None]
|
||||
find_links: Sequence[Optional[LinkSource]]
|
||||
index_urls: Sequence[Optional[LinkSource]]
|
||||
|
||||
|
||||
class LinkCollector:
|
||||
|
||||
"""
|
||||
Responsible for collecting Link objects from all configured locations,
|
||||
making network requests as needed.
|
||||
|
|
@ -404,7 +422,7 @@ class LinkCollector:
|
|||
session: PipSession,
|
||||
options: Values,
|
||||
suppress_no_index: bool = False,
|
||||
) -> LinkCollector:
|
||||
) -> "LinkCollector":
|
||||
"""
|
||||
:param session: The Session to use to make requests.
|
||||
:param suppress_no_index: Whether to ignore the --no-index option
|
||||
|
|
@ -433,10 +451,10 @@ class LinkCollector:
|
|||
return link_collector
|
||||
|
||||
@property
|
||||
def find_links(self) -> list[str]:
|
||||
def find_links(self) -> List[str]:
|
||||
return self.search_scope.find_links
|
||||
|
||||
def fetch_response(self, location: Link) -> IndexContent | None:
|
||||
def fetch_response(self, location: Link) -> Optional[IndexContent]:
|
||||
"""
|
||||
Fetch an HTML page containing package links.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,24 +1,16 @@
|
|||
"""Routines related to PyPI, indexes"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
import itertools
|
||||
import logging
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
|
||||
|
||||
from pip._vendor.packaging import specifiers
|
||||
from pip._vendor.packaging.tags import Tag
|
||||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
from pip._vendor.packaging.version import InvalidVersion, _BaseVersion
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.packaging.version import _BaseVersion
|
||||
from pip._vendor.packaging.version import parse as parse_version
|
||||
|
||||
from pip._internal.exceptions import (
|
||||
|
|
@ -45,20 +37,20 @@ from pip._internal.utils.packaging import check_requires_python
|
|||
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeGuard
|
||||
from pip._vendor.typing_extensions import TypeGuard
|
||||
|
||||
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
|
||||
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
BuildTag = Union[tuple[()], tuple[int, str]]
|
||||
CandidateSortingKey = tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
|
||||
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
||||
CandidateSortingKey = Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag]
|
||||
|
||||
|
||||
def _check_link_requires_python(
|
||||
link: Link,
|
||||
version_info: tuple[int, int, int],
|
||||
version_info: Tuple[int, int, int],
|
||||
ignore_requires_python: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
|
|
@ -114,6 +106,7 @@ class LinkType(enum.Enum):
|
|||
|
||||
|
||||
class LinkEvaluator:
|
||||
|
||||
"""
|
||||
Responsible for evaluating links for a particular project.
|
||||
"""
|
||||
|
|
@ -127,11 +120,11 @@ class LinkEvaluator:
|
|||
def __init__(
|
||||
self,
|
||||
project_name: str,
|
||||
canonical_name: NormalizedName,
|
||||
formats: frozenset[str],
|
||||
canonical_name: str,
|
||||
formats: FrozenSet[str],
|
||||
target_python: TargetPython,
|
||||
allow_yanked: bool,
|
||||
ignore_requires_python: bool | None = None,
|
||||
ignore_requires_python: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param project_name: The user supplied package name.
|
||||
|
|
@ -161,7 +154,7 @@ class LinkEvaluator:
|
|||
|
||||
self.project_name = project_name
|
||||
|
||||
def evaluate_link(self, link: Link) -> tuple[LinkType, str]:
|
||||
def evaluate_link(self, link: Link) -> Tuple[LinkType, str]:
|
||||
"""
|
||||
Determine whether a link is a candidate for installation.
|
||||
|
||||
|
|
@ -201,7 +194,7 @@ class LinkEvaluator:
|
|||
LinkType.format_invalid,
|
||||
"invalid wheel filename",
|
||||
)
|
||||
if wheel.name != self._canonical_name:
|
||||
if canonicalize_name(wheel.name) != self._canonical_name:
|
||||
reason = f"wrong project name (not {self.project_name})"
|
||||
return (LinkType.different_project, reason)
|
||||
|
||||
|
|
@ -248,19 +241,7 @@ class LinkEvaluator:
|
|||
ignore_requires_python=self._ignore_requires_python,
|
||||
)
|
||||
if not supports_python:
|
||||
requires_python = link.requires_python
|
||||
if requires_python:
|
||||
|
||||
def get_version_sort_key(v: str) -> tuple[int, ...]:
|
||||
return tuple(int(s) for s in v.split(".") if s.isdigit())
|
||||
|
||||
requires_python = ",".join(
|
||||
sorted(
|
||||
(str(s) for s in specifiers.SpecifierSet(requires_python)),
|
||||
key=get_version_sort_key,
|
||||
)
|
||||
)
|
||||
reason = f"{version} Requires-Python {requires_python}"
|
||||
reason = f"{version} Requires-Python {link.requires_python}"
|
||||
return (LinkType.requires_python_mismatch, reason)
|
||||
|
||||
logger.debug("Found link %s, version: %s", link, version)
|
||||
|
|
@ -269,10 +250,10 @@ class LinkEvaluator:
|
|||
|
||||
|
||||
def filter_unallowed_hashes(
|
||||
candidates: list[InstallationCandidate],
|
||||
hashes: Hashes | None,
|
||||
candidates: List[InstallationCandidate],
|
||||
hashes: Optional[Hashes],
|
||||
project_name: str,
|
||||
) -> list[InstallationCandidate]:
|
||||
) -> List[InstallationCandidate]:
|
||||
"""
|
||||
Filter out candidates whose hashes aren't allowed, and return a new
|
||||
list of candidates.
|
||||
|
|
@ -342,44 +323,67 @@ def filter_unallowed_hashes(
|
|||
return filtered
|
||||
|
||||
|
||||
@dataclass
|
||||
class CandidatePreferences:
|
||||
|
||||
"""
|
||||
Encapsulates some of the preferences for filtering and sorting
|
||||
InstallationCandidate objects.
|
||||
"""
|
||||
|
||||
prefer_binary: bool = False
|
||||
allow_all_prereleases: bool = False
|
||||
def __init__(
|
||||
self,
|
||||
prefer_binary: bool = False,
|
||||
allow_all_prereleases: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
:param allow_all_prereleases: Whether to allow all pre-releases.
|
||||
"""
|
||||
self.allow_all_prereleases = allow_all_prereleases
|
||||
self.prefer_binary = prefer_binary
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BestCandidateResult:
|
||||
"""A collection of candidates, returned by `PackageFinder.find_best_candidate`.
|
||||
|
||||
This class is only intended to be instantiated by CandidateEvaluator's
|
||||
`compute_best_candidate()` method.
|
||||
|
||||
:param all_candidates: A sequence of all available candidates found.
|
||||
:param applicable_candidates: The applicable candidates.
|
||||
:param best_candidate: The most preferred candidate found, or None
|
||||
if no applicable candidates were found.
|
||||
"""
|
||||
|
||||
all_candidates: list[InstallationCandidate]
|
||||
applicable_candidates: list[InstallationCandidate]
|
||||
best_candidate: InstallationCandidate | None
|
||||
def __init__(
|
||||
self,
|
||||
candidates: List[InstallationCandidate],
|
||||
applicable_candidates: List[InstallationCandidate],
|
||||
best_candidate: Optional[InstallationCandidate],
|
||||
) -> None:
|
||||
"""
|
||||
:param candidates: A sequence of all available candidates found.
|
||||
:param applicable_candidates: The applicable candidates.
|
||||
:param best_candidate: The most preferred candidate found, or None
|
||||
if no applicable candidates were found.
|
||||
"""
|
||||
assert set(applicable_candidates) <= set(candidates)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
assert set(self.applicable_candidates) <= set(self.all_candidates)
|
||||
|
||||
if self.best_candidate is None:
|
||||
assert not self.applicable_candidates
|
||||
if best_candidate is None:
|
||||
assert not applicable_candidates
|
||||
else:
|
||||
assert self.best_candidate in self.applicable_candidates
|
||||
assert best_candidate in applicable_candidates
|
||||
|
||||
self._applicable_candidates = applicable_candidates
|
||||
self._candidates = candidates
|
||||
|
||||
self.best_candidate = best_candidate
|
||||
|
||||
def iter_all(self) -> Iterable[InstallationCandidate]:
|
||||
"""Iterate through all candidates."""
|
||||
return iter(self._candidates)
|
||||
|
||||
def iter_applicable(self) -> Iterable[InstallationCandidate]:
|
||||
"""Iterate through the applicable candidates."""
|
||||
return iter(self._applicable_candidates)
|
||||
|
||||
|
||||
class CandidateEvaluator:
|
||||
|
||||
"""
|
||||
Responsible for filtering and sorting candidates for installation based
|
||||
on what tags are valid.
|
||||
|
|
@ -389,12 +393,12 @@ class CandidateEvaluator:
|
|||
def create(
|
||||
cls,
|
||||
project_name: str,
|
||||
target_python: TargetPython | None = None,
|
||||
target_python: Optional[TargetPython] = None,
|
||||
prefer_binary: bool = False,
|
||||
allow_all_prereleases: bool = False,
|
||||
specifier: specifiers.BaseSpecifier | None = None,
|
||||
hashes: Hashes | None = None,
|
||||
) -> CandidateEvaluator:
|
||||
specifier: Optional[specifiers.BaseSpecifier] = None,
|
||||
hashes: Optional[Hashes] = None,
|
||||
) -> "CandidateEvaluator":
|
||||
"""Create a CandidateEvaluator object.
|
||||
|
||||
:param target_python: The target Python interpreter to use when
|
||||
|
|
@ -424,11 +428,11 @@ class CandidateEvaluator:
|
|||
def __init__(
|
||||
self,
|
||||
project_name: str,
|
||||
supported_tags: list[Tag],
|
||||
supported_tags: List[Tag],
|
||||
specifier: specifiers.BaseSpecifier,
|
||||
prefer_binary: bool = False,
|
||||
allow_all_prereleases: bool = False,
|
||||
hashes: Hashes | None = None,
|
||||
hashes: Optional[Hashes] = None,
|
||||
) -> None:
|
||||
"""
|
||||
:param supported_tags: The PEP 425 tags supported by the target
|
||||
|
|
@ -449,31 +453,32 @@ class CandidateEvaluator:
|
|||
|
||||
def get_applicable_candidates(
|
||||
self,
|
||||
candidates: list[InstallationCandidate],
|
||||
) -> list[InstallationCandidate]:
|
||||
candidates: List[InstallationCandidate],
|
||||
) -> List[InstallationCandidate]:
|
||||
"""
|
||||
Return the applicable candidates from a list of candidates.
|
||||
"""
|
||||
# Using None infers from the specifier instead.
|
||||
allow_prereleases = self._allow_all_prereleases or None
|
||||
specifier = self._specifier
|
||||
|
||||
# We turn the version object into a str here because otherwise
|
||||
# when we're debundled but setuptools isn't, Python will see
|
||||
# packaging.version.Version and
|
||||
# pkg_resources._vendor.packaging.version.Version as different
|
||||
# types. This way we'll use a str as a common data interchange
|
||||
# format. If we stop using the pkg_resources provided specifier
|
||||
# and start using our own, we can drop the cast to str().
|
||||
candidates_and_versions = [(c, str(c.version)) for c in candidates]
|
||||
versions = set(
|
||||
specifier.filter(
|
||||
(v for _, v in candidates_and_versions),
|
||||
versions = {
|
||||
str(v)
|
||||
for v in specifier.filter(
|
||||
# We turn the version object into a str here because otherwise
|
||||
# when we're debundled but setuptools isn't, Python will see
|
||||
# packaging.version.Version and
|
||||
# pkg_resources._vendor.packaging.version.Version as different
|
||||
# types. This way we'll use a str as a common data interchange
|
||||
# format. If we stop using the pkg_resources provided specifier
|
||||
# and start using our own, we can drop the cast to str().
|
||||
(str(c.version) for c in candidates),
|
||||
prereleases=allow_prereleases,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
# Again, converting version to str to deal with debundling.
|
||||
applicable_candidates = [c for c in candidates if str(c.version) in versions]
|
||||
|
||||
applicable_candidates = [c for c, v in candidates_and_versions if v in versions]
|
||||
filtered_applicable_candidates = filter_unallowed_hashes(
|
||||
candidates=applicable_candidates,
|
||||
hashes=self._hashes,
|
||||
|
|
@ -533,7 +538,11 @@ class CandidateEvaluator:
|
|||
)
|
||||
if self._prefer_binary:
|
||||
binary_preference = 1
|
||||
build_tag = wheel.build_tag
|
||||
if wheel.build_tag is not None:
|
||||
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
|
||||
assert match is not None, "guaranteed by filename validation"
|
||||
build_tag_groups = match.groups()
|
||||
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
|
||||
else: # sdist
|
||||
pri = -(support_num)
|
||||
has_allowed_hash = int(link.is_hash_allowed(self._hashes))
|
||||
|
|
@ -549,8 +558,8 @@ class CandidateEvaluator:
|
|||
|
||||
def sort_best_candidate(
|
||||
self,
|
||||
candidates: list[InstallationCandidate],
|
||||
) -> InstallationCandidate | None:
|
||||
candidates: List[InstallationCandidate],
|
||||
) -> Optional[InstallationCandidate]:
|
||||
"""
|
||||
Return the best candidate per the instance's sort order, or None if
|
||||
no candidate is acceptable.
|
||||
|
|
@ -562,7 +571,7 @@ class CandidateEvaluator:
|
|||
|
||||
def compute_best_candidate(
|
||||
self,
|
||||
candidates: list[InstallationCandidate],
|
||||
candidates: List[InstallationCandidate],
|
||||
) -> BestCandidateResult:
|
||||
"""
|
||||
Compute and return a `BestCandidateResult` instance.
|
||||
|
|
@ -590,9 +599,9 @@ class PackageFinder:
|
|||
link_collector: LinkCollector,
|
||||
target_python: TargetPython,
|
||||
allow_yanked: bool,
|
||||
format_control: FormatControl | None = None,
|
||||
candidate_prefs: CandidatePreferences | None = None,
|
||||
ignore_requires_python: bool | None = None,
|
||||
format_control: Optional[FormatControl] = None,
|
||||
candidate_prefs: Optional[CandidatePreferences] = None,
|
||||
ignore_requires_python: Optional[bool] = None,
|
||||
) -> None:
|
||||
"""
|
||||
This constructor is primarily meant to be used by the create() class
|
||||
|
|
@ -618,14 +627,7 @@ class PackageFinder:
|
|||
self.format_control = format_control
|
||||
|
||||
# These are boring links that have already been logged somehow.
|
||||
self._logged_links: set[tuple[Link, LinkType, str]] = set()
|
||||
|
||||
# Cache of the result of finding candidates
|
||||
self._all_candidates: dict[str, list[InstallationCandidate]] = {}
|
||||
self._best_candidates: dict[
|
||||
tuple[str, specifiers.BaseSpecifier | None, Hashes | None],
|
||||
BestCandidateResult,
|
||||
] = {}
|
||||
self._logged_links: Set[Tuple[Link, LinkType, str]] = set()
|
||||
|
||||
# Don't include an allow_yanked default value to make sure each call
|
||||
# site considers whether yanked releases are allowed. This also causes
|
||||
|
|
@ -636,8 +638,8 @@ class PackageFinder:
|
|||
cls,
|
||||
link_collector: LinkCollector,
|
||||
selection_prefs: SelectionPreferences,
|
||||
target_python: TargetPython | None = None,
|
||||
) -> PackageFinder:
|
||||
target_python: Optional[TargetPython] = None,
|
||||
) -> "PackageFinder":
|
||||
"""Create a PackageFinder.
|
||||
|
||||
:param selection_prefs: The candidate selection preferences, as a
|
||||
|
|
@ -676,36 +678,18 @@ class PackageFinder:
|
|||
self._link_collector.search_scope = search_scope
|
||||
|
||||
@property
|
||||
def find_links(self) -> list[str]:
|
||||
def find_links(self) -> List[str]:
|
||||
return self._link_collector.find_links
|
||||
|
||||
@property
|
||||
def index_urls(self) -> list[str]:
|
||||
def index_urls(self) -> List[str]:
|
||||
return self.search_scope.index_urls
|
||||
|
||||
@property
|
||||
def proxy(self) -> str | None:
|
||||
return self._link_collector.session.pip_proxy
|
||||
|
||||
@property
|
||||
def trusted_hosts(self) -> Iterable[str]:
|
||||
for host_port in self._link_collector.session.pip_trusted_origins:
|
||||
yield build_netloc(*host_port)
|
||||
|
||||
@property
|
||||
def custom_cert(self) -> str | None:
|
||||
# session.verify is either a boolean (use default bundle/no SSL
|
||||
# verification) or a string path to a custom CA bundle to use. We only
|
||||
# care about the latter.
|
||||
verify = self._link_collector.session.verify
|
||||
return verify if isinstance(verify, str) else None
|
||||
|
||||
@property
|
||||
def client_cert(self) -> str | None:
|
||||
cert = self._link_collector.session.cert
|
||||
assert not isinstance(cert, tuple), "pip only supports PEM client certs"
|
||||
return cert
|
||||
|
||||
@property
|
||||
def allow_all_prereleases(self) -> bool:
|
||||
return self._candidate_prefs.allow_all_prereleases
|
||||
|
|
@ -720,7 +704,7 @@ class PackageFinder:
|
|||
def set_prefer_binary(self) -> None:
|
||||
self._candidate_prefs.prefer_binary = True
|
||||
|
||||
def requires_python_skipped_reasons(self) -> list[str]:
|
||||
def requires_python_skipped_reasons(self) -> List[str]:
|
||||
reasons = {
|
||||
detail
|
||||
for _, result, detail in self._logged_links
|
||||
|
|
@ -741,13 +725,13 @@ class PackageFinder:
|
|||
ignore_requires_python=self._ignore_requires_python,
|
||||
)
|
||||
|
||||
def _sort_links(self, links: Iterable[Link]) -> list[Link]:
|
||||
def _sort_links(self, links: Iterable[Link]) -> List[Link]:
|
||||
"""
|
||||
Returns elements of links in order, non-egg links first, egg links
|
||||
second, while eliminating duplicates
|
||||
"""
|
||||
eggs, no_eggs = [], []
|
||||
seen: set[Link] = set()
|
||||
seen: Set[Link] = set()
|
||||
for link in links:
|
||||
if link not in seen:
|
||||
seen.add(link)
|
||||
|
|
@ -767,7 +751,7 @@ class PackageFinder:
|
|||
|
||||
def get_install_candidate(
|
||||
self, link_evaluator: LinkEvaluator, link: Link
|
||||
) -> InstallationCandidate | None:
|
||||
) -> Optional[InstallationCandidate]:
|
||||
"""
|
||||
If the link is a candidate for install, convert it to an
|
||||
InstallationCandidate and return it. Otherwise, return None.
|
||||
|
|
@ -777,18 +761,15 @@ class PackageFinder:
|
|||
self._log_skipped_link(link, result, detail)
|
||||
return None
|
||||
|
||||
try:
|
||||
return InstallationCandidate(
|
||||
name=link_evaluator.project_name,
|
||||
link=link,
|
||||
version=detail,
|
||||
)
|
||||
except InvalidVersion:
|
||||
return None
|
||||
return InstallationCandidate(
|
||||
name=link_evaluator.project_name,
|
||||
link=link,
|
||||
version=detail,
|
||||
)
|
||||
|
||||
def evaluate_links(
|
||||
self, link_evaluator: LinkEvaluator, links: Iterable[Link]
|
||||
) -> list[InstallationCandidate]:
|
||||
) -> List[InstallationCandidate]:
|
||||
"""
|
||||
Convert links that are candidates to InstallationCandidate objects.
|
||||
"""
|
||||
|
|
@ -802,7 +783,7 @@ class PackageFinder:
|
|||
|
||||
def process_project_url(
|
||||
self, project_url: Link, link_evaluator: LinkEvaluator
|
||||
) -> list[InstallationCandidate]:
|
||||
) -> List[InstallationCandidate]:
|
||||
logger.debug(
|
||||
"Fetching project page and analyzing links: %s",
|
||||
project_url,
|
||||
|
|
@ -821,7 +802,8 @@ class PackageFinder:
|
|||
|
||||
return package_links
|
||||
|
||||
def find_all_candidates(self, project_name: str) -> list[InstallationCandidate]:
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
|
||||
"""Find all available InstallationCandidate for project_name
|
||||
|
||||
This checks index_urls and find_links.
|
||||
|
|
@ -830,9 +812,6 @@ class PackageFinder:
|
|||
See LinkEvaluator.evaluate_link() for details on which files
|
||||
are accepted.
|
||||
"""
|
||||
if project_name in self._all_candidates:
|
||||
return self._all_candidates[project_name]
|
||||
|
||||
link_evaluator = self.make_link_evaluator(project_name)
|
||||
|
||||
collected_sources = self._link_collector.collect_sources(
|
||||
|
|
@ -874,15 +853,13 @@ class PackageFinder:
|
|||
logger.debug("Local files found: %s", ", ".join(paths))
|
||||
|
||||
# This is an intentional priority ordering
|
||||
self._all_candidates[project_name] = file_candidates + page_candidates
|
||||
|
||||
return self._all_candidates[project_name]
|
||||
return file_candidates + page_candidates
|
||||
|
||||
def make_candidate_evaluator(
|
||||
self,
|
||||
project_name: str,
|
||||
specifier: specifiers.BaseSpecifier | None = None,
|
||||
hashes: Hashes | None = None,
|
||||
specifier: Optional[specifiers.BaseSpecifier] = None,
|
||||
hashes: Optional[Hashes] = None,
|
||||
) -> CandidateEvaluator:
|
||||
"""Create a CandidateEvaluator object to use."""
|
||||
candidate_prefs = self._candidate_prefs
|
||||
|
|
@ -895,11 +872,12 @@ class PackageFinder:
|
|||
hashes=hashes,
|
||||
)
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def find_best_candidate(
|
||||
self,
|
||||
project_name: str,
|
||||
specifier: specifiers.BaseSpecifier | None = None,
|
||||
hashes: Hashes | None = None,
|
||||
specifier: Optional[specifiers.BaseSpecifier] = None,
|
||||
hashes: Optional[Hashes] = None,
|
||||
) -> BestCandidateResult:
|
||||
"""Find matches for the given project and specifier.
|
||||
|
||||
|
|
@ -909,42 +887,32 @@ class PackageFinder:
|
|||
|
||||
:return: A `BestCandidateResult` instance.
|
||||
"""
|
||||
if (project_name, specifier, hashes) in self._best_candidates:
|
||||
return self._best_candidates[project_name, specifier, hashes]
|
||||
|
||||
candidates = self.find_all_candidates(project_name)
|
||||
candidate_evaluator = self.make_candidate_evaluator(
|
||||
project_name=project_name,
|
||||
specifier=specifier,
|
||||
hashes=hashes,
|
||||
)
|
||||
self._best_candidates[project_name, specifier, hashes] = (
|
||||
candidate_evaluator.compute_best_candidate(candidates)
|
||||
)
|
||||
|
||||
return self._best_candidates[project_name, specifier, hashes]
|
||||
return candidate_evaluator.compute_best_candidate(candidates)
|
||||
|
||||
def find_requirement(
|
||||
self, req: InstallRequirement, upgrade: bool
|
||||
) -> InstallationCandidate | None:
|
||||
) -> Optional[InstallationCandidate]:
|
||||
"""Try to find a Link matching req
|
||||
|
||||
Expects req, an InstallRequirement and upgrade, a boolean
|
||||
Returns a InstallationCandidate if found,
|
||||
Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise
|
||||
"""
|
||||
name = req.name
|
||||
assert name is not None, "find_requirement() called with no name"
|
||||
|
||||
hashes = req.hashes(trust_internet=False)
|
||||
best_candidate_result = self.find_best_candidate(
|
||||
name,
|
||||
req.name,
|
||||
specifier=req.specifier,
|
||||
hashes=hashes,
|
||||
)
|
||||
best_candidate = best_candidate_result.best_candidate
|
||||
|
||||
installed_version: _BaseVersion | None = None
|
||||
installed_version: Optional[_BaseVersion] = None
|
||||
if req.satisfied_by is not None:
|
||||
installed_version = req.satisfied_by.version
|
||||
|
||||
|
|
@ -968,14 +936,14 @@ class PackageFinder:
|
|||
"Could not find a version that satisfies the requirement %s "
|
||||
"(from versions: %s)",
|
||||
req,
|
||||
_format_versions(best_candidate_result.all_candidates),
|
||||
_format_versions(best_candidate_result.iter_all()),
|
||||
)
|
||||
|
||||
raise DistributionNotFound(f"No matching distribution found for {req}")
|
||||
|
||||
def _should_install_candidate(
|
||||
candidate: InstallationCandidate | None,
|
||||
) -> TypeGuard[InstallationCandidate]:
|
||||
candidate: Optional[InstallationCandidate],
|
||||
) -> "TypeGuard[InstallationCandidate]":
|
||||
if installed_version is None:
|
||||
return True
|
||||
if best_candidate is None:
|
||||
|
|
@ -1002,7 +970,7 @@ class PackageFinder:
|
|||
logger.debug(
|
||||
"Using version %s (newest of versions: %s)",
|
||||
best_candidate.version,
|
||||
_format_versions(best_candidate_result.applicable_candidates),
|
||||
_format_versions(best_candidate_result.iter_applicable()),
|
||||
)
|
||||
return best_candidate
|
||||
|
||||
|
|
@ -1010,7 +978,7 @@ class PackageFinder:
|
|||
logger.debug(
|
||||
"Installed version (%s) is most up-to-date (past versions: %s)",
|
||||
installed_version,
|
||||
_format_versions(best_candidate_result.applicable_candidates),
|
||||
_format_versions(best_candidate_result.iter_applicable()),
|
||||
)
|
||||
raise BestVersionAlreadyInstalled
|
||||
|
||||
|
|
@ -1041,7 +1009,7 @@ def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
|
|||
raise ValueError(f"{fragment} does not match {canonical_name}")
|
||||
|
||||
|
||||
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> str | None:
|
||||
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> Optional[str]:
|
||||
"""Parse the version string from a <package>+<version> filename
|
||||
"fragment" (stem) or egg fragment.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable
|
||||
from typing import Callable
|
||||
from typing import Callable, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from pip._vendor.packaging.utils import (
|
||||
InvalidSdistFilename,
|
||||
InvalidVersion,
|
||||
InvalidWheelFilename,
|
||||
canonicalize_name,
|
||||
parse_sdist_filename,
|
||||
|
|
@ -30,7 +28,7 @@ PageValidator = Callable[[Link], bool]
|
|||
|
||||
class LinkSource:
|
||||
@property
|
||||
def link(self) -> Link | None:
|
||||
def link(self) -> Optional[Link]:
|
||||
"""Returns the underlying link, if there's one."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
|
@ -52,8 +50,8 @@ class _FlatDirectoryToUrls:
|
|||
|
||||
def __init__(self, path: str) -> None:
|
||||
self._path = path
|
||||
self._page_candidates: list[str] = []
|
||||
self._project_name_to_urls: dict[str, list[str]] = defaultdict(list)
|
||||
self._page_candidates: List[str] = []
|
||||
self._project_name_to_urls: Dict[str, List[str]] = defaultdict(list)
|
||||
self._scanned_directory = False
|
||||
|
||||
def _scan_directory(self) -> None:
|
||||
|
|
@ -70,24 +68,24 @@ class _FlatDirectoryToUrls:
|
|||
# otherwise not worth considering as a package
|
||||
try:
|
||||
project_filename = parse_wheel_filename(entry.name)[0]
|
||||
except InvalidWheelFilename:
|
||||
except (InvalidWheelFilename, InvalidVersion):
|
||||
try:
|
||||
project_filename = parse_sdist_filename(entry.name)[0]
|
||||
except InvalidSdistFilename:
|
||||
except (InvalidSdistFilename, InvalidVersion):
|
||||
continue
|
||||
|
||||
self._project_name_to_urls[project_filename].append(url)
|
||||
self._scanned_directory = True
|
||||
|
||||
@property
|
||||
def page_candidates(self) -> list[str]:
|
||||
def page_candidates(self) -> List[str]:
|
||||
if not self._scanned_directory:
|
||||
self._scan_directory()
|
||||
|
||||
return self._page_candidates
|
||||
|
||||
@property
|
||||
def project_name_to_urls(self) -> dict[str, list[str]]:
|
||||
def project_name_to_urls(self) -> Dict[str, List[str]]:
|
||||
if not self._scanned_directory:
|
||||
self._scan_directory()
|
||||
|
||||
|
|
@ -103,7 +101,7 @@ class _FlatDirectorySource(LinkSource):
|
|||
* ``file_candidates``: Archives in the directory.
|
||||
"""
|
||||
|
||||
_paths_to_urls: dict[str, _FlatDirectoryToUrls] = {}
|
||||
_paths_to_urls: Dict[str, _FlatDirectoryToUrls] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -122,7 +120,7 @@ class _FlatDirectorySource(LinkSource):
|
|||
self._paths_to_urls[path] = self._path_to_urls
|
||||
|
||||
@property
|
||||
def link(self) -> Link | None:
|
||||
def link(self) -> Optional[Link]:
|
||||
return None
|
||||
|
||||
def page_candidates(self) -> FoundCandidates:
|
||||
|
|
@ -153,7 +151,7 @@ class _LocalFileSource(LinkSource):
|
|||
self._link = link
|
||||
|
||||
@property
|
||||
def link(self) -> Link | None:
|
||||
def link(self) -> Optional[Link]:
|
||||
return self._link
|
||||
|
||||
def page_candidates(self) -> FoundCandidates:
|
||||
|
|
@ -187,7 +185,7 @@ class _RemoteFileSource(LinkSource):
|
|||
self._link = link
|
||||
|
||||
@property
|
||||
def link(self) -> Link | None:
|
||||
def link(self) -> Optional[Link]:
|
||||
return self._link
|
||||
|
||||
def page_candidates(self) -> FoundCandidates:
|
||||
|
|
@ -215,7 +213,7 @@ class _IndexDirectorySource(LinkSource):
|
|||
self._link = link
|
||||
|
||||
@property
|
||||
def link(self) -> Link | None:
|
||||
def link(self) -> Optional[Link]:
|
||||
return self._link
|
||||
|
||||
def page_candidates(self) -> FoundCandidates:
|
||||
|
|
@ -233,9 +231,9 @@ def build_source(
|
|||
expand_dir: bool,
|
||||
cache_link_parsing: bool,
|
||||
project_name: str,
|
||||
) -> tuple[str | None, LinkSource | None]:
|
||||
path: str | None = None
|
||||
url: str | None = None
|
||||
) -> Tuple[Optional[str], Optional[LinkSource]]:
|
||||
path: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
if os.path.exists(location): # Is a local path.
|
||||
url = path_to_url(location)
|
||||
path = location
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue