[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2024-04-13 00:00:18 +00:00
parent 72ad6dc953
commit f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions

View file

@ -1,2 +1,3 @@
"""Index interaction code
"""
from __future__ import annotations

View file

@ -1,6 +1,7 @@
"""
The main purpose of this module is to expose LinkCollector.collect_sources().
"""
from __future__ import annotations
import cgi
import collections
@ -14,23 +15,17 @@ import urllib.request
import xml.etree.ElementTree
from html.parser import HTMLParser
from optparse import Values
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
List,
MutableMapping,
NamedTuple,
Optional,
Sequence,
Tuple,
Union,
)
from pip._vendor import html5lib, requests
from pip._vendor.requests import Response
from pip._vendor.requests.exceptions import RetryError, SSLError
from typing import Callable
from typing import Dict
from typing import Iterable
from typing import List
from typing import MutableMapping
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.models.link import Link
@ -38,10 +33,18 @@ from pip._internal.models.search_scope import SearchScope
from pip._internal.network.session import PipSession
from pip._internal.network.utils import raise_for_status
from pip._internal.utils.filetypes import is_archive_file
from pip._internal.utils.misc import pairwise, redact_auth_from_url
from pip._internal.utils.misc import pairwise
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.vcs import vcs
from pip._vendor import html5lib
from pip._vendor import requests
from pip._vendor.requests import Response
from pip._vendor.requests.exceptions import RetryError
from pip._vendor.requests.exceptions import SSLError
from .sources import CandidatesFromPage, LinkSource, build_source
from .sources import build_source
from .sources import CandidatesFromPage
from .sources import LinkSource
if TYPE_CHECKING:
from typing import Protocol
@ -54,13 +57,13 @@ HTMLElement = xml.etree.ElementTree.Element
ResponseHeaders = MutableMapping[str, str]
def _match_vcs_scheme(url: str) -> Optional[str]:
def _match_vcs_scheme(url: str) -> str | None:
"""Look for VCS schemes in the URL.
Returns the matched VCS scheme, or None if there's no match.
"""
for scheme in vcs.schemes:
if url.lower().startswith(scheme) and url[len(scheme)] in "+:":
if url.lower().startswith(scheme) and url[len(scheme)] in '+:':
return scheme
return None
@ -77,8 +80,8 @@ def _ensure_html_header(response: Response) -> None:
Raises `_NotHTML` if the content type is not text/html.
"""
content_type = response.headers.get("Content-Type", "")
if not content_type.lower().startswith("text/html"):
content_type = response.headers.get('Content-Type', '')
if not content_type.lower().startswith('text/html'):
raise _NotHTML(content_type, response.request.method)
@ -93,7 +96,7 @@ def _ensure_html_response(url: str, session: PipSession) -> None:
`_NotHTML` if the content type is not text/html.
"""
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
if scheme not in {"http", "https"}:
if scheme not in {'http', 'https'}:
raise _NotHTTP()
resp = session.head(url, allow_redirects=True)
@ -118,12 +121,12 @@ def _get_html_response(url: str, session: PipSession) -> Response:
if is_archive_file(Link(url).filename):
_ensure_html_response(url, session=session)
logger.debug("Getting page %s", redact_auth_from_url(url))
logger.debug('Getting page %s', redact_auth_from_url(url))
resp = session.get(
url,
headers={
"Accept": "text/html",
'Accept': 'text/html',
# We don't want to blindly returned cached data for
# /simple/, because authors generally expecting that
# twine upload && pip install will function, but if
@ -137,7 +140,7 @@ def _get_html_response(url: str, session: PipSession) -> Response:
# trip for the conditional GET now instead of only
# once per 10 minutes.
# For more information, please see pypa/pip#5670.
"Cache-Control": "max-age=0",
'Cache-Control': 'max-age=0',
},
)
raise_for_status(resp)
@ -152,12 +155,12 @@ def _get_html_response(url: str, session: PipSession) -> Response:
return resp
def _get_encoding_from_headers(headers: ResponseHeaders) -> Optional[str]:
def _get_encoding_from_headers(headers: ResponseHeaders) -> str | None:
"""Determine if we have any encoding information in our headers."""
if headers and "Content-Type" in headers:
content_type, params = cgi.parse_header(headers["Content-Type"])
if "charset" in params:
return params["charset"]
if headers and 'Content-Type' in headers:
content_type, params = cgi.parse_header(headers['Content-Type'])
if 'charset' in params:
return params['charset']
return None
@ -175,8 +178,8 @@ def _determine_base_url(document: HTMLElement, page_url: str) -> str:
TODO: Remove when `html5lib` is dropped.
"""
for base in document.findall(".//base"):
href = base.get("href")
for base in document.findall('.//base'):
href = base.get('href')
if href is not None:
return href
return page_url
@ -204,7 +207,7 @@ def _clean_file_url_path(part: str) -> str:
# percent-encoded: /
_reserved_chars_re = re.compile("(@|%2F)", re.IGNORECASE)
_reserved_chars_re = re.compile('(@|%2F)', re.IGNORECASE)
def _clean_url_path(path: str, is_local_path: bool) -> str:
@ -221,12 +224,12 @@ def _clean_url_path(path: str, is_local_path: bool) -> str:
parts = _reserved_chars_re.split(path)
cleaned_parts = []
for to_clean, reserved in pairwise(itertools.chain(parts, [""])):
for to_clean, reserved in pairwise(itertools.chain(parts, [''])):
cleaned_parts.append(clean_func(to_clean))
# Normalize %xx escapes (e.g. %2f -> %2F)
cleaned_parts.append(reserved.upper())
return "".join(cleaned_parts)
return ''.join(cleaned_parts)
def _clean_link(url: str) -> str:
@ -245,20 +248,20 @@ def _clean_link(url: str) -> str:
def _create_link_from_element(
element_attribs: Dict[str, Optional[str]],
element_attribs: dict[str, str | None],
page_url: str,
base_url: str,
) -> Optional[Link]:
) -> Link | None:
"""
Convert an anchor element's attributes in a simple repository page to a Link.
"""
href = element_attribs.get("href")
href = element_attribs.get('href')
if not href:
return None
url = _clean_link(urllib.parse.urljoin(base_url, href))
pyrequire = element_attribs.get("data-requires-python")
yanked_reason = element_attribs.get("data-yanked")
pyrequire = element_attribs.get('data-requires-python')
yanked_reason = element_attribs.get('data-yanked')
link = Link(
url,
@ -271,7 +274,7 @@ def _create_link_from_element(
class CacheablePageContent:
def __init__(self, page: "HTMLPage") -> None:
def __init__(self, page: HTMLPage) -> None:
assert page.cache_link_parsing
self.page = page
@ -284,7 +287,7 @@ class CacheablePageContent:
class ParseLinks(Protocol):
def __call__(
self, page: "HTMLPage", use_deprecated_html5lib: bool
self, page: HTMLPage, use_deprecated_html5lib: bool,
) -> Iterable[Link]:
...
@ -298,12 +301,12 @@ def with_cached_html_pages(fn: ParseLinks) -> ParseLinks:
@functools.lru_cache(maxsize=None)
def wrapper(
cacheable_page: CacheablePageContent, use_deprecated_html5lib: bool
) -> List[Link]:
cacheable_page: CacheablePageContent, use_deprecated_html5lib: bool,
) -> list[Link]:
return list(fn(cacheable_page.page, use_deprecated_html5lib))
@functools.wraps(fn)
def wrapper_wrapper(page: "HTMLPage", use_deprecated_html5lib: bool) -> List[Link]:
def wrapper_wrapper(page: HTMLPage, use_deprecated_html5lib: bool) -> list[Link]:
if page.cache_link_parsing:
return wrapper(CacheablePageContent(page), use_deprecated_html5lib)
return list(fn(page, use_deprecated_html5lib))
@ -311,7 +314,7 @@ def with_cached_html_pages(fn: ParseLinks) -> ParseLinks:
return wrapper_wrapper
def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
def _parse_links_html5lib(page: HTMLPage) -> Iterable[Link]:
"""
Parse an HTML document, and yield its anchor elements as Link objects.
@ -325,7 +328,7 @@ def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
url = page.url
base_url = _determine_base_url(document, url)
for anchor in document.findall(".//a"):
for anchor in document.findall('.//a'):
link = _create_link_from_element(
anchor.attrib,
page_url=url,
@ -337,7 +340,7 @@ def _parse_links_html5lib(page: "HTMLPage") -> Iterable[Link]:
@with_cached_html_pages
def parse_links(page: "HTMLPage", use_deprecated_html5lib: bool) -> Iterable[Link]:
def parse_links(page: HTMLPage, use_deprecated_html5lib: bool) -> Iterable[Link]:
"""
Parse an HTML document, and yield its anchor elements as Link objects.
"""
@ -347,7 +350,7 @@ def parse_links(page: "HTMLPage", use_deprecated_html5lib: bool) -> Iterable[Lin
return
parser = HTMLLinkParser(page.url)
encoding = page.encoding or "utf-8"
encoding = page.encoding or 'utf-8'
parser.feed(page.content.decode(encoding))
url = page.url
@ -369,7 +372,7 @@ class HTMLPage:
def __init__(
self,
content: bytes,
encoding: Optional[str],
encoding: str | None,
url: str,
cache_link_parsing: bool = True,
) -> None:
@ -399,32 +402,32 @@ class HTMLLinkParser(HTMLParser):
super().__init__(convert_charrefs=True)
self.url: str = url
self.base_url: Optional[str] = None
self.anchors: List[Dict[str, Optional[str]]] = []
self.base_url: str | None = None
self.anchors: list[dict[str, str | None]] = []
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Optional[str]]]) -> None:
if tag == "base" and self.base_url is None:
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == 'base' and self.base_url is None:
href = self.get_href(attrs)
if href is not None:
self.base_url = href
elif tag == "a":
elif tag == 'a':
self.anchors.append(dict(attrs))
def get_href(self, attrs: List[Tuple[str, Optional[str]]]) -> Optional[str]:
def get_href(self, attrs: list[tuple[str, str | None]]) -> str | None:
for name, value in attrs:
if name == "href":
if name == 'href':
return value
return None
def _handle_get_page_fail(
link: Link,
reason: Union[str, Exception],
meth: Optional[Callable[..., None]] = None,
reason: str | Exception,
meth: Callable[..., None] | None = None,
) -> None:
if meth is None:
meth = logger.debug
meth("Could not fetch URL %s: %s - skipping", link, reason)
meth('Could not fetch URL %s: %s - skipping', link, reason)
def _make_html_page(response: Response, cache_link_parsing: bool = True) -> HTMLPage:
@ -438,20 +441,20 @@ def _make_html_page(response: Response, cache_link_parsing: bool = True) -> HTML
def _get_html_page(
link: Link, session: Optional[PipSession] = None
) -> Optional["HTMLPage"]:
link: Link, session: PipSession | None = None,
) -> HTMLPage | None:
if session is None:
raise TypeError(
"_get_html_page() missing 1 required keyword argument: 'session'"
"_get_html_page() missing 1 required keyword argument: 'session'",
)
url = link.url.split("#", 1)[0]
url = link.url.split('#', 1)[0]
# Check for VCS schemes that do not support lookup as web pages.
vcs_scheme = _match_vcs_scheme(url)
if vcs_scheme:
logger.warning(
"Cannot look at %s URL %s because it does not support lookup as web pages.",
'Cannot look at %s URL %s because it does not support lookup as web pages.',
vcs_scheme,
link,
)
@ -459,26 +462,26 @@ def _get_html_page(
# Tack index.html onto file:// URLs that point to directories
scheme, _, path, _, _, _ = urllib.parse.urlparse(url)
if scheme == "file" and os.path.isdir(urllib.request.url2pathname(path)):
if scheme == 'file' and os.path.isdir(urllib.request.url2pathname(path)):
# add trailing slash if not present so urljoin doesn't trim
# final segment
if not url.endswith("/"):
url += "/"
url = urllib.parse.urljoin(url, "index.html")
logger.debug(" file: URL is directory, getting %s", url)
if not url.endswith('/'):
url += '/'
url = urllib.parse.urljoin(url, 'index.html')
logger.debug(' file: URL is directory, getting %s', url)
try:
resp = _get_html_response(url, session=session)
except _NotHTTP:
logger.warning(
"Skipping page %s because it looks like an archive, and cannot "
"be checked by a HTTP HEAD request.",
'Skipping page %s because it looks like an archive, and cannot '
'be checked by a HTTP HEAD request.',
link,
)
except _NotHTML as exc:
logger.warning(
"Skipping page %s because the %s request got Content-Type: %s."
"The only supported Content-Type is text/html",
'Skipping page %s because the %s request got Content-Type: %s.'
'The only supported Content-Type is text/html',
link,
exc.request_desc,
exc.content_type,
@ -488,21 +491,21 @@ def _get_html_page(
except RetryError as exc:
_handle_get_page_fail(link, exc)
except SSLError as exc:
reason = "There was a problem confirming the ssl certificate: "
reason = 'There was a problem confirming the ssl certificate: '
reason += str(exc)
_handle_get_page_fail(link, reason, meth=logger.info)
except requests.ConnectionError as exc:
_handle_get_page_fail(link, f"connection error: {exc}")
_handle_get_page_fail(link, f'connection error: {exc}')
except requests.Timeout:
_handle_get_page_fail(link, "timed out")
_handle_get_page_fail(link, 'timed out')
else:
return _make_html_page(resp, cache_link_parsing=link.cache_link_parsing)
return None
class CollectedSources(NamedTuple):
find_links: Sequence[Optional[LinkSource]]
index_urls: Sequence[Optional[LinkSource]]
find_links: Sequence[LinkSource | None]
index_urls: Sequence[LinkSource | None]
class LinkCollector:
@ -528,7 +531,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
@ -537,8 +540,8 @@ class LinkCollector:
index_urls = [options.index_url] + options.extra_index_urls
if options.no_index and not suppress_no_index:
logger.debug(
"Ignoring indexes: %s",
",".join(redact_auth_from_url(url) for url in index_urls),
'Ignoring indexes: %s',
','.join(redact_auth_from_url(url) for url in index_urls),
)
index_urls = []
@ -556,10 +559,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_page(self, location: Link) -> Optional[HTMLPage]:
def fetch_page(self, location: Link) -> HTMLPage | None:
"""
Fetch an HTML page containing package links.
"""
@ -594,15 +597,15 @@ class LinkCollector:
if logger.isEnabledFor(logging.DEBUG):
lines = [
f"* {s.link}"
f'* {s.link}'
for s in itertools.chain(find_links_sources, index_url_sources)
if s is not None and s.link is not None
]
lines = [
f"{len(lines)} location(s) to search "
f"for versions of {project_name}:"
f'{len(lines)} location(s) to search '
f'for versions of {project_name}:',
] + lines
logger.debug("\n".join(lines))
logger.debug('\n'.join(lines))
return CollectedSources(
find_links=list(find_links_sources),

View file

@ -1,27 +1,26 @@
"""Routines related to PyPI, indexes"""
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import functools
import itertools
import logging
import re
from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union
from typing import FrozenSet
from typing import Iterable
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Union
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
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 (
BestVersionAlreadyInstalled,
DistributionNotFound,
InvalidWheelFilename,
UnsupportedWheel,
)
from pip._internal.index.collector import LinkCollector, parse_links
from pip._internal.exceptions import BestVersionAlreadyInstalled
from pip._internal.exceptions import DistributionNotFound
from pip._internal.exceptions import InvalidWheelFilename
from pip._internal.exceptions import UnsupportedWheel
from pip._internal.index.collector import LinkCollector
from pip._internal.index.collector import parse_links
from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.format_control import FormatControl
from pip._internal.models.link import Link
@ -37,8 +36,13 @@ from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import build_netloc
from pip._internal.utils.packaging import check_requires_python
from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.tags import Tag
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
__all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
__all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder']
logger = getLogger(__name__)
@ -49,7 +53,7 @@ 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:
"""
@ -68,16 +72,16 @@ def _check_link_requires_python(
)
except specifiers.InvalidSpecifier:
logger.debug(
"Ignoring invalid Requires-Python (%r) for link: %s",
'Ignoring invalid Requires-Python (%r) for link: %s',
link.requires_python,
link,
)
else:
if not is_compatible:
version = ".".join(map(str, version_info))
version = '.'.join(map(str, version_info))
if not ignore_requires_python:
logger.verbose(
"Link requires a different Python (%s not in: %r): %s",
'Link requires a different Python (%s not in: %r): %s',
version,
link.requires_python,
link,
@ -85,7 +89,7 @@ def _check_link_requires_python(
return False
logger.debug(
"Ignoring failed Requires-Python check (%s not in: %r) for link: %s",
'Ignoring failed Requires-Python check (%s not in: %r) for link: %s',
version,
link.requires_python,
link,
@ -100,7 +104,7 @@ class LinkEvaluator:
Responsible for evaluating links for a particular project.
"""
_py_version_re = re.compile(r"-py([123]\.?[0-9]?)$")
_py_version_re = re.compile(r'-py([123]\.?[0-9]?)$')
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@ -110,10 +114,10 @@ class LinkEvaluator:
self,
project_name: str,
canonical_name: str,
formats: FrozenSet[str],
formats: frozenset[str],
target_python: TargetPython,
allow_yanked: bool,
ignore_requires_python: Optional[bool] = None,
ignore_requires_python: bool | None = None,
) -> None:
"""
:param project_name: The user supplied package name.
@ -143,7 +147,7 @@ class LinkEvaluator:
self.project_name = project_name
def evaluate_link(self, link: Link) -> Tuple[bool, Optional[str]]:
def evaluate_link(self, link: Link) -> tuple[bool, str | None]:
"""
Determine whether a link is a candidate for installation.
@ -154,8 +158,8 @@ class LinkEvaluator:
"""
version = None
if link.is_yanked and not self._allow_yanked:
reason = link.yanked_reason or "<none given>"
return (False, f"yanked for reason: {reason}")
reason = link.yanked_reason or '<none given>'
return (False, f'yanked for reason: {reason}')
if link.egg_fragment:
egg_info = link.egg_fragment
@ -163,21 +167,21 @@ class LinkEvaluator:
else:
egg_info, ext = link.splitext()
if not ext:
return (False, "not a file")
return (False, 'not a file')
if ext not in SUPPORTED_EXTENSIONS:
return (False, f"unsupported archive format: {ext}")
if "binary" not in self._formats and ext == WHEEL_EXTENSION:
reason = "No binaries permitted for {}".format(self.project_name)
return (False, f'unsupported archive format: {ext}')
if 'binary' not in self._formats and ext == WHEEL_EXTENSION:
reason = f'No binaries permitted for {self.project_name}'
return (False, reason)
if "macosx10" in link.path and ext == ".zip":
return (False, "macosx10 one")
if 'macosx10' in link.path and ext == '.zip':
return (False, 'macosx10 one')
if ext == WHEEL_EXTENSION:
try:
wheel = Wheel(link.filename)
except InvalidWheelFilename:
return (False, "invalid wheel filename")
return (False, 'invalid wheel filename')
if canonicalize_name(wheel.name) != self._canonical_name:
reason = "wrong project name (not {})".format(self.project_name)
reason = f'wrong project name (not {self.project_name})'
return (False, reason)
supported_tags = self._target_python.get_tags()
@ -187,8 +191,8 @@ class LinkEvaluator:
file_tags = wheel.get_formatted_file_tags()
reason = (
"none of the wheel's tags ({}) are compatible "
"(run pip debug --verbose to show compatible tags)".format(
", ".join(file_tags)
'(run pip debug --verbose to show compatible tags)'.format(
', '.join(file_tags),
)
)
return (False, reason)
@ -196,8 +200,8 @@ class LinkEvaluator:
version = wheel.version
# This should be up by the self.ok_binary check, but see issue 2700.
if "source" not in self._formats and ext != WHEEL_EXTENSION:
reason = f"No sources permitted for {self.project_name}"
if 'source' not in self._formats and ext != WHEEL_EXTENSION:
reason = f'No sources permitted for {self.project_name}'
return (False, reason)
if not version:
@ -206,7 +210,7 @@ class LinkEvaluator:
self._canonical_name,
)
if not version:
reason = f"Missing project version for {self.project_name}"
reason = f'Missing project version for {self.project_name}'
return (False, reason)
match = self._py_version_re.search(version)
@ -214,7 +218,7 @@ class LinkEvaluator:
version = version[: match.start()]
py_version = match.group(1)
if py_version != self._target_python.py_version:
return (False, "Python version is incorrect")
return (False, 'Python version is incorrect')
supports_python = _check_link_requires_python(
link,
@ -226,16 +230,16 @@ class LinkEvaluator:
# _log_skipped_link().
return (False, None)
logger.debug("Found link %s, version: %s", link, version)
logger.debug('Found link %s, version: %s', link, version)
return (True, version)
def filter_unallowed_hashes(
candidates: List[InstallationCandidate],
candidates: list[InstallationCandidate],
hashes: Hashes,
project_name: str,
) -> List[InstallationCandidate]:
) -> list[InstallationCandidate]:
"""
Filter out candidates whose hashes aren't allowed, and return a new
list of candidates.
@ -253,8 +257,8 @@ def filter_unallowed_hashes(
"""
if not hashes:
logger.debug(
"Given no hashes to check %s links for project %r: "
"discarding no candidates",
'Given no hashes to check %s links for project %r: '
'discarding no candidates',
len(candidates),
project_name,
)
@ -284,16 +288,16 @@ def filter_unallowed_hashes(
filtered = list(candidates)
if len(filtered) == len(candidates):
discard_message = "discarding no candidates"
discard_message = 'discarding no candidates'
else:
discard_message = "discarding {} non-matches:\n {}".format(
discard_message = 'discarding {} non-matches:\n {}'.format(
len(non_matches),
"\n ".join(str(candidate.link) for candidate in non_matches),
'\n '.join(str(candidate.link) for candidate in non_matches),
)
logger.debug(
"Checked %s links for project %r against %s hashes "
"(%s matches, %s no digest): %s",
'Checked %s links for project %r against %s hashes '
'(%s matches, %s no digest): %s',
len(candidates),
project_name,
hashes.digest_count,
@ -333,9 +337,9 @@ class BestCandidateResult:
def __init__(
self,
candidates: List[InstallationCandidate],
applicable_candidates: List[InstallationCandidate],
best_candidate: Optional[InstallationCandidate],
candidates: list[InstallationCandidate],
applicable_candidates: list[InstallationCandidate],
best_candidate: InstallationCandidate | None,
) -> None:
"""
:param candidates: A sequence of all available candidates found.
@ -375,12 +379,12 @@ class CandidateEvaluator:
def create(
cls,
project_name: str,
target_python: Optional[TargetPython] = None,
target_python: TargetPython | None = None,
prefer_binary: bool = False,
allow_all_prereleases: bool = False,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
) -> "CandidateEvaluator":
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> CandidateEvaluator:
"""Create a CandidateEvaluator object.
:param target_python: The target Python interpreter to use when
@ -410,11 +414,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: Optional[Hashes] = None,
hashes: Hashes | None = None,
) -> None:
"""
:param supported_tags: The PEP 425 tags supported by the target
@ -435,8 +439,8 @@ 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.
"""
@ -510,18 +514,18 @@ class CandidateEvaluator:
try:
pri = -(
wheel.find_most_preferred_tag(
valid_tags, self._wheel_tag_preferences
valid_tags, self._wheel_tag_preferences,
)
)
except ValueError:
raise UnsupportedWheel(
"{} is not a supported wheel for this platform. It "
"can't be sorted.".format(wheel.filename)
'{} is not a supported wheel for this platform. It '
"can't be sorted.".format(wheel.filename),
)
if self._prefer_binary:
binary_preference = 1
if wheel.build_tag is not None:
match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
match = re.match(r'^(\d+)(.*)$', wheel.build_tag)
build_tag_groups = match.groups()
build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
else: # sdist
@ -539,8 +543,8 @@ class CandidateEvaluator:
def sort_best_candidate(
self,
candidates: List[InstallationCandidate],
) -> Optional[InstallationCandidate]:
candidates: list[InstallationCandidate],
) -> InstallationCandidate | None:
"""
Return the best candidate per the instance's sort order, or None if
no candidate is acceptable.
@ -552,7 +556,7 @@ class CandidateEvaluator:
def compute_best_candidate(
self,
candidates: List[InstallationCandidate],
candidates: list[InstallationCandidate],
) -> BestCandidateResult:
"""
Compute and return a `BestCandidateResult` instance.
@ -581,9 +585,9 @@ class PackageFinder:
target_python: TargetPython,
allow_yanked: bool,
use_deprecated_html5lib: bool,
format_control: Optional[FormatControl] = None,
candidate_prefs: Optional[CandidatePreferences] = None,
ignore_requires_python: Optional[bool] = None,
format_control: FormatControl | None = None,
candidate_prefs: CandidatePreferences | None = None,
ignore_requires_python: bool | None = None,
) -> None:
"""
This constructor is primarily meant to be used by the create() class
@ -610,7 +614,7 @@ class PackageFinder:
self.format_control = format_control
# These are boring links that have already been logged somehow.
self._logged_links: Set[Link] = set()
self._logged_links: set[Link] = set()
# Don't include an allow_yanked default value to make sure each call
# site considers whether yanked releases are allowed. This also causes
@ -621,10 +625,10 @@ class PackageFinder:
cls,
link_collector: LinkCollector,
selection_prefs: SelectionPreferences,
target_python: Optional[TargetPython] = None,
target_python: TargetPython | None = None,
*,
use_deprecated_html5lib: bool,
) -> "PackageFinder":
) -> PackageFinder:
"""Create a PackageFinder.
:param selection_prefs: The candidate selection preferences, as a
@ -664,11 +668,11 @@ 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
@ -703,13 +707,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)
@ -723,12 +727,12 @@ class PackageFinder:
if link not in self._logged_links:
# Put the link at the end so the reason is more visible and because
# the link string is usually very long.
logger.debug("Skipping link: %s: %s", reason, link)
logger.debug('Skipping link: %s: %s', reason, link)
self._logged_links.add(link)
def get_install_candidate(
self, link_evaluator: LinkEvaluator, link: Link
) -> Optional[InstallationCandidate]:
self, link_evaluator: LinkEvaluator, link: Link,
) -> InstallationCandidate | None:
"""
If the link is a candidate for install, convert it to an
InstallationCandidate and return it. Otherwise, return None.
@ -746,8 +750,8 @@ class PackageFinder:
)
def evaluate_links(
self, link_evaluator: LinkEvaluator, links: Iterable[Link]
) -> List[InstallationCandidate]:
self, link_evaluator: LinkEvaluator, links: Iterable[Link],
) -> list[InstallationCandidate]:
"""
Convert links that are candidates to InstallationCandidate objects.
"""
@ -760,10 +764,10 @@ class PackageFinder:
return candidates
def process_project_url(
self, project_url: Link, link_evaluator: LinkEvaluator
) -> List[InstallationCandidate]:
self, project_url: Link, link_evaluator: LinkEvaluator,
) -> list[InstallationCandidate]:
logger.debug(
"Fetching project page and analyzing links: %s",
'Fetching project page and analyzing links: %s',
project_url,
)
html_page = self._link_collector.fetch_page(project_url)
@ -781,7 +785,7 @@ class PackageFinder:
return package_links
@functools.lru_cache(maxsize=None)
def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]:
def find_all_candidates(self, project_name: str) -> list[InstallationCandidate]:
"""Find all available InstallationCandidate for project_name
This checks index_urls and find_links.
@ -828,7 +832,7 @@ class PackageFinder:
except Exception:
paths.append(candidate.link.url) # it's not a local file
logger.debug("Local files found: %s", ", ".join(paths))
logger.debug('Local files found: %s', ', '.join(paths))
# This is an intentional priority ordering
return file_candidates + page_candidates
@ -836,8 +840,8 @@ class PackageFinder:
def make_candidate_evaluator(
self,
project_name: str,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> CandidateEvaluator:
"""Create a CandidateEvaluator object to use."""
candidate_prefs = self._candidate_prefs
@ -854,8 +858,8 @@ class PackageFinder:
def find_best_candidate(
self,
project_name: str,
specifier: Optional[specifiers.BaseSpecifier] = None,
hashes: Optional[Hashes] = None,
specifier: specifiers.BaseSpecifier | None = None,
hashes: Hashes | None = None,
) -> BestCandidateResult:
"""Find matches for the given project and specifier.
@ -874,8 +878,8 @@ class PackageFinder:
return candidate_evaluator.compute_best_candidate(candidates)
def find_requirement(
self, req: InstallRequirement, upgrade: bool
) -> Optional[InstallationCandidate]:
self, req: InstallRequirement, upgrade: bool,
) -> InstallationCandidate | None:
"""Try to find a Link matching req
Expects req, an InstallRequirement and upgrade, a boolean
@ -890,7 +894,7 @@ class PackageFinder:
)
best_candidate = best_candidate_result.best_candidate
installed_version: Optional[_BaseVersion] = None
installed_version: _BaseVersion | None = None
if req.satisfied_by is not None:
installed_version = req.satisfied_by.version
@ -900,25 +904,25 @@ class PackageFinder:
# If we stop using the pkg_resources provided specifier and start
# using our own, we can drop the cast to str().
return (
", ".join(
', '.join(
sorted(
{str(c.version) for c in cand_iter},
key=parse_version,
)
)
or "none"
),
) or
'none'
)
if installed_version is None and best_candidate is None:
logger.critical(
"Could not find a version that satisfies the requirement %s "
"(from versions: %s)",
'Could not find a version that satisfies the requirement %s '
'(from versions: %s)',
req,
_format_versions(best_candidate_result.iter_all()),
)
raise DistributionNotFound(
"No matching distribution found for {}".format(req)
f'No matching distribution found for {req}',
)
best_installed = False
@ -930,14 +934,14 @@ class PackageFinder:
if not upgrade and installed_version is not None:
if best_installed:
logger.debug(
"Existing installed version (%s) is most up-to-date and "
"satisfies requirement",
'Existing installed version (%s) is most up-to-date and '
'satisfies requirement',
installed_version,
)
else:
logger.debug(
"Existing installed version (%s) satisfies requirement "
"(most up-to-date version is %s)",
'Existing installed version (%s) satisfies requirement '
'(most up-to-date version is %s)',
installed_version,
best_candidate.version,
)
@ -946,14 +950,14 @@ class PackageFinder:
if best_installed:
# We have an existing version, and its the best version
logger.debug(
"Installed version (%s) is most up-to-date (past versions: %s)",
'Installed version (%s) is most up-to-date (past versions: %s)',
installed_version,
_format_versions(best_candidate_result.iter_applicable()),
)
raise BestVersionAlreadyInstalled
logger.debug(
"Using version %s (newest of versions: %s)",
'Using version %s (newest of versions: %s)',
best_candidate.version,
_format_versions(best_candidate_result.iter_applicable()),
)
@ -979,14 +983,14 @@ def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
# occurrences of dashes; if the string in front of it matches the canonical
# name, this is the one separating the name and version parts.
for i, c in enumerate(fragment):
if c != "-":
if c != '-':
continue
if canonicalize_name(fragment[:i]) == canonical_name:
return i
raise ValueError(f"{fragment} does not match {canonical_name}")
raise ValueError(f'{fragment} does not match {canonical_name}')
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> Optional[str]:
def _extract_version_from_fragment(fragment: str, canonical_name: str) -> str | None:
"""Parse the version string from a <package>+<version> filename
"fragment" (stem) or egg fragment.

View file

@ -1,12 +1,18 @@
from __future__ import annotations
import logging
import mimetypes
import os
import pathlib
from typing import Callable, Iterable, Optional, Tuple
from typing import Callable
from typing import Iterable
from typing import Optional
from typing import Tuple
from pip._internal.models.candidate import InstallationCandidate
from pip._internal.models.link import Link
from pip._internal.utils.urls import path_to_url, url_to_path
from pip._internal.utils.urls import path_to_url
from pip._internal.utils.urls import url_to_path
from pip._internal.vcs import is_url
logger = logging.getLogger(__name__)
@ -19,7 +25,7 @@ PageValidator = Callable[[Link], bool]
class LinkSource:
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
"""Returns the underlying link, if there's one."""
raise NotImplementedError()
@ -33,7 +39,7 @@ class LinkSource:
def _is_html_file(file_url: str) -> bool:
return mimetypes.guess_type(file_url, strict=False)[0] == "text/html"
return mimetypes.guess_type(file_url, strict=False)[0] == 'text/html'
class _FlatDirectorySource(LinkSource):
@ -54,7 +60,7 @@ class _FlatDirectorySource(LinkSource):
self._path = pathlib.Path(os.path.realpath(path))
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return None
def page_candidates(self) -> FoundCandidates:
@ -91,7 +97,7 @@ class _LocalFileSource(LinkSource):
self._link = link
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -125,7 +131,7 @@ class _RemoteFileSource(LinkSource):
self._link = link
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -153,7 +159,7 @@ class _IndexDirectorySource(LinkSource):
self._link = link
@property
def link(self) -> Optional[Link]:
def link(self) -> Link | None:
return self._link
def page_candidates(self) -> FoundCandidates:
@ -170,14 +176,14 @@ def build_source(
page_validator: PageValidator,
expand_dir: bool,
cache_link_parsing: bool,
) -> Tuple[Optional[str], Optional[LinkSource]]:
) -> tuple[str | None, LinkSource | None]:
path: Optional[str] = None
url: Optional[str] = None
path: str | None = None
url: str | None = None
if os.path.exists(location): # Is a local path.
url = path_to_url(location)
path = location
elif location.startswith("file:"): # A file: URL.
elif location.startswith('file:'): # A file: URL.
url = location
path = url_to_path(location)
elif is_url(location):
@ -186,7 +192,7 @@ def build_source(
if url is None:
msg = (
"Location '%s' is ignored: "
"it is either a non-existing path or lacks a specific scheme."
'it is either a non-existing path or lacks a specific scheme.'
)
logger.warning(msg, location)
return (None, None)