AutoFramework/venv/Lib/site-packages/pip/_internal/models/link.py

289 lines
9.6 KiB
Python
Raw Normal View History

2022-07-21 08:44:10 +08:00
import functools
import logging
2022-07-17 01:48:29 +08:00
import os
import posixpath
import re
import urllib.parse
2022-07-21 08:44:10 +08:00
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Tuple, Union
2022-07-17 01:48:29 +08:00
from pip._internal.utils.filetypes import WHEEL_EXTENSION
from pip._internal.utils.hashes import Hashes
from pip._internal.utils.misc import (
redact_auth_from_url,
split_auth_from_netloc,
splitext,
)
from pip._internal.utils.models import KeyBasedCompareMixin
from pip._internal.utils.urls import path_to_url, url_to_path
if TYPE_CHECKING:
from pip._internal.index.collector import HTMLPage
2022-07-21 08:44:10 +08:00
logger = logging.getLogger(__name__)
_SUPPORTED_HASHES = ("sha1", "sha224", "sha384", "sha256", "sha512", "md5")
2022-07-17 01:48:29 +08:00
class Link(KeyBasedCompareMixin):
2022-07-21 08:44:10 +08:00
"""Represents a parsed link from a Package Index's simple URL"""
2022-07-17 01:48:29 +08:00
__slots__ = [
"_parsed_url",
"_url",
"comes_from",
"requires_python",
"yanked_reason",
"cache_link_parsing",
]
def __init__(
self,
2022-07-21 08:44:10 +08:00
url: str,
comes_from: Optional[Union[str, "HTMLPage"]] = None,
requires_python: Optional[str] = None,
yanked_reason: Optional[str] = None,
cache_link_parsing: bool = True,
) -> None:
2022-07-17 01:48:29 +08:00
"""
:param url: url of the resource pointed to (href of the link)
:param comes_from: instance of HTMLPage where the link was found,
or string.
:param requires_python: String containing the `Requires-Python`
metadata field, specified in PEP 345. This may be specified by
a data-requires-python attribute in the HTML link tag, as
described in PEP 503.
:param yanked_reason: the reason the file has been yanked, if the
file has been yanked, or None if the file hasn't been yanked.
This is the value of the "data-yanked" attribute, if present, in
a simple repository HTML link. If the file has been yanked but
no reason was provided, this should be the empty string. See
PEP 592 for more information and the specification.
:param cache_link_parsing: A flag that is used elsewhere to determine
whether resources retrieved from this link
should be cached. PyPI index urls should
generally have this set to False, for
example.
"""
# url can be a UNC windows share
2022-07-21 08:44:10 +08:00
if url.startswith("\\\\"):
2022-07-17 01:48:29 +08:00
url = path_to_url(url)
self._parsed_url = urllib.parse.urlsplit(url)
# Store the url as a private attribute to prevent accidentally
# trying to set a new value.
self._url = url
self.comes_from = comes_from
self.requires_python = requires_python if requires_python else None
self.yanked_reason = yanked_reason
super().__init__(key=url, defining_class=Link)
self.cache_link_parsing = cache_link_parsing
2022-07-21 08:44:10 +08:00
def __str__(self) -> str:
2022-07-17 01:48:29 +08:00
if self.requires_python:
2022-07-21 08:44:10 +08:00
rp = f" (requires-python:{self.requires_python})"
2022-07-17 01:48:29 +08:00
else:
2022-07-21 08:44:10 +08:00
rp = ""
2022-07-17 01:48:29 +08:00
if self.comes_from:
2022-07-21 08:44:10 +08:00
return "{} (from {}){}".format(
redact_auth_from_url(self._url), self.comes_from, rp
)
2022-07-17 01:48:29 +08:00
else:
return redact_auth_from_url(str(self._url))
2022-07-21 08:44:10 +08:00
def __repr__(self) -> str:
return f"<Link {self}>"
2022-07-17 01:48:29 +08:00
@property
2022-07-21 08:44:10 +08:00
def url(self) -> str:
2022-07-17 01:48:29 +08:00
return self._url
@property
2022-07-21 08:44:10 +08:00
def filename(self) -> str:
path = self.path.rstrip("/")
2022-07-17 01:48:29 +08:00
name = posixpath.basename(path)
if not name:
# Make sure we don't leak auth information if the netloc
# includes a username and password.
netloc, user_pass = split_auth_from_netloc(self.netloc)
return netloc
name = urllib.parse.unquote(name)
2022-07-21 08:44:10 +08:00
assert name, f"URL {self._url!r} produced no filename"
2022-07-17 01:48:29 +08:00
return name
@property
2022-07-21 08:44:10 +08:00
def file_path(self) -> str:
2022-07-17 01:48:29 +08:00
return url_to_path(self.url)
@property
2022-07-21 08:44:10 +08:00
def scheme(self) -> str:
2022-07-17 01:48:29 +08:00
return self._parsed_url.scheme
@property
2022-07-21 08:44:10 +08:00
def netloc(self) -> str:
2022-07-17 01:48:29 +08:00
"""
This can contain auth information.
"""
return self._parsed_url.netloc
@property
2022-07-21 08:44:10 +08:00
def path(self) -> str:
2022-07-17 01:48:29 +08:00
return urllib.parse.unquote(self._parsed_url.path)
2022-07-21 08:44:10 +08:00
def splitext(self) -> Tuple[str, str]:
return splitext(posixpath.basename(self.path.rstrip("/")))
2022-07-17 01:48:29 +08:00
@property
2022-07-21 08:44:10 +08:00
def ext(self) -> str:
2022-07-17 01:48:29 +08:00
return self.splitext()[1]
@property
2022-07-21 08:44:10 +08:00
def url_without_fragment(self) -> str:
2022-07-17 01:48:29 +08:00
scheme, netloc, path, query, fragment = self._parsed_url
2022-07-21 08:44:10 +08:00
return urllib.parse.urlunsplit((scheme, netloc, path, query, ""))
2022-07-17 01:48:29 +08:00
2022-07-21 08:44:10 +08:00
_egg_fragment_re = re.compile(r"[#&]egg=([^&]*)")
2022-07-17 01:48:29 +08:00
@property
2022-07-21 08:44:10 +08:00
def egg_fragment(self) -> Optional[str]:
2022-07-17 01:48:29 +08:00
match = self._egg_fragment_re.search(self._url)
if not match:
return None
return match.group(1)
2022-07-21 08:44:10 +08:00
_subdirectory_fragment_re = re.compile(r"[#&]subdirectory=([^&]*)")
2022-07-17 01:48:29 +08:00
@property
2022-07-21 08:44:10 +08:00
def subdirectory_fragment(self) -> Optional[str]:
2022-07-17 01:48:29 +08:00
match = self._subdirectory_fragment_re.search(self._url)
if not match:
return None
return match.group(1)
_hash_re = re.compile(
2022-07-21 08:44:10 +08:00
r"({choices})=([a-f0-9]+)".format(choices="|".join(_SUPPORTED_HASHES))
2022-07-17 01:48:29 +08:00
)
@property
2022-07-21 08:44:10 +08:00
def hash(self) -> Optional[str]:
2022-07-17 01:48:29 +08:00
match = self._hash_re.search(self._url)
if match:
return match.group(2)
return None
@property
2022-07-21 08:44:10 +08:00
def hash_name(self) -> Optional[str]:
2022-07-17 01:48:29 +08:00
match = self._hash_re.search(self._url)
if match:
return match.group(1)
return None
@property
2022-07-21 08:44:10 +08:00
def show_url(self) -> str:
return posixpath.basename(self._url.split("#", 1)[0].split("?", 1)[0])
2022-07-17 01:48:29 +08:00
@property
2022-07-21 08:44:10 +08:00
def is_file(self) -> bool:
return self.scheme == "file"
2022-07-17 01:48:29 +08:00
2022-07-21 08:44:10 +08:00
def is_existing_dir(self) -> bool:
2022-07-17 01:48:29 +08:00
return self.is_file and os.path.isdir(self.file_path)
@property
2022-07-21 08:44:10 +08:00
def is_wheel(self) -> bool:
2022-07-17 01:48:29 +08:00
return self.ext == WHEEL_EXTENSION
@property
2022-07-21 08:44:10 +08:00
def is_vcs(self) -> bool:
2022-07-17 01:48:29 +08:00
from pip._internal.vcs import vcs
return self.scheme in vcs.all_schemes
@property
2022-07-21 08:44:10 +08:00
def is_yanked(self) -> bool:
2022-07-17 01:48:29 +08:00
return self.yanked_reason is not None
@property
2022-07-21 08:44:10 +08:00
def has_hash(self) -> bool:
2022-07-17 01:48:29 +08:00
return self.hash_name is not None
2022-07-21 08:44:10 +08:00
def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
2022-07-17 01:48:29 +08:00
"""
Return True if the link has a hash and it is allowed.
"""
if hashes is None or not self.has_hash:
return False
# Assert non-None so mypy knows self.hash_name and self.hash are str.
assert self.hash_name is not None
assert self.hash is not None
return hashes.is_hash_allowed(self.hash_name, hex_digest=self.hash)
2022-07-21 08:44:10 +08:00
class _CleanResult(NamedTuple):
"""Convert link for equivalency check.
This is used in the resolver to check whether two URL-specified requirements
likely point to the same distribution and can be considered equivalent. This
equivalency logic avoids comparing URLs literally, which can be too strict
(e.g. "a=1&b=2" vs "b=2&a=1") and produce conflicts unexpecting to users.
Currently this does three things:
1. Drop the basic auth part. This is technically wrong since a server can
serve different content based on auth, but if it does that, it is even
impossible to guarantee two URLs without auth are equivalent, since
the user can input different auth information when prompted. So the
practical solution is to assume the auth doesn't affect the response.
2. Parse the query to avoid the ordering issue. Note that ordering under the
same key in the query are NOT cleaned; i.e. "a=1&a=2" and "a=2&a=1" are
still considered different.
3. Explicitly drop most of the fragment part, except ``subdirectory=`` and
hash values, since it should have no impact the downloaded content. Note
that this drops the "egg=" part historically used to denote the requested
project (and extras), which is wrong in the strictest sense, but too many
people are supplying it inconsistently to cause superfluous resolution
conflicts, so we choose to also ignore them.
"""
parsed: urllib.parse.SplitResult
query: Dict[str, List[str]]
subdirectory: str
hashes: Dict[str, str]
def _clean_link(link: Link) -> _CleanResult:
parsed = link._parsed_url
netloc = parsed.netloc.rsplit("@", 1)[-1]
# According to RFC 8089, an empty host in file: means localhost.
if parsed.scheme == "file" and not netloc:
netloc = "localhost"
fragment = urllib.parse.parse_qs(parsed.fragment)
if "egg" in fragment:
logger.debug("Ignoring egg= fragment in %s", link)
try:
# If there are multiple subdirectory values, use the first one.
# This matches the behavior of Link.subdirectory_fragment.
subdirectory = fragment["subdirectory"][0]
except (IndexError, KeyError):
subdirectory = ""
# If there are multiple hash values under the same algorithm, use the
# first one. This matches the behavior of Link.hash_value.
hashes = {k: fragment[k][0] for k in _SUPPORTED_HASHES if k in fragment}
return _CleanResult(
parsed=parsed._replace(netloc=netloc, query="", fragment=""),
query=urllib.parse.parse_qs(parsed.query),
subdirectory=subdirectory,
hashes=hashes,
)
@functools.lru_cache(maxsize=None)
def links_equivalent(link1: Link, link2: Link) -> bool:
return _clean_link(link1) == _clean_link(link2)