Maj pip
This commit is contained in:
@@ -3,9 +3,18 @@
|
||||
Contains interface (MultiDomainBasicAuth) and associated glue code for
|
||||
providing credentials in the context of network requests.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sysconfig
|
||||
import typing
|
||||
import urllib.parse
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from abc import ABC, abstractmethod
|
||||
from functools import lru_cache
|
||||
from os.path import commonprefix
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
|
||||
|
||||
from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth
|
||||
from pip._vendor.requests.models import Request, Response
|
||||
@@ -23,59 +32,204 @@ from pip._internal.vcs.versioncontrol import AuthInfo
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
Credentials = Tuple[str, str, str]
|
||||
|
||||
try:
|
||||
import keyring
|
||||
except ImportError:
|
||||
keyring = None
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Keyring is skipped due to an exception: %s",
|
||||
str(exc),
|
||||
)
|
||||
keyring = None
|
||||
KEYRING_DISABLED = False
|
||||
|
||||
|
||||
def get_keyring_auth(url: Optional[str], username: Optional[str]) -> Optional[AuthInfo]:
|
||||
"""Return the tuple auth for a given url from keyring."""
|
||||
global keyring
|
||||
if not url or not keyring:
|
||||
class Credentials(NamedTuple):
|
||||
url: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
|
||||
class KeyRingBaseProvider(ABC):
|
||||
"""Keyring base provider interface"""
|
||||
|
||||
has_keyring: bool
|
||||
|
||||
@abstractmethod
|
||||
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
||||
...
|
||||
|
||||
|
||||
class KeyRingNullProvider(KeyRingBaseProvider):
|
||||
"""Keyring null provider"""
|
||||
|
||||
has_keyring = False
|
||||
|
||||
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
||||
return None
|
||||
|
||||
try:
|
||||
try:
|
||||
get_credential = keyring.get_credential
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class KeyRingPythonProvider(KeyRingBaseProvider):
|
||||
"""Keyring interface which uses locally imported `keyring`"""
|
||||
|
||||
has_keyring = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
import keyring
|
||||
|
||||
self.keyring = keyring
|
||||
|
||||
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
||||
# Support keyring's get_credential interface which supports getting
|
||||
# credentials without a username. This is only available for
|
||||
# keyring>=15.2.0.
|
||||
if hasattr(self.keyring, "get_credential"):
|
||||
logger.debug("Getting credentials from keyring for %s", url)
|
||||
cred = get_credential(url, username)
|
||||
cred = self.keyring.get_credential(url, username)
|
||||
if cred is not None:
|
||||
return cred.username, cred.password
|
||||
return None
|
||||
|
||||
if username:
|
||||
if username is not None:
|
||||
logger.debug("Getting password from keyring for %s", url)
|
||||
password = keyring.get_password(url, username)
|
||||
password = self.keyring.get_password(url, username)
|
||||
if password:
|
||||
return username, password
|
||||
return None
|
||||
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Keyring is skipped due to an exception: %s",
|
||||
str(exc),
|
||||
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
||||
self.keyring.set_password(url, username, password)
|
||||
|
||||
|
||||
class KeyRingCliProvider(KeyRingBaseProvider):
|
||||
"""Provider which uses `keyring` cli
|
||||
|
||||
Instead of calling the keyring package installed alongside pip
|
||||
we call keyring on the command line which will enable pip to
|
||||
use which ever installation of keyring is available first in
|
||||
PATH.
|
||||
"""
|
||||
|
||||
has_keyring = True
|
||||
|
||||
def __init__(self, cmd: str) -> None:
|
||||
self.keyring = cmd
|
||||
|
||||
def get_auth_info(self, url: str, username: Optional[str]) -> Optional[AuthInfo]:
|
||||
# This is the default implementation of keyring.get_credential
|
||||
# https://github.com/jaraco/keyring/blob/97689324abcf01bd1793d49063e7ca01e03d7d07/keyring/backend.py#L134-L139
|
||||
if username is not None:
|
||||
password = self._get_password(url, username)
|
||||
if password is not None:
|
||||
return username, password
|
||||
return None
|
||||
|
||||
def save_auth_info(self, url: str, username: str, password: str) -> None:
|
||||
return self._set_password(url, username, password)
|
||||
|
||||
def _get_password(self, service_name: str, username: str) -> Optional[str]:
|
||||
"""Mirror the implementation of keyring.get_password using cli"""
|
||||
if self.keyring is None:
|
||||
return None
|
||||
|
||||
cmd = [self.keyring, "get", service_name, username]
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
res = subprocess.run(
|
||||
cmd,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
env=env,
|
||||
)
|
||||
keyring = None
|
||||
return None
|
||||
if res.returncode:
|
||||
return None
|
||||
return res.stdout.decode("utf-8").strip(os.linesep)
|
||||
|
||||
def _set_password(self, service_name: str, username: str, password: str) -> None:
|
||||
"""Mirror the implementation of keyring.set_password using cli"""
|
||||
if self.keyring is None:
|
||||
return None
|
||||
env = os.environ.copy()
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
subprocess.run(
|
||||
[self.keyring, "set", service_name, username],
|
||||
input=f"{password}{os.linesep}".encode("utf-8"),
|
||||
env=env,
|
||||
check=True,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_keyring_provider(provider: str) -> KeyRingBaseProvider:
|
||||
logger.verbose("Keyring provider requested: %s", provider)
|
||||
|
||||
# keyring has previously failed and been disabled
|
||||
if KEYRING_DISABLED:
|
||||
provider = "disabled"
|
||||
if provider in ["import", "auto"]:
|
||||
try:
|
||||
impl = KeyRingPythonProvider()
|
||||
logger.verbose("Keyring provider set: import")
|
||||
return impl
|
||||
except ImportError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
# In the event of an unexpected exception
|
||||
# we should warn the user
|
||||
msg = "Installed copy of keyring fails with exception %s"
|
||||
if provider == "auto":
|
||||
msg = msg + ", trying to find a keyring executable as a fallback"
|
||||
logger.warning(msg, exc, exc_info=logger.isEnabledFor(logging.DEBUG))
|
||||
if provider in ["subprocess", "auto"]:
|
||||
cli = shutil.which("keyring")
|
||||
if cli and cli.startswith(sysconfig.get_path("scripts")):
|
||||
# all code within this function is stolen from shutil.which implementation
|
||||
@typing.no_type_check
|
||||
def PATH_as_shutil_which_determines_it() -> str:
|
||||
path = os.environ.get("PATH", None)
|
||||
if path is None:
|
||||
try:
|
||||
path = os.confstr("CS_PATH")
|
||||
except (AttributeError, ValueError):
|
||||
# os.confstr() or CS_PATH is not available
|
||||
path = os.defpath
|
||||
# bpo-35755: Don't use os.defpath if the PATH environment variable is
|
||||
# set to an empty string
|
||||
|
||||
return path
|
||||
|
||||
scripts = Path(sysconfig.get_path("scripts"))
|
||||
|
||||
paths = []
|
||||
for path in PATH_as_shutil_which_determines_it().split(os.pathsep):
|
||||
p = Path(path)
|
||||
try:
|
||||
if not p.samefile(scripts):
|
||||
paths.append(path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
path = os.pathsep.join(paths)
|
||||
|
||||
cli = shutil.which("keyring", path=path)
|
||||
|
||||
if cli:
|
||||
logger.verbose("Keyring provider set: subprocess with executable %s", cli)
|
||||
return KeyRingCliProvider(cli)
|
||||
|
||||
logger.verbose("Keyring provider set: disabled")
|
||||
return KeyRingNullProvider()
|
||||
|
||||
|
||||
class MultiDomainBasicAuth(AuthBase):
|
||||
def __init__(
|
||||
self, prompting: bool = True, index_urls: Optional[List[str]] = None
|
||||
self,
|
||||
prompting: bool = True,
|
||||
index_urls: Optional[List[str]] = None,
|
||||
keyring_provider: str = "auto",
|
||||
) -> None:
|
||||
self.prompting = prompting
|
||||
self.index_urls = index_urls
|
||||
self.keyring_provider = keyring_provider # type: ignore[assignment]
|
||||
self.passwords: Dict[str, AuthInfo] = {}
|
||||
# When the user is prompted to enter credentials and keyring is
|
||||
# available, we will offer to save them. If the user accepts,
|
||||
@@ -84,6 +238,47 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
# ``save_credentials`` to save these.
|
||||
self._credentials_to_save: Optional[Credentials] = None
|
||||
|
||||
@property
|
||||
def keyring_provider(self) -> KeyRingBaseProvider:
|
||||
return get_keyring_provider(self._keyring_provider)
|
||||
|
||||
@keyring_provider.setter
|
||||
def keyring_provider(self, provider: str) -> None:
|
||||
# The free function get_keyring_provider has been decorated with
|
||||
# functools.cache. If an exception occurs in get_keyring_auth that
|
||||
# cache will be cleared and keyring disabled, take that into account
|
||||
# if you want to remove this indirection.
|
||||
self._keyring_provider = provider
|
||||
|
||||
@property
|
||||
def use_keyring(self) -> bool:
|
||||
# We won't use keyring when --no-input is passed unless
|
||||
# a specific provider is requested because it might require
|
||||
# user interaction
|
||||
return self.prompting or self._keyring_provider not in ["auto", "disabled"]
|
||||
|
||||
def _get_keyring_auth(
|
||||
self,
|
||||
url: Optional[str],
|
||||
username: Optional[str],
|
||||
) -> Optional[AuthInfo]:
|
||||
"""Return the tuple auth for a given url from keyring."""
|
||||
# Do nothing if no url was provided
|
||||
if not url:
|
||||
return None
|
||||
|
||||
try:
|
||||
return self.keyring_provider.get_auth_info(url, username)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Keyring is skipped due to an exception: %s",
|
||||
str(exc),
|
||||
)
|
||||
global KEYRING_DISABLED
|
||||
KEYRING_DISABLED = True
|
||||
get_keyring_provider.cache_clear()
|
||||
return None
|
||||
|
||||
def _get_index_url(self, url: str) -> Optional[str]:
|
||||
"""Return the original index URL matching the requested URL.
|
||||
|
||||
@@ -100,15 +295,42 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
if not url or not self.index_urls:
|
||||
return None
|
||||
|
||||
for u in self.index_urls:
|
||||
prefix = remove_auth_from_url(u).rstrip("/") + "/"
|
||||
if url.startswith(prefix):
|
||||
return u
|
||||
return None
|
||||
url = remove_auth_from_url(url).rstrip("/") + "/"
|
||||
parsed_url = urllib.parse.urlsplit(url)
|
||||
|
||||
candidates = []
|
||||
|
||||
for index in self.index_urls:
|
||||
index = index.rstrip("/") + "/"
|
||||
parsed_index = urllib.parse.urlsplit(remove_auth_from_url(index))
|
||||
if parsed_url == parsed_index:
|
||||
return index
|
||||
|
||||
if parsed_url.netloc != parsed_index.netloc:
|
||||
continue
|
||||
|
||||
candidate = urllib.parse.urlsplit(index)
|
||||
candidates.append(candidate)
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
candidates.sort(
|
||||
reverse=True,
|
||||
key=lambda candidate: commonprefix(
|
||||
[
|
||||
parsed_url.path,
|
||||
candidate.path,
|
||||
]
|
||||
).rfind("/"),
|
||||
)
|
||||
|
||||
return urllib.parse.urlunsplit(candidates[0])
|
||||
|
||||
def _get_new_credentials(
|
||||
self,
|
||||
original_url: str,
|
||||
*,
|
||||
allow_netrc: bool = True,
|
||||
allow_keyring: bool = False,
|
||||
) -> AuthInfo:
|
||||
@@ -152,8 +374,8 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
# The index url is more specific than the netloc, so try it first
|
||||
# fmt: off
|
||||
kr_auth = (
|
||||
get_keyring_auth(index_url, username) or
|
||||
get_keyring_auth(netloc, username)
|
||||
self._get_keyring_auth(index_url, username) or
|
||||
self._get_keyring_auth(netloc, username)
|
||||
)
|
||||
# fmt: on
|
||||
if kr_auth:
|
||||
@@ -179,9 +401,16 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
# Try to get credentials from original url
|
||||
username, password = self._get_new_credentials(original_url)
|
||||
|
||||
# If credentials not found, use any stored credentials for this netloc
|
||||
if username is None and password is None:
|
||||
username, password = self.passwords.get(netloc, (None, None))
|
||||
# If credentials not found, use any stored credentials for this netloc.
|
||||
# Do this if either the username or the password is missing.
|
||||
# This accounts for the situation in which the user has specified
|
||||
# the username in the index url, but the password comes from keyring.
|
||||
if (username is None or password is None) and netloc in self.passwords:
|
||||
un, pw = self.passwords[netloc]
|
||||
# It is possible that the cached credentials are for a different username,
|
||||
# in which case the cache should be ignored.
|
||||
if username is None or username == un:
|
||||
username, password = un, pw
|
||||
|
||||
if username is not None or password is not None:
|
||||
# Convert the username and password if they're None, so that
|
||||
@@ -223,18 +452,23 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
def _prompt_for_password(
|
||||
self, netloc: str
|
||||
) -> Tuple[Optional[str], Optional[str], bool]:
|
||||
username = ask_input(f"User for {netloc}: ")
|
||||
username = ask_input(f"User for {netloc}: ") if self.prompting else None
|
||||
if not username:
|
||||
return None, None, False
|
||||
auth = get_keyring_auth(netloc, username)
|
||||
if auth and auth[0] is not None and auth[1] is not None:
|
||||
return auth[0], auth[1], False
|
||||
if self.use_keyring:
|
||||
auth = self._get_keyring_auth(netloc, username)
|
||||
if auth and auth[0] is not None and auth[1] is not None:
|
||||
return auth[0], auth[1], False
|
||||
password = ask_password("Password: ")
|
||||
return username, password, True
|
||||
|
||||
# Factored out to allow for easy patching in tests
|
||||
def _should_save_password_to_keyring(self) -> bool:
|
||||
if not keyring:
|
||||
if (
|
||||
not self.prompting
|
||||
or not self.use_keyring
|
||||
or not self.keyring_provider.has_keyring
|
||||
):
|
||||
return False
|
||||
return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y"
|
||||
|
||||
@@ -244,19 +478,22 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
if resp.status_code != 401:
|
||||
return resp
|
||||
|
||||
username, password = None, None
|
||||
|
||||
# Query the keyring for credentials:
|
||||
if self.use_keyring:
|
||||
username, password = self._get_new_credentials(
|
||||
resp.url,
|
||||
allow_netrc=False,
|
||||
allow_keyring=True,
|
||||
)
|
||||
|
||||
# We are not able to prompt the user so simply return the response
|
||||
if not self.prompting:
|
||||
if not self.prompting and not username and not password:
|
||||
return resp
|
||||
|
||||
parsed = urllib.parse.urlparse(resp.url)
|
||||
|
||||
# Query the keyring for credentials:
|
||||
username, password = self._get_new_credentials(
|
||||
resp.url,
|
||||
allow_netrc=False,
|
||||
allow_keyring=True,
|
||||
)
|
||||
|
||||
# Prompt the user for a new username and password
|
||||
save = False
|
||||
if not username and not password:
|
||||
@@ -269,7 +506,11 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
|
||||
# Prompt to save the password to keyring
|
||||
if save and self._should_save_password_to_keyring():
|
||||
self._credentials_to_save = (parsed.netloc, username, password)
|
||||
self._credentials_to_save = Credentials(
|
||||
url=parsed.netloc,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
|
||||
# Consume content and release the original connection to allow our new
|
||||
# request to reuse the same one.
|
||||
@@ -302,15 +543,17 @@ class MultiDomainBasicAuth(AuthBase):
|
||||
|
||||
def save_credentials(self, resp: Response, **kwargs: Any) -> None:
|
||||
"""Response callback to save credentials on success."""
|
||||
assert keyring is not None, "should never reach here without keyring"
|
||||
if not keyring:
|
||||
return
|
||||
assert (
|
||||
self.keyring_provider.has_keyring
|
||||
), "should never reach here without keyring"
|
||||
|
||||
creds = self._credentials_to_save
|
||||
self._credentials_to_save = None
|
||||
if creds and resp.status_code < 400:
|
||||
try:
|
||||
logger.info("Saving credentials to keyring")
|
||||
keyring.set_password(*creds)
|
||||
self.keyring_provider.save_auth_info(
|
||||
creds.url, creds.username, creds.password
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Failed to save credentials")
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator, Optional
|
||||
from typing import Generator, Optional
|
||||
|
||||
from pip._vendor.cachecontrol.cache import BaseCache
|
||||
from pip._vendor.cachecontrol.caches import FileCache
|
||||
@@ -18,7 +18,7 @@ def is_from_cache(response: Response) -> bool:
|
||||
|
||||
|
||||
@contextmanager
|
||||
def suppressed_cache_errors() -> Iterator[None]:
|
||||
def suppressed_cache_errors() -> Generator[None, None, None]:
|
||||
"""If we can't access the cache then we can just skip caching and process
|
||||
requests as if caching wasn't enabled.
|
||||
"""
|
||||
@@ -53,7 +53,7 @@ class SafeFileCache(BaseCache):
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
def set(self, key: str, value: bytes) -> None:
|
||||
def set(self, key: str, value: bytes, expires: Optional[int] = None) -> None:
|
||||
path = self._get_cache_path(key)
|
||||
with suppressed_cache_errors():
|
||||
ensure_dir(os.path.dirname(path))
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""Download files with progress indicators.
|
||||
"""
|
||||
import cgi
|
||||
import email.message
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
@@ -8,7 +8,7 @@ from typing import Iterable, Optional, Tuple
|
||||
|
||||
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
||||
|
||||
from pip._internal.cli.progress_bars import DownloadProgressProvider
|
||||
from pip._internal.cli.progress_bars import get_download_progress_renderer
|
||||
from pip._internal.exceptions import NetworkConnectionError
|
||||
from pip._internal.models.index import PyPI
|
||||
from pip._internal.models.link import Link
|
||||
@@ -65,7 +65,8 @@ def _prepare_download(
|
||||
if not show_progress:
|
||||
return chunks
|
||||
|
||||
return DownloadProgressProvider(progress_bar, max=total_length)(chunks)
|
||||
renderer = get_download_progress_renderer(bar_type=progress_bar, size=total_length)
|
||||
return renderer(chunks)
|
||||
|
||||
|
||||
def sanitize_content_filename(filename: str) -> str:
|
||||
@@ -80,12 +81,13 @@ def parse_content_disposition(content_disposition: str, default_filename: str) -
|
||||
Parse the "filename" value from a Content-Disposition header, and
|
||||
return the default filename if the result is empty.
|
||||
"""
|
||||
_type, params = cgi.parse_header(content_disposition)
|
||||
filename = params.get("filename")
|
||||
m = email.message.Message()
|
||||
m["content-type"] = content_disposition
|
||||
filename = m.get_param("filename")
|
||||
if filename:
|
||||
# We need to sanitize the filename to prevent directory traversal
|
||||
# in case the filename contains ".." path parts.
|
||||
filename = sanitize_content_filename(filename)
|
||||
filename = sanitize_content_filename(str(filename))
|
||||
return filename or default_filename
|
||||
|
||||
|
||||
|
@@ -5,36 +5,36 @@ __all__ = ["HTTPRangeRequestUnsupported", "dist_from_wheel_url"]
|
||||
from bisect import bisect_left, bisect_right
|
||||
from contextlib import contextmanager
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||
from zipfile import BadZipfile, ZipFile
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
from pip._vendor.pkg_resources import Distribution
|
||||
from pip._vendor.packaging.utils import canonicalize_name
|
||||
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
||||
|
||||
from pip._internal.metadata import BaseDistribution, MemoryWheel, get_wheel_distribution
|
||||
from pip._internal.network.session import PipSession
|
||||
from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks
|
||||
from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
|
||||
|
||||
|
||||
class HTTPRangeRequestUnsupported(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def dist_from_wheel_url(name: str, url: str, session: PipSession) -> Distribution:
|
||||
"""Return a pkg_resources.Distribution from the given wheel URL.
|
||||
def dist_from_wheel_url(name: str, url: str, session: PipSession) -> BaseDistribution:
|
||||
"""Return a distribution object from the given wheel URL.
|
||||
|
||||
This uses HTTP range requests to only fetch the potion of the wheel
|
||||
This uses HTTP range requests to only fetch the portion of the wheel
|
||||
containing metadata, just enough for the object to be constructed.
|
||||
If such requests are not supported, HTTPRangeRequestUnsupported
|
||||
is raised.
|
||||
"""
|
||||
with LazyZipOverHTTP(url, session) as wheel:
|
||||
with LazyZipOverHTTP(url, session) as zf:
|
||||
# For read-only ZIP files, ZipFile only needs methods read,
|
||||
# seek, seekable and tell, not the whole IO protocol.
|
||||
zip_file = ZipFile(wheel) # type: ignore
|
||||
wheel = MemoryWheel(zf.name, zf) # type: ignore
|
||||
# After context manager exit, wheel.name
|
||||
# is an invalid file by intention.
|
||||
return pkg_resources_distribution_for_wheel(zip_file, name, wheel.name)
|
||||
return get_wheel_distribution(wheel, canonicalize_name(name))
|
||||
|
||||
|
||||
class LazyZipOverHTTP:
|
||||
@@ -135,11 +135,11 @@ class LazyZipOverHTTP:
|
||||
self._file.__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc: Any) -> Optional[bool]:
|
||||
return self._file.__exit__(*exc)
|
||||
def __exit__(self, *exc: Any) -> None:
|
||||
self._file.__exit__(*exc)
|
||||
|
||||
@contextmanager
|
||||
def _stay(self) -> Iterator[None]:
|
||||
def _stay(self) -> Generator[None, None, None]:
|
||||
"""Return a context manager keeping the position.
|
||||
|
||||
At the end of the block, seek back to original position.
|
||||
@@ -160,7 +160,7 @@ class LazyZipOverHTTP:
|
||||
# For read-only ZIP files, ZipFile only needs
|
||||
# methods read, seek, seekable and tell.
|
||||
ZipFile(self) # type: ignore
|
||||
except BadZipfile:
|
||||
except BadZipFile:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
@@ -177,8 +177,8 @@ class LazyZipOverHTTP:
|
||||
|
||||
def _merge(
|
||||
self, start: int, end: int, left: int, right: int
|
||||
) -> Iterator[Tuple[int, int]]:
|
||||
"""Return an iterator of intervals to be fetched.
|
||||
) -> Generator[Tuple[int, int], None, None]:
|
||||
"""Return a generator of intervals to be fetched.
|
||||
|
||||
Args:
|
||||
start (int): Start of needed interval
|
||||
|
@@ -2,17 +2,8 @@
|
||||
network request configuration and behavior.
|
||||
"""
|
||||
|
||||
# When mypy runs on Windows the call to distro.linux_distribution() is skipped
|
||||
# resulting in the failure:
|
||||
#
|
||||
# error: unused 'type: ignore' comment
|
||||
#
|
||||
# If the upstream module adds typing, this comment should be removed. See
|
||||
# https://github.com/nir0s/distro/pull/269
|
||||
#
|
||||
# mypy: warn-unused-ignores=False
|
||||
|
||||
import email.utils
|
||||
import io
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
@@ -24,11 +15,23 @@ import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import warnings
|
||||
from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Tuple, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Generator,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from pip._vendor import requests, urllib3
|
||||
from pip._vendor.cachecontrol import CacheControlAdapter
|
||||
from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter
|
||||
from pip._vendor.cachecontrol import CacheControlAdapter as _BaseCacheControlAdapter
|
||||
from pip._vendor.requests.adapters import DEFAULT_POOLBLOCK, BaseAdapter
|
||||
from pip._vendor.requests.adapters import HTTPAdapter as _BaseHTTPAdapter
|
||||
from pip._vendor.requests.models import PreparedRequest, Response
|
||||
from pip._vendor.requests.structures import CaseInsensitiveDict
|
||||
from pip._vendor.urllib3.connectionpool import ConnectionPool
|
||||
@@ -46,6 +49,12 @@ from pip._internal.utils.glibc import libc_ver
|
||||
from pip._internal.utils.misc import build_url_from_netloc, parse_netloc
|
||||
from pip._internal.utils.urls import url_to_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ssl import SSLContext
|
||||
|
||||
from pip._vendor.urllib3.poolmanager import PoolManager
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SecureOrigin = Tuple[str, str, Optional[Union[int, str]]]
|
||||
@@ -128,9 +137,8 @@ def user_agent() -> str:
|
||||
if sys.platform.startswith("linux"):
|
||||
from pip._vendor import distro
|
||||
|
||||
# https://github.com/nir0s/distro/pull/269
|
||||
linux_distribution = distro.linux_distribution() # type: ignore
|
||||
distro_infos = dict(
|
||||
linux_distribution = distro.name(), distro.version(), distro.codename()
|
||||
distro_infos: Dict[str, Any] = dict(
|
||||
filter(
|
||||
lambda x: x[1],
|
||||
zip(["name", "version", "id"], linux_distribution),
|
||||
@@ -218,8 +226,11 @@ class LocalFSAdapter(BaseAdapter):
|
||||
try:
|
||||
stats = os.stat(pathname)
|
||||
except OSError as exc:
|
||||
# format the exception raised as a io.BytesIO object,
|
||||
# to return a better error message:
|
||||
resp.status_code = 404
|
||||
resp.raw = exc
|
||||
resp.reason = type(exc).__name__
|
||||
resp.raw = io.BytesIO(f"{resp.reason}: {exc}".encode("utf8"))
|
||||
else:
|
||||
modified = email.utils.formatdate(stats.st_mtime, usegmt=True)
|
||||
content_type = mimetypes.guess_type(pathname)[0] or "text/plain"
|
||||
@@ -240,6 +251,48 @@ class LocalFSAdapter(BaseAdapter):
|
||||
pass
|
||||
|
||||
|
||||
class _SSLContextAdapterMixin:
|
||||
"""Mixin to add the ``ssl_context`` constructor argument to HTTP adapters.
|
||||
|
||||
The additional argument is forwarded directly to the pool manager. This allows us
|
||||
to dynamically decide what SSL store to use at runtime, which is used to implement
|
||||
the optional ``truststore`` backend.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
ssl_context: Optional["SSLContext"] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self._ssl_context = ssl_context
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_poolmanager(
|
||||
self,
|
||||
connections: int,
|
||||
maxsize: int,
|
||||
block: bool = DEFAULT_POOLBLOCK,
|
||||
**pool_kwargs: Any,
|
||||
) -> "PoolManager":
|
||||
if self._ssl_context is not None:
|
||||
pool_kwargs.setdefault("ssl_context", self._ssl_context)
|
||||
return super().init_poolmanager( # type: ignore[misc]
|
||||
connections=connections,
|
||||
maxsize=maxsize,
|
||||
block=block,
|
||||
**pool_kwargs,
|
||||
)
|
||||
|
||||
|
||||
class HTTPAdapter(_SSLContextAdapterMixin, _BaseHTTPAdapter):
|
||||
pass
|
||||
|
||||
|
||||
class CacheControlAdapter(_SSLContextAdapterMixin, _BaseCacheControlAdapter):
|
||||
pass
|
||||
|
||||
|
||||
class InsecureHTTPAdapter(HTTPAdapter):
|
||||
def cert_verify(
|
||||
self,
|
||||
@@ -263,7 +316,6 @@ class InsecureCacheControlAdapter(CacheControlAdapter):
|
||||
|
||||
|
||||
class PipSession(requests.Session):
|
||||
|
||||
timeout: Optional[int] = None
|
||||
|
||||
def __init__(
|
||||
@@ -273,6 +325,7 @@ class PipSession(requests.Session):
|
||||
cache: Optional[str] = None,
|
||||
trusted_hosts: Sequence[str] = (),
|
||||
index_urls: Optional[List[str]] = None,
|
||||
ssl_context: Optional["SSLContext"] = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
@@ -325,13 +378,14 @@ class PipSession(requests.Session):
|
||||
secure_adapter = CacheControlAdapter(
|
||||
cache=SafeFileCache(cache),
|
||||
max_retries=retries,
|
||||
ssl_context=ssl_context,
|
||||
)
|
||||
self._trusted_host_adapter = InsecureCacheControlAdapter(
|
||||
cache=SafeFileCache(cache),
|
||||
max_retries=retries,
|
||||
)
|
||||
else:
|
||||
secure_adapter = HTTPAdapter(max_retries=retries)
|
||||
secure_adapter = HTTPAdapter(max_retries=retries, ssl_context=ssl_context)
|
||||
self._trusted_host_adapter = insecure_adapter
|
||||
|
||||
self.mount("https://", secure_adapter)
|
||||
@@ -369,12 +423,19 @@ class PipSession(requests.Session):
|
||||
if host_port not in self.pip_trusted_origins:
|
||||
self.pip_trusted_origins.append(host_port)
|
||||
|
||||
self.mount(
|
||||
build_url_from_netloc(host, scheme="http") + "/", self._trusted_host_adapter
|
||||
)
|
||||
self.mount(build_url_from_netloc(host) + "/", self._trusted_host_adapter)
|
||||
if not host_port[1]:
|
||||
self.mount(
|
||||
build_url_from_netloc(host, scheme="http") + ":",
|
||||
self._trusted_host_adapter,
|
||||
)
|
||||
# Mount wildcard ports for the same host.
|
||||
self.mount(build_url_from_netloc(host) + ":", self._trusted_host_adapter)
|
||||
|
||||
def iter_secure_origins(self) -> Iterator[SecureOrigin]:
|
||||
def iter_secure_origins(self) -> Generator[SecureOrigin, None, None]:
|
||||
yield from SECURE_ORIGINS
|
||||
for host, port in self.pip_trusted_origins:
|
||||
yield ("*", host, "*" if port is None else port)
|
||||
@@ -403,7 +464,7 @@ class PipSession(requests.Session):
|
||||
continue
|
||||
|
||||
try:
|
||||
addr = ipaddress.ip_address(origin_host)
|
||||
addr = ipaddress.ip_address(origin_host or "")
|
||||
network = ipaddress.ip_network(secure_host)
|
||||
except ValueError:
|
||||
# We don't have both a valid address or a valid network, so
|
||||
@@ -449,6 +510,8 @@ class PipSession(requests.Session):
|
||||
def request(self, method: str, url: str, *args: Any, **kwargs: Any) -> Response:
|
||||
# Allow setting a default timeout on a session
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
# Allow setting a default proxies on a session
|
||||
kwargs.setdefault("proxies", self.proxies)
|
||||
|
||||
# Dispatch the actual request
|
||||
return super().request(method, url, *args, **kwargs)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, Iterator
|
||||
from typing import Dict, Generator
|
||||
|
||||
from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response
|
||||
|
||||
@@ -56,7 +56,7 @@ def raise_for_status(resp: Response) -> None:
|
||||
|
||||
def response_chunks(
|
||||
response: Response, chunk_size: int = CONTENT_CHUNK_SIZE
|
||||
) -> Iterator[bytes]:
|
||||
) -> Generator[bytes, None, None]:
|
||||
"""Given a requests Response, provide the data chunks."""
|
||||
try:
|
||||
# Special case for urllib3.
|
||||
|
Reference in New Issue
Block a user