Загрузить файлы в «venv/Lib/site-packages/pip/_internal/utils»
This commit is contained in:
88
venv/Lib/site-packages/pip/_internal/utils/entrypoints.py
Normal file
88
venv/Lib/site-packages/pip/_internal/utils/entrypoints.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pip._internal.cli.main import main
|
||||||
|
from pip._internal.utils.compat import WINDOWS
|
||||||
|
|
||||||
|
_EXECUTABLE_NAMES = [
|
||||||
|
"pip",
|
||||||
|
f"pip{sys.version_info.major}",
|
||||||
|
f"pip{sys.version_info.major}.{sys.version_info.minor}",
|
||||||
|
]
|
||||||
|
if WINDOWS:
|
||||||
|
_allowed_extensions = {"", ".exe"}
|
||||||
|
_EXECUTABLE_NAMES = [
|
||||||
|
"".join(parts)
|
||||||
|
for parts in itertools.product(_EXECUTABLE_NAMES, _allowed_extensions)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _wrapper(args: list[str] | None = None) -> int:
|
||||||
|
"""Central wrapper for all old entrypoints.
|
||||||
|
|
||||||
|
Historically pip has had several entrypoints defined. Because of issues
|
||||||
|
arising from PATH, sys.path, multiple Pythons, their interactions, and most
|
||||||
|
of them having a pip installed, users suffer every time an entrypoint gets
|
||||||
|
moved.
|
||||||
|
|
||||||
|
To alleviate this pain, and provide a mechanism for warning users and
|
||||||
|
directing them to an appropriate place for help, we now define all of
|
||||||
|
our old entrypoints as wrappers for the current one.
|
||||||
|
"""
|
||||||
|
sys.stderr.write(
|
||||||
|
"WARNING: pip is being invoked by an old script wrapper. This will "
|
||||||
|
"fail in a future version of pip.\n"
|
||||||
|
"Please see https://github.com/pypa/pip/issues/5599 for advice on "
|
||||||
|
"fixing the underlying issue.\n"
|
||||||
|
"To avoid this problem you can invoke Python with '-m pip' instead of "
|
||||||
|
"running pip directly.\n"
|
||||||
|
)
|
||||||
|
return main(args)
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_invocation_for_this_pip() -> str:
|
||||||
|
"""Try to figure out the best way to invoke pip in the current environment."""
|
||||||
|
binary_directory = "Scripts" if WINDOWS else "bin"
|
||||||
|
binary_prefix = os.path.join(sys.prefix, binary_directory)
|
||||||
|
|
||||||
|
# Try to use pip[X[.Y]] names, if those executables for this environment are
|
||||||
|
# the first on PATH with that name.
|
||||||
|
path_parts = os.path.normcase(os.environ.get("PATH", "")).split(os.pathsep)
|
||||||
|
exe_are_in_PATH = os.path.normcase(binary_prefix) in path_parts
|
||||||
|
if exe_are_in_PATH:
|
||||||
|
for exe_name in _EXECUTABLE_NAMES:
|
||||||
|
found_executable = shutil.which(exe_name)
|
||||||
|
binary_executable = os.path.join(binary_prefix, exe_name)
|
||||||
|
if (
|
||||||
|
found_executable
|
||||||
|
and os.path.exists(binary_executable)
|
||||||
|
and os.path.samefile(
|
||||||
|
found_executable,
|
||||||
|
binary_executable,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return exe_name
|
||||||
|
|
||||||
|
# Use the `-m` invocation, if there's no "nice" invocation.
|
||||||
|
return f"{get_best_invocation_for_this_python()} -m pip"
|
||||||
|
|
||||||
|
|
||||||
|
def get_best_invocation_for_this_python() -> str:
|
||||||
|
"""Try to figure out the best way to invoke the current Python."""
|
||||||
|
exe = sys.executable
|
||||||
|
exe_name = os.path.basename(exe)
|
||||||
|
|
||||||
|
# Try to use the basename, if it's the first executable.
|
||||||
|
found_executable = shutil.which(exe_name)
|
||||||
|
# Virtual environments often symlink to their parent Python binaries, but we don't
|
||||||
|
# want to treat the Python binaries as equivalent when the environment's Python is
|
||||||
|
# not on PATH (not activated). Thus, we don't follow symlinks.
|
||||||
|
if found_executable and os.path.samestat(os.lstat(found_executable), os.lstat(exe)):
|
||||||
|
return exe_name
|
||||||
|
|
||||||
|
# Use the full executable name, because we couldn't find something simpler.
|
||||||
|
return exe
|
||||||
203
venv/Lib/site-packages/pip/_internal/utils/filesystem.py
Normal file
203
venv/Lib/site-packages/pip/_internal/utils/filesystem.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
from collections.abc import Generator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import Any, BinaryIO, Callable, cast
|
||||||
|
|
||||||
|
from pip._internal.utils.compat import get_path_uid
|
||||||
|
from pip._internal.utils.misc import format_size
|
||||||
|
from pip._internal.utils.retry import retry
|
||||||
|
|
||||||
|
|
||||||
|
def check_path_owner(path: str) -> bool:
|
||||||
|
# If we don't have a way to check the effective uid of this process, then
|
||||||
|
# we'll just assume that we own the directory.
|
||||||
|
if sys.platform == "win32" or not hasattr(os, "geteuid"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
assert os.path.isabs(path)
|
||||||
|
|
||||||
|
previous = None
|
||||||
|
while path != previous:
|
||||||
|
if os.path.lexists(path):
|
||||||
|
# Check if path is writable by current user.
|
||||||
|
if os.geteuid() == 0:
|
||||||
|
# Special handling for root user in order to handle properly
|
||||||
|
# cases where users use sudo without -H flag.
|
||||||
|
try:
|
||||||
|
path_uid = get_path_uid(path)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
return path_uid == 0
|
||||||
|
else:
|
||||||
|
return os.access(path, os.W_OK)
|
||||||
|
else:
|
||||||
|
previous, path = path, os.path.dirname(path)
|
||||||
|
return False # assume we don't own the path
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def adjacent_tmp_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, None]:
|
||||||
|
"""Return a file-like object pointing to a tmp file next to path.
|
||||||
|
|
||||||
|
The file is created securely and is ensured to be written to disk
|
||||||
|
after the context reaches its end.
|
||||||
|
|
||||||
|
kwargs will be passed to tempfile.NamedTemporaryFile to control
|
||||||
|
the way the temporary file will be opened.
|
||||||
|
"""
|
||||||
|
with NamedTemporaryFile(
|
||||||
|
delete=False,
|
||||||
|
dir=os.path.dirname(path),
|
||||||
|
prefix=os.path.basename(path),
|
||||||
|
suffix=".tmp",
|
||||||
|
**kwargs,
|
||||||
|
) as f:
|
||||||
|
result = cast(BinaryIO, f)
|
||||||
|
try:
|
||||||
|
yield result
|
||||||
|
finally:
|
||||||
|
result.flush()
|
||||||
|
os.fsync(result.fileno())
|
||||||
|
|
||||||
|
|
||||||
|
replace = retry(stop_after_delay=1, wait=0.25)(os.replace)
|
||||||
|
|
||||||
|
|
||||||
|
# test_writable_dir and _test_writable_dir_win are copied from Flit,
|
||||||
|
# with the author's agreement to also place them under pip's license.
|
||||||
|
def test_writable_dir(path: str) -> bool:
|
||||||
|
"""Check if a directory is writable.
|
||||||
|
|
||||||
|
Uses os.access() on POSIX, tries creating files on Windows.
|
||||||
|
"""
|
||||||
|
# If the directory doesn't exist, find the closest parent that does.
|
||||||
|
while not os.path.isdir(path):
|
||||||
|
parent = os.path.dirname(path)
|
||||||
|
if parent == path:
|
||||||
|
break # Should never get here, but infinite loops are bad
|
||||||
|
path = parent
|
||||||
|
|
||||||
|
if os.name == "posix":
|
||||||
|
return os.access(path, os.W_OK)
|
||||||
|
|
||||||
|
return _test_writable_dir_win(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _test_writable_dir_win(path: str) -> bool:
|
||||||
|
# os.access doesn't work on Windows: http://bugs.python.org/issue2528
|
||||||
|
# and we can't use tempfile: http://bugs.python.org/issue22107
|
||||||
|
basename = "accesstest_deleteme_fishfingers_custard_"
|
||||||
|
alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
for _ in range(10):
|
||||||
|
name = basename + "".join(random.choice(alphabet) for _ in range(6))
|
||||||
|
file = os.path.join(path, name)
|
||||||
|
try:
|
||||||
|
fd = os.open(file, os.O_RDWR | os.O_CREAT | os.O_EXCL)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
except PermissionError:
|
||||||
|
# This could be because there's a directory with the same name.
|
||||||
|
# But it's highly unlikely there's a directory called that,
|
||||||
|
# so we'll assume it's because the parent dir is not writable.
|
||||||
|
# This could as well be because the parent dir is not readable,
|
||||||
|
# due to non-privileged user access.
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
os.close(fd)
|
||||||
|
os.unlink(file)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# This should never be reached
|
||||||
|
raise OSError("Unexpected condition testing for writable directory")
|
||||||
|
|
||||||
|
|
||||||
|
def find_files(path: str, pattern: str) -> list[str]:
|
||||||
|
"""Returns a list of absolute paths of files beneath path, recursively,
|
||||||
|
with filenames which match the UNIX-style shell glob pattern."""
|
||||||
|
result: list[str] = []
|
||||||
|
for root, _, files in os.walk(path):
|
||||||
|
matches = fnmatch.filter(files, pattern)
|
||||||
|
result.extend(os.path.join(root, f) for f in matches)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def file_size(path: str) -> int | float:
|
||||||
|
# If it's a symlink, return 0.
|
||||||
|
if os.path.islink(path):
|
||||||
|
return 0
|
||||||
|
return os.path.getsize(path)
|
||||||
|
|
||||||
|
|
||||||
|
def format_file_size(path: str) -> str:
|
||||||
|
return format_size(file_size(path))
|
||||||
|
|
||||||
|
|
||||||
|
def directory_size(path: str) -> int | float:
|
||||||
|
size = 0.0
|
||||||
|
for root, _dirs, files in os.walk(path):
|
||||||
|
for filename in files:
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
size += file_size(file_path)
|
||||||
|
return size
|
||||||
|
|
||||||
|
|
||||||
|
def format_directory_size(path: str) -> str:
|
||||||
|
return format_size(directory_size(path))
|
||||||
|
|
||||||
|
|
||||||
|
def copy_directory_permissions(directory: str, target_file: BinaryIO) -> None:
|
||||||
|
mode = (
|
||||||
|
os.stat(directory).st_mode & 0o666 # select read/write permissions of directory
|
||||||
|
| 0o600 # set owner read/write permissions
|
||||||
|
)
|
||||||
|
# Change permissions only if there is no risk of following a symlink.
|
||||||
|
if os.chmod in os.supports_fd:
|
||||||
|
os.chmod(target_file.fileno(), mode)
|
||||||
|
elif os.chmod in os.supports_follow_symlinks:
|
||||||
|
os.chmod(target_file.name, mode, follow_symlinks=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _subdirs_without_generic(
|
||||||
|
path: str, predicate: Callable[[str, list[str]], bool]
|
||||||
|
) -> Generator[Path]:
|
||||||
|
"""Yields every subdirectory of +path+ that has no files matching the
|
||||||
|
predicate under it."""
|
||||||
|
|
||||||
|
directories = []
|
||||||
|
excluded = set()
|
||||||
|
|
||||||
|
for root_str, _, filenames in os.walk(Path(path).resolve()):
|
||||||
|
root = Path(root_str)
|
||||||
|
if predicate(root_str, filenames):
|
||||||
|
# This directory should be excluded, so exclude it and all of its
|
||||||
|
# parent directories.
|
||||||
|
# The last item in root.parents is ".", so we ignore it.
|
||||||
|
#
|
||||||
|
# Wrapping this in `list()` is only needed for Python 3.9.
|
||||||
|
excluded.update(list(root.parents)[:-1])
|
||||||
|
excluded.add(root)
|
||||||
|
directories.append(root)
|
||||||
|
|
||||||
|
for d in sorted(directories, reverse=True):
|
||||||
|
if d not in excluded:
|
||||||
|
yield d
|
||||||
|
|
||||||
|
|
||||||
|
def subdirs_without_files(path: str) -> Generator[Path]:
|
||||||
|
"""Yields every subdirectory of +path+ that has no files under it."""
|
||||||
|
return _subdirs_without_generic(path, lambda root, filenames: len(filenames) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
def subdirs_without_wheels(path: str) -> Generator[Path]:
|
||||||
|
"""Yields every subdirectory of +path+ that has no .whl files under it."""
|
||||||
|
return _subdirs_without_generic(
|
||||||
|
path, lambda root, filenames: any(x.endswith(".whl") for x in filenames)
|
||||||
|
)
|
||||||
24
venv/Lib/site-packages/pip/_internal/utils/filetypes.py
Normal file
24
venv/Lib/site-packages/pip/_internal/utils/filetypes.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""Filetype information."""
|
||||||
|
|
||||||
|
from pip._internal.utils.misc import splitext
|
||||||
|
|
||||||
|
WHEEL_EXTENSION = ".whl"
|
||||||
|
BZ2_EXTENSIONS: tuple[str, ...] = (".tar.bz2", ".tbz")
|
||||||
|
XZ_EXTENSIONS: tuple[str, ...] = (
|
||||||
|
".tar.xz",
|
||||||
|
".txz",
|
||||||
|
".tlz",
|
||||||
|
".tar.lz",
|
||||||
|
".tar.lzma",
|
||||||
|
)
|
||||||
|
ZIP_EXTENSIONS: tuple[str, ...] = (".zip", WHEEL_EXTENSION)
|
||||||
|
TAR_EXTENSIONS: tuple[str, ...] = (".tar.gz", ".tgz", ".tar")
|
||||||
|
ARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS
|
||||||
|
|
||||||
|
|
||||||
|
def is_archive_file(name: str) -> bool:
|
||||||
|
"""Return True if `name` is a considered as an archive file."""
|
||||||
|
ext = splitext(name)[1].lower()
|
||||||
|
if ext in ARCHIVE_EXTENSIONS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
102
venv/Lib/site-packages/pip/_internal/utils/glibc.py
Normal file
102
venv/Lib/site-packages/pip/_internal/utils/glibc.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def glibc_version_string() -> str | None:
|
||||||
|
"Returns glibc version string, or None if not using glibc."
|
||||||
|
return glibc_version_string_confstr() or glibc_version_string_ctypes()
|
||||||
|
|
||||||
|
|
||||||
|
def glibc_version_string_confstr() -> str | None:
|
||||||
|
"Primary implementation of glibc_version_string using os.confstr."
|
||||||
|
# os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
|
||||||
|
# to be broken or missing. This strategy is used in the standard library
|
||||||
|
# platform module:
|
||||||
|
# https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183
|
||||||
|
if sys.platform == "win32":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
gnu_libc_version = os.confstr("CS_GNU_LIBC_VERSION")
|
||||||
|
if gnu_libc_version is None:
|
||||||
|
return None
|
||||||
|
# os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17":
|
||||||
|
_, version = gnu_libc_version.split()
|
||||||
|
except (AttributeError, OSError, ValueError):
|
||||||
|
# os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
|
||||||
|
return None
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def glibc_version_string_ctypes() -> str | None:
|
||||||
|
"Fallback implementation of glibc_version_string using ctypes."
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
|
||||||
|
# manpage says, "If filename is NULL, then the returned handle is for the
|
||||||
|
# main program". This way we can let the linker do the work to figure out
|
||||||
|
# which libc our process is actually using.
|
||||||
|
#
|
||||||
|
# We must also handle the special case where the executable is not a
|
||||||
|
# dynamically linked executable. This can occur when using musl libc,
|
||||||
|
# for example. In this situation, dlopen() will error, leading to an
|
||||||
|
# OSError. Interestingly, at least in the case of musl, there is no
|
||||||
|
# errno set on the OSError. The single string argument used to construct
|
||||||
|
# OSError comes from libc itself and is therefore not portable to
|
||||||
|
# hard code here. In any case, failure to call dlopen() means we
|
||||||
|
# can't proceed, so we bail on our attempt.
|
||||||
|
try:
|
||||||
|
process_namespace = ctypes.CDLL(None)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
gnu_get_libc_version = process_namespace.gnu_get_libc_version
|
||||||
|
except AttributeError:
|
||||||
|
# Symbol doesn't exist -> therefore, we are not linked to
|
||||||
|
# glibc.
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Call gnu_get_libc_version, which returns a string like "2.5"
|
||||||
|
gnu_get_libc_version.restype = ctypes.c_char_p
|
||||||
|
version_str: str = gnu_get_libc_version()
|
||||||
|
# py2 / py3 compatibility:
|
||||||
|
if not isinstance(version_str, str):
|
||||||
|
version_str = version_str.decode("ascii")
|
||||||
|
|
||||||
|
return version_str
|
||||||
|
|
||||||
|
|
||||||
|
# platform.libc_ver regularly returns completely nonsensical glibc
|
||||||
|
# versions. E.g. on my computer, platform says:
|
||||||
|
#
|
||||||
|
# ~$ python2.7 -c 'import platform; print(platform.libc_ver())'
|
||||||
|
# ('glibc', '2.7')
|
||||||
|
# ~$ python3.5 -c 'import platform; print(platform.libc_ver())'
|
||||||
|
# ('glibc', '2.9')
|
||||||
|
#
|
||||||
|
# But the truth is:
|
||||||
|
#
|
||||||
|
# ~$ ldd --version
|
||||||
|
# ldd (Debian GLIBC 2.22-11) 2.22
|
||||||
|
#
|
||||||
|
# This is unfortunate, because it means that the linehaul data on libc
|
||||||
|
# versions that was generated by pip 8.1.2 and earlier is useless and
|
||||||
|
# misleading. Solution: instead of using platform, use our code that actually
|
||||||
|
# works.
|
||||||
|
def libc_ver() -> tuple[str, str]:
|
||||||
|
"""Try to determine the glibc version
|
||||||
|
|
||||||
|
Returns a tuple of strings (lib, version) which default to empty strings
|
||||||
|
in case the lookup fails.
|
||||||
|
"""
|
||||||
|
glibc_version = glibc_version_string()
|
||||||
|
if glibc_version is None:
|
||||||
|
return ("", "")
|
||||||
|
else:
|
||||||
|
return ("glibc", glibc_version)
|
||||||
150
venv/Lib/site-packages/pip/_internal/utils/hashes.py
Normal file
150
venv/Lib/site-packages/pip/_internal/utils/hashes.py
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
from collections.abc import Iterable
|
||||||
|
from typing import TYPE_CHECKING, BinaryIO, NoReturn
|
||||||
|
|
||||||
|
from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError
|
||||||
|
from pip._internal.utils.misc import read_chunks
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from hashlib import _Hash
|
||||||
|
|
||||||
|
|
||||||
|
# The recommended hash algo of the moment. Change this whenever the state of
|
||||||
|
# the art changes; it won't hurt backward compatibility.
|
||||||
|
FAVORITE_HASH = "sha256"
|
||||||
|
|
||||||
|
|
||||||
|
# Names of hashlib algorithms allowed by the --hash option and ``pip hash``
|
||||||
|
# Currently, those are the ones at least as collision-resistant as sha256.
|
||||||
|
STRONG_HASHES = ["sha256", "sha384", "sha512"]
|
||||||
|
|
||||||
|
|
||||||
|
class Hashes:
|
||||||
|
"""A wrapper that builds multiple hashes at once and checks them against
|
||||||
|
known-good values
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hashes: dict[str, list[str]] | None = None) -> None:
|
||||||
|
"""
|
||||||
|
:param hashes: A dict of algorithm names pointing to lists of allowed
|
||||||
|
hex digests
|
||||||
|
"""
|
||||||
|
allowed = {}
|
||||||
|
if hashes is not None:
|
||||||
|
for alg, keys in hashes.items():
|
||||||
|
# Make sure values are always sorted (to ease equality checks)
|
||||||
|
allowed[alg] = [k.lower() for k in sorted(keys)]
|
||||||
|
self._allowed = allowed
|
||||||
|
|
||||||
|
def __and__(self, other: Hashes) -> Hashes:
|
||||||
|
if not isinstance(other, Hashes):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
# If either of the Hashes object is entirely empty (i.e. no hash
|
||||||
|
# specified at all), all hashes from the other object are allowed.
|
||||||
|
if not other:
|
||||||
|
return self
|
||||||
|
if not self:
|
||||||
|
return other
|
||||||
|
|
||||||
|
# Otherwise only hashes that present in both objects are allowed.
|
||||||
|
new = {}
|
||||||
|
for alg, values in other._allowed.items():
|
||||||
|
if alg not in self._allowed:
|
||||||
|
continue
|
||||||
|
new[alg] = [v for v in values if v in self._allowed[alg]]
|
||||||
|
return Hashes(new)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def digest_count(self) -> int:
|
||||||
|
return sum(len(digests) for digests in self._allowed.values())
|
||||||
|
|
||||||
|
def is_hash_allowed(self, hash_name: str, hex_digest: str) -> bool:
|
||||||
|
"""Return whether the given hex digest is allowed."""
|
||||||
|
return hex_digest in self._allowed.get(hash_name, [])
|
||||||
|
|
||||||
|
def check_against_chunks(self, chunks: Iterable[bytes]) -> None:
|
||||||
|
"""Check good hashes against ones built from iterable of chunks of
|
||||||
|
data.
|
||||||
|
|
||||||
|
Raise HashMismatch if none match.
|
||||||
|
|
||||||
|
"""
|
||||||
|
gots = {}
|
||||||
|
for hash_name in self._allowed.keys():
|
||||||
|
try:
|
||||||
|
gots[hash_name] = hashlib.new(hash_name)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raise InstallationError(f"Unknown hash name: {hash_name}")
|
||||||
|
|
||||||
|
for chunk in chunks:
|
||||||
|
for hash in gots.values():
|
||||||
|
hash.update(chunk)
|
||||||
|
|
||||||
|
for hash_name, got in gots.items():
|
||||||
|
if got.hexdigest() in self._allowed[hash_name]:
|
||||||
|
return
|
||||||
|
self._raise(gots)
|
||||||
|
|
||||||
|
def _raise(self, gots: dict[str, _Hash]) -> NoReturn:
|
||||||
|
raise HashMismatch(self._allowed, gots)
|
||||||
|
|
||||||
|
def check_against_file(self, file: BinaryIO) -> None:
|
||||||
|
"""Check good hashes against a file-like object
|
||||||
|
|
||||||
|
Raise HashMismatch if none match.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.check_against_chunks(read_chunks(file))
|
||||||
|
|
||||||
|
def check_against_path(self, path: str) -> None:
|
||||||
|
with open(path, "rb") as file:
|
||||||
|
return self.check_against_file(file)
|
||||||
|
|
||||||
|
def has_one_of(self, hashes: dict[str, str]) -> bool:
|
||||||
|
"""Return whether any of the given hashes are allowed."""
|
||||||
|
for hash_name, hex_digest in hashes.items():
|
||||||
|
if self.is_hash_allowed(hash_name, hex_digest):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
"""Return whether I know any known-good hashes."""
|
||||||
|
return bool(self._allowed)
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
if not isinstance(other, Hashes):
|
||||||
|
return NotImplemented
|
||||||
|
return self._allowed == other._allowed
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash(
|
||||||
|
",".join(
|
||||||
|
sorted(
|
||||||
|
":".join((alg, digest))
|
||||||
|
for alg, digest_list in self._allowed.items()
|
||||||
|
for digest in digest_list
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MissingHashes(Hashes):
|
||||||
|
"""A workalike for Hashes used when we're missing a hash for a requirement
|
||||||
|
|
||||||
|
It computes the actual hash of the requirement and raises a HashMissing
|
||||||
|
exception showing it to the user.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Don't offer the ``hashes`` kwarg."""
|
||||||
|
# Pass our favorite hash in to generate a "gotten hash". With the
|
||||||
|
# empty list, it will never match, so an error will always raise.
|
||||||
|
super().__init__(hashes={FAVORITE_HASH: []})
|
||||||
|
|
||||||
|
def _raise(self, gots: dict[str, _Hash]) -> NoReturn:
|
||||||
|
raise HashMissing(gots[FAVORITE_HASH].hexdigest())
|
||||||
Reference in New Issue
Block a user