Voice et bot modif

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

View file

@ -1 +1,2 @@
"""Index interaction code"""
"""Index interaction code
"""

View file

@ -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.
"""

View file

@ -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.

View file

@ -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