Voice et bot modif

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

View file

@ -88,7 +88,7 @@ def join_continuation(lines):
['foobarbaz']
Not sure why, but...
The character preceding the backslash is also elided.
The character preceeding the backslash is also elided.
>>> list(join_continuation(['goo\\', 'dly']))
['godly']

View file

@ -8,6 +8,7 @@ and eventually drop this after all usages are changed.
import os
import sys
from typing import List
from pip._vendor import platformdirs as _appdirs
@ -39,10 +40,9 @@ def user_config_dir(appname: str, roaming: bool = True) -> str:
# for the discussion regarding site_config_dir locations
# see <https://github.com/pypa/pip/issues/1733>
def site_config_dirs(appname: str) -> list[str]:
def site_config_dirs(appname: str) -> List[str]:
if sys.platform == "darwin":
dirval = _appdirs.site_data_dir(appname, appauthor=False, multipath=True)
return dirval.split(os.pathsep)
return [_appdirs.site_data_dir(appname, appauthor=False, multipath=True)]
dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True)
if sys.platform == "win32":

View file

@ -1,13 +1,11 @@
"""Stuff that differs in different Python versions and platform
distributions."""
import importlib.resources
import logging
import os
import sys
from typing import IO
__all__ = ["get_path_uid", "stdlib_pkgs", "tomllib", "WINDOWS"]
__all__ = ["get_path_uid", "stdlib_pkgs", "WINDOWS"]
logger = logging.getLogger(__name__)
@ -53,26 +51,6 @@ def get_path_uid(path: str) -> int:
return file_uid
# The importlib.resources.open_text function was deprecated in 3.11 with suggested
# replacement we use below.
if sys.version_info < (3, 11):
open_text_resource = importlib.resources.open_text
else:
def open_text_resource(
package: str, resource: str, encoding: str = "utf-8", errors: str = "strict"
) -> IO[str]:
return (importlib.resources.files(package) / resource).open(
"r", encoding=encoding, errors=errors
)
if sys.version_info >= (3, 11):
import tomllib
else:
from pip._vendor import tomli as tomllib
# packages in the stdlib that may have installation metadata, but should not be
# considered 'installed'. this theoretically could be determined based on
# dist.location (py27:`sysconfig.get_paths()['stdlib']`,

View file

@ -1,32 +1,30 @@
"""Generate and work with PEP 425 Compatibility Tags."""
from __future__ import annotations
"""Generate and work with PEP 425 Compatibility Tags.
"""
import re
from typing import List, Optional, Tuple
from pip._vendor.packaging.tags import (
PythonVersion,
Tag,
android_platforms,
compatible_tags,
cpython_tags,
generic_tags,
interpreter_name,
interpreter_version,
ios_platforms,
mac_platforms,
)
_apple_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")
_osx_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)")
def version_info_to_nodot(version_info: tuple[int, ...]) -> str:
def version_info_to_nodot(version_info: Tuple[int, ...]) -> str:
# Only use up to the first two numbers.
return "".join(map(str, version_info[:2]))
def _mac_platforms(arch: str) -> list[str]:
match = _apple_arch_pat.match(arch)
def _mac_platforms(arch: str) -> List[str]:
match = _osx_arch_pat.match(arch)
if match:
name, major, minor, actual_arch = match.groups()
mac_version = (int(major), int(minor))
@ -45,37 +43,7 @@ def _mac_platforms(arch: str) -> list[str]:
return arches
def _ios_platforms(arch: str) -> list[str]:
match = _apple_arch_pat.match(arch)
if match:
name, major, minor, actual_multiarch = match.groups()
ios_version = (int(major), int(minor))
arches = [
# Since we have always only checked that the platform starts
# with "ios", for backwards-compatibility we extract the
# actual prefix provided by the user in case they provided
# something like "ioscustom_". It may be good to remove
# this as undocumented or deprecate it in the future.
"{}_{}".format(name, arch[len("ios_") :])
for arch in ios_platforms(ios_version, actual_multiarch)
]
else:
# arch pattern didn't match (?!)
arches = [arch]
return arches
def _android_platforms(arch: str) -> list[str]:
match = re.fullmatch(r"android_(\d+)_(.+)", arch)
if match:
api_level, abi = match.groups()
return list(android_platforms(int(api_level), abi))
else:
# arch pattern didn't match (?!)
return [arch]
def _custom_manylinux_platforms(arch: str) -> list[str]:
def _custom_manylinux_platforms(arch: str) -> List[str]:
arches = [arch]
arch_prefix, arch_sep, arch_suffix = arch.partition("_")
if arch_prefix == "manylinux2014":
@ -96,14 +64,10 @@ def _custom_manylinux_platforms(arch: str) -> list[str]:
return arches
def _get_custom_platforms(arch: str) -> list[str]:
def _get_custom_platforms(arch: str) -> List[str]:
arch_prefix, arch_sep, arch_suffix = arch.partition("_")
if arch.startswith("macosx"):
arches = _mac_platforms(arch)
elif arch.startswith("ios"):
arches = _ios_platforms(arch)
elif arch_prefix == "android":
arches = _android_platforms(arch)
elif arch_prefix in ["manylinux2014", "manylinux2010"]:
arches = _custom_manylinux_platforms(arch)
else:
@ -111,7 +75,7 @@ def _get_custom_platforms(arch: str) -> list[str]:
return arches
def _expand_allowed_platforms(platforms: list[str] | None) -> list[str] | None:
def _expand_allowed_platforms(platforms: Optional[List[str]]) -> Optional[List[str]]:
if not platforms:
return None
@ -136,7 +100,7 @@ def _get_python_version(version: str) -> PythonVersion:
def _get_custom_interpreter(
implementation: str | None = None, version: str | None = None
implementation: Optional[str] = None, version: Optional[str] = None
) -> str:
if implementation is None:
implementation = interpreter_name()
@ -146,11 +110,11 @@ def _get_custom_interpreter(
def get_supported(
version: str | None = None,
platforms: list[str] | None = None,
impl: str | None = None,
abis: list[str] | None = None,
) -> list[Tag]:
version: Optional[str] = None,
platforms: Optional[List[str]] = None,
impl: Optional[str] = None,
abis: Optional[List[str]] = None,
) -> List[Tag]:
"""Return a list of supported tags for each version specified in
`versions`.
@ -163,9 +127,9 @@ def get_supported(
:param abis: specify a list of abis you want valid
tags for, or None. If None, use the local interpreter abi.
"""
supported: list[Tag] = []
supported: List[Tag] = []
python_version: PythonVersion | None = None
python_version: Optional[PythonVersion] = None
if version is not None:
python_version = _get_python_version(version)

View file

@ -1,4 +1,5 @@
"""For when pip wants to check the date or time."""
"""For when pip wants to check the date or time.
"""
import datetime

View file

@ -2,11 +2,9 @@
A module that implements tooling to enable easy warnings about deprecations.
"""
from __future__ import annotations
import logging
import warnings
from typing import Any, TextIO
from typing import Any, Optional, TextIO, Type, Union
from pip._vendor.packaging.version import parse
@ -24,12 +22,12 @@ _original_showwarning: Any = None
# Warnings <-> Logging Integration
def _showwarning(
message: Warning | str,
category: type[Warning],
message: Union[Warning, str],
category: Type[Warning],
filename: str,
lineno: int,
file: TextIO | None = None,
line: str | None = None,
file: Optional[TextIO] = None,
line: Optional[str] = None,
) -> None:
if file is not None:
if _original_showwarning is not None:
@ -57,10 +55,10 @@ def install_warning_logger() -> None:
def deprecated(
*,
reason: str,
replacement: str | None,
gone_in: str | None,
feature_flag: str | None = None,
issue: int | None = None,
replacement: Optional[str],
gone_in: Optional[str],
feature_flag: Optional[str] = None,
issue: Optional[int] = None,
) -> None:
"""Helper to deprecate existing functionality.
@ -89,11 +87,9 @@ def deprecated(
(reason, f"{DEPRECATION_MSG_PREFIX}{{}}"),
(
gone_in,
(
"pip {} will enforce this behaviour change."
if not is_gone
else "Since pip {}, this is no longer supported."
),
"pip {} will enforce this behaviour change."
if not is_gone
else "Since pip {}, this is no longer supported.",
),
(
replacement,
@ -101,11 +97,9 @@ def deprecated(
),
(
feature_flag,
(
"You can use the flag --use-feature={} to test the upcoming behaviour."
if not is_gone
else None
),
"You can use the flag --use-feature={} to test the upcoming behaviour."
if not is_gone
else None,
),
(
issue,

View file

@ -1,4 +1,4 @@
from __future__ import annotations
from typing import Optional
from pip._internal.models.direct_url import ArchiveInfo, DirectUrl, DirInfo, VcsInfo
from pip._internal.models.link import Link
@ -12,8 +12,8 @@ def direct_url_as_pep440_direct_reference(direct_url: DirectUrl, name: str) -> s
requirement = name + " @ "
fragments = []
if isinstance(direct_url.info, VcsInfo):
requirement += (
f"{direct_url.info.vcs}+{direct_url.url}@{direct_url.info.commit_id}"
requirement += "{}+{}@{}".format(
direct_url.info.vcs, direct_url.url, direct_url.info.commit_id
)
elif isinstance(direct_url.info, ArchiveInfo):
requirement += direct_url.url
@ -37,7 +37,7 @@ def direct_url_for_editable(source_dir: str) -> DirectUrl:
def direct_url_from_link(
link: Link, source_dir: str | None = None, link_is_in_wheel_cache: bool = False
link: Link, source_dir: Optional[str] = None, link_is_in_wheel_cache: bool = False
) -> DirectUrl:
if link.is_vcs:
vcs_backend = vcs.get_backend_for_scheme(link.scheme)

View file

@ -1,8 +1,7 @@
from __future__ import annotations
import os
import re
import sys
from typing import List, Optional
from pip._internal.locations import site_packages, user_site
from pip._internal.utils.virtualenv import (
@ -16,7 +15,7 @@ __all__ = [
]
def _egg_link_names(raw_name: str) -> list[str]:
def _egg_link_names(raw_name: str) -> List[str]:
"""
Convert a Name metadata value to a .egg-link name, by applying
the same substitution as pkg_resources's safe_name function.
@ -31,7 +30,7 @@ def _egg_link_names(raw_name: str) -> list[str]:
]
def egg_link_path_from_sys_path(raw_name: str) -> str | None:
def egg_link_path_from_sys_path(raw_name: str) -> Optional[str]:
"""
Look for a .egg-link file for project name, by walking sys.path.
"""
@ -44,7 +43,7 @@ def egg_link_path_from_sys_path(raw_name: str) -> str | None:
return None
def egg_link_path_from_location(raw_name: str) -> str | None:
def egg_link_path_from_location(raw_name: str) -> Optional[str]:
"""
Return the path for the .egg-link file if it exists, otherwise, None.
@ -62,7 +61,7 @@ def egg_link_path_from_location(raw_name: str) -> str | None:
This method will just return the first one found.
"""
sites: list[str] = []
sites: List[str] = []
if running_under_virtualenv():
sites.append(site_packages)
if not virtualenv_no_global() and user_site:

View file

@ -0,0 +1,36 @@
import codecs
import locale
import re
import sys
from typing import List, Tuple
BOMS: List[Tuple[bytes, str]] = [
(codecs.BOM_UTF8, "utf-8"),
(codecs.BOM_UTF16, "utf-16"),
(codecs.BOM_UTF16_BE, "utf-16-be"),
(codecs.BOM_UTF16_LE, "utf-16-le"),
(codecs.BOM_UTF32, "utf-32"),
(codecs.BOM_UTF32_BE, "utf-32-be"),
(codecs.BOM_UTF32_LE, "utf-32-le"),
]
ENCODING_RE = re.compile(rb"coding[:=]\s*([-\w.]+)")
def auto_decode(data: bytes) -> str:
"""Check a bytes string for a BOM to correctly detect the encoding
Fallback to locale.getpreferredencoding(False) like open() on Python3"""
for bom, encoding in BOMS:
if data.startswith(bom):
return data[len(bom) :].decode(encoding)
# Lets check the first two lines as in PEP263
for line in data.split(b"\n")[:2]:
if line[0:1] == b"#" and ENCODING_RE.search(line):
result = ENCODING_RE.search(line)
assert result is not None
encoding = result.groups()[0].decode("ascii")
return data.decode(encoding)
return data.decode(
locale.getpreferredencoding(False) or sys.getdefaultencoding(),
)

View file

@ -1,9 +1,8 @@
from __future__ import annotations
import itertools
import os
import shutil
import sys
from typing import List, Optional
from pip._internal.cli.main import main
from pip._internal.utils.compat import WINDOWS
@ -21,7 +20,7 @@ if WINDOWS:
]
def _wrapper(args: list[str] | None = None) -> int:
def _wrapper(args: Optional[List[str]] = None) -> int:
"""Central wrapper for all old entrypoints.
Historically pip has had several entrypoints defined. Because of issues
@ -78,10 +77,7 @@ def get_best_invocation_for_this_python() -> str:
# 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)):
if found_executable and os.path.samefile(found_executable, exe):
return exe_name
# Use the full executable name, because we couldn't find something simpler.

View file

@ -1,18 +1,16 @@
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 tempfile import NamedTemporaryFile
from typing import Any, BinaryIO, cast
from typing import Any, BinaryIO, Generator, List, Union, cast
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
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:
@ -67,7 +65,10 @@ def adjacent_tmp_file(path: str, **kwargs: Any) -> Generator[BinaryIO, None, Non
os.fsync(result.fileno())
replace = retry(stop_after_delay=1, wait=0.25)(os.replace)
# Tenacity raises RetryError by default, explicitly raise the original exception
_replace_retry = retry(reraise=True, stop=stop_after_delay(1), wait=wait_fixed(0.25))
replace = _replace_retry(os.replace)
# test_writable_dir and _test_writable_dir_win are copied from Flit,
@ -118,17 +119,17 @@ def _test_writable_dir_win(path: str) -> bool:
raise OSError("Unexpected condition testing for writable directory")
def find_files(path: str, pattern: str) -> list[str]:
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] = []
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:
def file_size(path: str) -> Union[int, float]:
# If it's a symlink, return 0.
if os.path.islink(path):
return 0
@ -139,7 +140,7 @@ def format_file_size(path: str) -> str:
return format_size(file_size(path))
def directory_size(path: str) -> int | float:
def directory_size(path: str) -> Union[int, float]:
size = 0.0
for root, _dirs, files in os.walk(path):
for filename in files:
@ -150,15 +151,3 @@ def directory_size(path: str) -> int | float:
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)

View file

@ -1,18 +1,21 @@
"""Filetype information."""
"""Filetype information.
"""
from typing import Tuple
from pip._internal.utils.misc import splitext
WHEEL_EXTENSION = ".whl"
BZ2_EXTENSIONS: tuple[str, ...] = (".tar.bz2", ".tbz")
XZ_EXTENSIONS: tuple[str, ...] = (
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")
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

View file

@ -1,15 +1,14 @@
from __future__ import annotations
import os
import sys
from typing import Optional, Tuple
def glibc_version_string() -> str | None:
def glibc_version_string() -> Optional[str]:
"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:
def glibc_version_string_confstr() -> Optional[str]:
"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
@ -29,7 +28,7 @@ def glibc_version_string_confstr() -> str | None:
return version
def glibc_version_string_ctypes() -> str | None:
def glibc_version_string_ctypes() -> Optional[str]:
"Fallback implementation of glibc_version_string using ctypes."
try:
@ -41,20 +40,7 @@ def glibc_version_string_ctypes() -> str | None:
# 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
process_namespace = ctypes.CDLL(None)
try:
gnu_get_libc_version = process_namespace.gnu_get_libc_version
except AttributeError:
@ -64,7 +50,7 @@ def glibc_version_string_ctypes() -> str | 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()
version_str = gnu_get_libc_version()
# py2 / py3 compatibility:
if not isinstance(version_str, str):
version_str = version_str.decode("ascii")
@ -89,7 +75,7 @@ def glibc_version_string_ctypes() -> str | None:
# 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]:
def libc_ver() -> Tuple[str, str]:
"""Try to determine the glibc version
Returns a tuple of strings (lib, version) which default to empty strings

View file

@ -1,8 +1,5 @@
from __future__ import annotations
import hashlib
from collections.abc import Iterable
from typing import TYPE_CHECKING, BinaryIO, NoReturn
from typing import TYPE_CHECKING, BinaryIO, Dict, Iterable, List, Optional
from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError
from pip._internal.utils.misc import read_chunks
@ -10,6 +7,10 @@ from pip._internal.utils.misc import read_chunks
if TYPE_CHECKING:
from hashlib import _Hash
# NoReturn introduced in 3.6.2; imported only for type checking to maintain
# pip compatibility with older patch versions of Python 3.6
from typing import NoReturn
# The recommended hash algo of the moment. Change this whenever the state of
# the art changes; it won't hurt backward compatibility.
@ -27,7 +28,7 @@ class Hashes:
"""
def __init__(self, hashes: dict[str, list[str]] | None = None) -> None:
def __init__(self, hashes: Optional[Dict[str, List[str]]] = None) -> None:
"""
:param hashes: A dict of algorithm names pointing to lists of allowed
hex digests
@ -36,10 +37,10 @@ class Hashes:
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)]
allowed[alg] = sorted(keys)
self._allowed = allowed
def __and__(self, other: Hashes) -> Hashes:
def __and__(self, other: "Hashes") -> "Hashes":
if not isinstance(other, Hashes):
return NotImplemented
@ -89,7 +90,7 @@ class Hashes:
return
self._raise(gots)
def _raise(self, gots: dict[str, _Hash]) -> NoReturn:
def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
raise HashMismatch(self._allowed, gots)
def check_against_file(self, file: BinaryIO) -> None:
@ -104,7 +105,7 @@ class Hashes:
with open(path, "rb") as file:
return self.check_against_file(file)
def has_one_of(self, hashes: dict[str, str]) -> bool:
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):
@ -146,5 +147,5 @@ class MissingHashes(Hashes):
# 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:
def _raise(self, gots: Dict[str, "_Hash"]) -> "NoReturn":
raise HashMissing(gots[FAVORITE_HASH].hexdigest())

View file

@ -1,5 +1,3 @@
from __future__ import annotations
import contextlib
import errno
import logging
@ -7,11 +5,10 @@ import logging.handlers
import os
import sys
import threading
from collections.abc import Generator
from dataclasses import dataclass
from io import TextIOWrapper
from logging import Filter
from typing import Any, ClassVar
from typing import Any, ClassVar, Generator, List, Optional, TextIO, Type
from pip._vendor.rich.console import (
Console,
@ -32,8 +29,6 @@ from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX
from pip._internal.utils.misc import ensure_dir
_log_state = threading.local()
_stdout_console = None
_stderr_console = None
subprocess_logger = getLogger("pip.subprocessor")
@ -43,7 +38,7 @@ class BrokenStdoutLoggingError(Exception):
"""
def _is_broken_pipe_error(exc_class: type[BaseException], exc: BaseException) -> bool:
def _is_broken_pipe_error(exc_class: Type[BaseException], exc: BaseException) -> bool:
if exc_class is BrokenPipeError:
return True
@ -142,28 +137,12 @@ class IndentedRenderable:
yield Segment("\n")
class PipConsole(Console):
def on_broken_pipe(self) -> None:
# Reraise the original exception, rich 13.8.0+ exits by default
# instead, preventing our handler from firing.
raise BrokenPipeError() from None
def get_console(*, stderr: bool = False) -> Console:
if stderr:
assert _stderr_console is not None, "stderr rich console is missing!"
return _stderr_console
else:
assert _stdout_console is not None, "stdout rich console is missing!"
return _stdout_console
class RichPipStreamHandler(RichHandler):
KEYWORDS: ClassVar[list[str] | None] = []
KEYWORDS: ClassVar[Optional[List[str]]] = []
def __init__(self, console: Console) -> None:
def __init__(self, stream: Optional[TextIO], no_color: bool) -> None:
super().__init__(
console=console,
console=Console(file=stream, no_color=no_color, soft_wrap=True),
show_time=False,
show_level=False,
show_path=False,
@ -172,11 +151,11 @@ class RichPipStreamHandler(RichHandler):
# Our custom override on Rich's logger, to make things work as we need them to.
def emit(self, record: logging.LogRecord) -> None:
style: Style | None = None
style: Optional[Style] = None
# If we are given a diagnostic error to present, present it with indentation.
assert isinstance(record.args, tuple)
if getattr(record, "rich", False):
assert isinstance(record.args, tuple)
(rich_renderable,) = record.args
assert isinstance(
rich_renderable, (ConsoleRenderable, RichCast, str)
@ -233,6 +212,7 @@ class MaxLevelFilter(Filter):
class ExcludeLoggerFilter(Filter):
"""
A logging Filter that excludes records from a logger (or its children).
"""
@ -243,7 +223,7 @@ class ExcludeLoggerFilter(Filter):
return not super().filter(record)
def setup_logging(verbosity: int, no_color: bool, user_log_file: str | None) -> int:
def setup_logging(verbosity: int, no_color: bool, user_log_file: Optional[str]) -> int:
"""Configures and sets up all of the logging
Returns the requested logging level, as its integer value.
@ -280,6 +260,10 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: str | None) ->
vendored_log_level = "WARNING" if level in ["INFO", "ERROR"] else "DEBUG"
# Shorthands for clarity
log_streams = {
"stdout": "ext://sys.stdout",
"stderr": "ext://sys.stderr",
}
handler_classes = {
"stream": "pip._internal.utils.logging.RichPipStreamHandler",
"file": "pip._internal.utils.logging.BetterRotatingFileHandler",
@ -287,9 +271,6 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: str | None) ->
handlers = ["console", "console_errors", "console_subprocess"] + (
["user_log"] if include_user_log else []
)
global _stdout_console, stderr_console
_stdout_console = PipConsole(file=sys.stdout, no_color=no_color, soft_wrap=True)
_stderr_console = PipConsole(file=sys.stderr, no_color=no_color, soft_wrap=True)
logging.config.dictConfig(
{
@ -324,14 +305,16 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: str | None) ->
"console": {
"level": level,
"class": handler_classes["stream"],
"console": _stdout_console,
"no_color": no_color,
"stream": log_streams["stdout"],
"filters": ["exclude_subprocess", "exclude_warnings"],
"formatter": "indent",
},
"console_errors": {
"level": "WARNING",
"class": handler_classes["stream"],
"console": _stderr_console,
"no_color": no_color,
"stream": log_streams["stderr"],
"filters": ["exclude_subprocess"],
"formatter": "indent",
},
@ -340,7 +323,8 @@ def setup_logging(verbosity: int, no_color: bool, user_log_file: str | None) ->
"console_subprocess": {
"level": level,
"class": handler_classes["stream"],
"console": _stderr_console,
"stream": log_streams["stderr"],
"no_color": no_color,
"filters": ["restrict_to_subprocess"],
"formatter": "indent",
},

View file

@ -1,8 +1,8 @@
from __future__ import annotations
import contextlib
import errno
import getpass
import hashlib
import io
import logging
import os
import posixpath
@ -11,8 +11,6 @@ import stat
import sys
import sysconfig
import urllib.parse
from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence
from dataclasses import dataclass
from functools import partial
from io import StringIO
from itertools import filterfalse, tee, zip_longest
@ -22,20 +20,29 @@ from typing import (
Any,
BinaryIO,
Callable,
ContextManager,
Dict,
Generator,
Iterable,
Iterator,
List,
Optional,
TextIO,
Tuple,
Type,
TypeVar,
Union,
cast,
)
from pip._vendor.packaging.requirements import Requirement
from pip._vendor.pyproject_hooks import BuildBackendHookCaller
from pip._vendor.tenacity import retry, stop_after_delay, wait_fixed
from pip import __version__
from pip._internal.exceptions import CommandError, ExternallyManagedEnvironment
from pip._internal.locations import get_major_minor_version
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.retry import retry
from pip._internal.utils.virtualenv import running_under_virtualenv
__all__ = [
@ -49,6 +56,7 @@ __all__ = [
"normalize_path",
"renames",
"get_prog",
"captured_stdout",
"ensure_dir",
"remove_auth_from_url",
"check_externally_managed",
@ -58,14 +66,12 @@ __all__ = [
logger = logging.getLogger(__name__)
T = TypeVar("T")
ExcInfo = tuple[type[BaseException], BaseException, TracebackType]
VersionInfo = tuple[int, int, int]
NetlocTuple = tuple[str, tuple[Optional[str], Optional[str]]]
ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType]
VersionInfo = Tuple[int, int, int]
NetlocTuple = Tuple[str, Tuple[Optional[str], Optional[str]]]
OnExc = Callable[[FunctionType, Path, BaseException], Any]
OnErr = Callable[[FunctionType, Path, ExcInfo], Any]
FILE_CHUNK_SIZE = 1024 * 1024
def get_pip_version() -> str:
pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..")
@ -74,7 +80,7 @@ def get_pip_version() -> str:
return f"pip {__version__} from {pip_pkg_dir} (python {get_major_minor_version()})"
def normalize_version_info(py_version_info: tuple[int, ...]) -> tuple[int, int, int]:
def normalize_version_info(py_version_info: Tuple[int, ...]) -> Tuple[int, int, int]:
"""
Convert a tuple of ints representing a Python version to one of length
three.
@ -116,13 +122,23 @@ def get_prog() -> str:
# Retry every half second for up to 3 seconds
@retry(stop_after_delay=3, wait=0.5)
def rmtree(dir: str, ignore_errors: bool = False, onexc: OnExc | None = None) -> None:
# Tenacity raises RetryError by default, explicitly raise the original exception
@retry(reraise=True, stop=stop_after_delay(3), wait=wait_fixed(0.5))
def rmtree(
dir: str,
ignore_errors: bool = False,
onexc: Optional[OnExc] = None,
) -> None:
if ignore_errors:
onexc = _onerror_ignore
if onexc is None:
onexc = _onerror_reraise
handler: OnErr = partial(rmtree_errorhandler, onexc=onexc)
handler: OnErr = partial(
# `[func, path, Union[ExcInfo, BaseException]] -> Any` is equivalent to
# `Union[([func, path, ExcInfo] -> Any), ([func, path, BaseException] -> Any)]`.
cast(Union[OnExc, OnErr], rmtree_errorhandler),
onexc=onexc,
)
if sys.version_info >= (3, 12):
# See https://docs.python.org/3.12/whatsnew/3.12.html#shutil.
shutil.rmtree(dir, onexc=handler) # type: ignore
@ -135,13 +151,13 @@ def _onerror_ignore(*_args: Any) -> None:
def _onerror_reraise(*_args: Any) -> None:
raise # noqa: PLE0704 - Bare exception used to reraise existing exception
raise
def rmtree_errorhandler(
func: FunctionType,
path: Path,
exc_info: ExcInfo | BaseException,
exc_info: Union[ExcInfo, BaseException],
*,
onexc: OnExc = _onerror_reraise,
) -> None:
@ -268,7 +284,7 @@ def format_size(bytes: float) -> str:
return f"{int(bytes)} bytes"
def tabulate(rows: Iterable[Iterable[Any]]) -> tuple[list[str], list[int]]:
def tabulate(rows: Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]]:
"""Return a list of formatted rows and a list of column sizes.
For example::
@ -300,7 +316,7 @@ def is_installable_dir(path: str) -> bool:
def read_chunks(
file: BinaryIO, size: int = FILE_CHUNK_SIZE
file: BinaryIO, size: int = io.DEFAULT_BUFFER_SIZE
) -> Generator[bytes, None, None]:
"""Yield pieces of data from a file-like object until EOF."""
while True:
@ -323,7 +339,7 @@ def normalize_path(path: str, resolve_symlinks: bool = True) -> str:
return os.path.normcase(path)
def splitext(path: str) -> tuple[str, str]:
def splitext(path: str) -> Tuple[str, str]:
"""Like os.path.splitext, but take off .tar too"""
base, ext = posixpath.splitext(path)
if base.lower().endswith(".tar"):
@ -371,7 +387,7 @@ class StreamWrapper(StringIO):
orig_stream: TextIO
@classmethod
def from_stream(cls, orig_stream: TextIO) -> StreamWrapper:
def from_stream(cls, orig_stream: TextIO) -> "StreamWrapper":
ret = cls()
ret.orig_stream = orig_stream
return ret
@ -383,15 +399,49 @@ class StreamWrapper(StringIO):
return self.orig_stream.encoding
@contextlib.contextmanager
def captured_output(stream_name: str) -> Generator[StreamWrapper, None, None]:
"""Return a context manager used by captured_stdout/stdin/stderr
that temporarily replaces the sys stream *stream_name* with a StringIO.
Taken from Lib/support/__init__.py in the CPython repo.
"""
orig_stdout = getattr(sys, stream_name)
setattr(sys, stream_name, StreamWrapper.from_stream(orig_stdout))
try:
yield getattr(sys, stream_name)
finally:
setattr(sys, stream_name, orig_stdout)
def captured_stdout() -> ContextManager[StreamWrapper]:
"""Capture the output of sys.stdout:
with captured_stdout() as stdout:
print('hello')
self.assertEqual(stdout.getvalue(), 'hello\n')
Taken from Lib/support/__init__.py in the CPython repo.
"""
return captured_output("stdout")
def captured_stderr() -> ContextManager[StreamWrapper]:
"""
See captured_stdout().
"""
return captured_output("stderr")
# Simulates an enum
def enum(*sequential: Any, **named: Any) -> type[Any]:
def enum(*sequential: Any, **named: Any) -> Type[Any]:
enums = dict(zip(sequential, range(len(sequential))), **named)
reverse = {value: key for key, value in enums.items()}
enums["reverse_mapping"] = reverse
return type("Enum", (), enums)
def build_netloc(host: str, port: int | None) -> str:
def build_netloc(host: str, port: Optional[int]) -> str:
"""
Build a netloc from a host-port pair
"""
@ -413,7 +463,7 @@ def build_url_from_netloc(netloc: str, scheme: str = "https") -> str:
return f"{scheme}://{netloc}"
def parse_netloc(netloc: str) -> tuple[str | None, int | None]:
def parse_netloc(netloc: str) -> Tuple[Optional[str], Optional[int]]:
"""
Return the host-port pair from a netloc.
"""
@ -435,7 +485,7 @@ def split_auth_from_netloc(netloc: str) -> NetlocTuple:
# behaves if more than one @ is present (which can be checked using
# the password attribute of urlsplit()'s return value).
auth, netloc = netloc.rsplit("@", 1)
pw: str | None = None
pw: Optional[str] = None
if ":" in auth:
# Split from the left because that's how urllib.parse.urlsplit()
# behaves if more than one : is present (which again can be checked
@ -472,8 +522,8 @@ def redact_netloc(netloc: str) -> str:
def _transform_url(
url: str, transform_netloc: Callable[[str], tuple[Any, ...]]
) -> tuple[str, NetlocTuple]:
url: str, transform_netloc: Callable[[str], Tuple[Any, ...]]
) -> Tuple[str, NetlocTuple]:
"""Transform and replace netloc in a url.
transform_netloc is a function taking the netloc and returning a
@ -495,13 +545,13 @@ def _get_netloc(netloc: str) -> NetlocTuple:
return split_auth_from_netloc(netloc)
def _redact_netloc(netloc: str) -> tuple[str]:
def _redact_netloc(netloc: str) -> Tuple[str]:
return (redact_netloc(netloc),)
def split_auth_netloc_from_url(
url: str,
) -> tuple[str, str, tuple[str | None, str | None]]:
) -> Tuple[str, str, Tuple[Optional[str], Optional[str]]]:
"""
Parse a url into separate netloc, auth, and url with no auth.
@ -530,10 +580,10 @@ def redact_auth_from_requirement(req: Requirement) -> str:
return str(req).replace(req.url, redact_auth_from_url(req.url))
@dataclass(frozen=True)
class HiddenText:
secret: str
redacted: str
def __init__(self, secret: str, redacted: str) -> None:
self.secret = secret
self.redacted = redacted
def __repr__(self) -> str:
return f"<HiddenText {str(self)!r}>"
@ -543,7 +593,7 @@ class HiddenText:
# This is useful for testing.
def __eq__(self, other: Any) -> bool:
if type(self) is not type(other):
if type(self) != type(other):
return False
# The string being used for redaction doesn't also have to match,
@ -606,7 +656,7 @@ def is_console_interactive() -> bool:
return sys.stdin is not None and sys.stdin.isatty()
def hash_file(path: str, blocksize: int = 1 << 20) -> tuple[Any, int]:
def hash_file(path: str, blocksize: int = 1 << 20) -> Tuple[Any, int]:
"""Return (hash, length) for path using hashlib.sha256()"""
h = hashlib.sha256()
@ -618,7 +668,7 @@ def hash_file(path: str, blocksize: int = 1 << 20) -> tuple[Any, int]:
return h, length
def pairwise(iterable: Iterable[Any]) -> Iterator[tuple[Any, Any]]:
def pairwise(iterable: Iterable[Any]) -> Iterator[Tuple[Any, Any]]:
"""
Return paired elements.
@ -630,8 +680,9 @@ def pairwise(iterable: Iterable[Any]) -> Iterator[tuple[Any, Any]]:
def partition(
pred: Callable[[T], bool], iterable: Iterable[T]
) -> tuple[Iterable[T], Iterable[T]]:
pred: Callable[[T], bool],
iterable: Iterable[T],
) -> Tuple[Iterable[T], Iterable[T]]:
"""
Use a predicate to partition entries into false entries and true entries,
like
@ -648,9 +699,9 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
config_holder: Any,
source_dir: str,
build_backend: str,
backend_path: str | None = None,
runner: Callable[..., None] | None = None,
python_executable: str | None = None,
backend_path: Optional[str] = None,
runner: Optional[Callable[..., None]] = None,
python_executable: Optional[str] = None,
):
super().__init__(
source_dir, build_backend, backend_path, runner, python_executable
@ -660,8 +711,8 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
def build_wheel(
self,
wheel_directory: str,
config_settings: Mapping[str, Any] | None = None,
metadata_directory: str | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
metadata_directory: Optional[str] = None,
) -> str:
cs = self.config_holder.config_settings
return super().build_wheel(
@ -671,7 +722,7 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
def build_sdist(
self,
sdist_directory: str,
config_settings: Mapping[str, Any] | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> str:
cs = self.config_holder.config_settings
return super().build_sdist(sdist_directory, config_settings=cs)
@ -679,8 +730,8 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
def build_editable(
self,
wheel_directory: str,
config_settings: Mapping[str, Any] | None = None,
metadata_directory: str | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
metadata_directory: Optional[str] = None,
) -> str:
cs = self.config_holder.config_settings
return super().build_editable(
@ -688,27 +739,27 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
)
def get_requires_for_build_wheel(
self, config_settings: Mapping[str, Any] | None = None
) -> Sequence[str]:
self, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None
) -> List[str]:
cs = self.config_holder.config_settings
return super().get_requires_for_build_wheel(config_settings=cs)
def get_requires_for_build_sdist(
self, config_settings: Mapping[str, Any] | None = None
) -> Sequence[str]:
self, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None
) -> List[str]:
cs = self.config_holder.config_settings
return super().get_requires_for_build_sdist(config_settings=cs)
def get_requires_for_build_editable(
self, config_settings: Mapping[str, Any] | None = None
) -> Sequence[str]:
self, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None
) -> List[str]:
cs = self.config_holder.config_settings
return super().get_requires_for_build_editable(config_settings=cs)
def prepare_metadata_for_build_wheel(
self,
metadata_directory: str,
config_settings: Mapping[str, Any] | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
_allow_fallback: bool = True,
) -> str:
cs = self.config_holder.config_settings
@ -721,45 +772,12 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller):
def prepare_metadata_for_build_editable(
self,
metadata_directory: str,
config_settings: Mapping[str, Any] | None = None,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
_allow_fallback: bool = True,
) -> str | None:
) -> str:
cs = self.config_holder.config_settings
return super().prepare_metadata_for_build_editable(
metadata_directory=metadata_directory,
config_settings=cs,
_allow_fallback=_allow_fallback,
)
def warn_if_run_as_root() -> None:
"""Output a warning for sudo users on Unix.
In a virtual environment, sudo pip still writes to virtualenv.
On Windows, users may run pip as Administrator without issues.
This warning only applies to Unix root users outside of virtualenv.
"""
if running_under_virtualenv():
return
if not hasattr(os, "getuid"):
return
# On Windows, there are no "system managed" Python packages. Installing as
# Administrator via pip is the correct way of updating system environments.
#
# We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
if sys.platform == "win32" or sys.platform == "cygwin":
return
if os.getuid() != 0:
return
logger.warning(
"Running pip as the 'root' user can result in broken permissions and "
"conflicting behaviour with the system package manager, possibly "
"rendering your system unusable. "
"It is recommended to use a virtual environment instead: "
"https://pip.pypa.io/warnings/venv. "
"Use the --root-user-action option if you know what you are doing and "
"want to suppress this warning."
)

View file

@ -0,0 +1,39 @@
"""Utilities for defining models
"""
import operator
from typing import Any, Callable, Type
class KeyBasedCompareMixin:
"""Provides comparison capabilities that is based on a key"""
__slots__ = ["_compare_key", "_defining_class"]
def __init__(self, key: Any, defining_class: Type["KeyBasedCompareMixin"]) -> None:
self._compare_key = key
self._defining_class = defining_class
def __hash__(self) -> int:
return hash(self._compare_key)
def __lt__(self, other: Any) -> bool:
return self._compare(other, operator.__lt__)
def __le__(self, other: Any) -> bool:
return self._compare(other, operator.__le__)
def __gt__(self, other: Any) -> bool:
return self._compare(other, operator.__gt__)
def __ge__(self, other: Any) -> bool:
return self._compare(other, operator.__ge__)
def __eq__(self, other: Any) -> bool:
return self._compare(other, operator.__eq__)
def _compare(self, other: Any, method: Callable[[Any, Any], bool]) -> bool:
if not isinstance(other, self._defining_class):
return NotImplemented
return method(self._compare_key, other._compare_key)

View file

@ -1,17 +1,18 @@
from __future__ import annotations
import functools
import logging
import re
from typing import NewType, Optional, Tuple, cast
from pip._vendor.packaging import specifiers, version
from pip._vendor.packaging.requirements import Requirement
NormalizedExtra = NewType("NormalizedExtra", str)
logger = logging.getLogger(__name__)
@functools.lru_cache(maxsize=32)
def check_requires_python(
requires_python: str | None, version_info: tuple[int, ...]
requires_python: Optional[str], version_info: Tuple[int, ...]
) -> bool:
"""
Check if the given Python version matches a "Requires-Python" specifier.
@ -33,7 +34,7 @@ def check_requires_python(
return python_version in requires_python_specifier
@functools.lru_cache(maxsize=10000)
@functools.lru_cache(maxsize=512)
def get_requirement(req_string: str) -> Requirement:
"""Construct a packaging.Requirement object with caching"""
# Parsing requirement strings is expensive, and is also expected to happen
@ -42,3 +43,15 @@ def get_requirement(req_string: str) -> Requirement:
# minimize repeated parsing of the same string to construct equivalent
# Requirement objects.
return Requirement(req_string)
def safe_extra(extra: str) -> NormalizedExtra:
"""Convert an arbitrary string to a standard 'extra' name
Any runs of non-alphanumeric characters are replaced with a single '_',
and the result is always lowercased.
This function is duplicated from ``pkg_resources``. Note that this is not
the same to either ``canonicalize_name`` or ``_egg_link_name``.
"""
return cast(NormalizedExtra, re.sub("[^A-Za-z0-9.-]+", "_", extra).lower())

View file

@ -1,45 +0,0 @@
from __future__ import annotations
import functools
from time import perf_counter, sleep
from typing import TYPE_CHECKING, Callable, TypeVar
if TYPE_CHECKING:
from typing_extensions import ParamSpec
T = TypeVar("T")
P = ParamSpec("P")
def retry(
wait: float, stop_after_delay: float
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Decorator to automatically retry a function on error.
If the function raises, the function is recalled with the same arguments
until it returns or the time limit is reached. When the time limit is
surpassed, the last exception raised is reraised.
:param wait: The time to wait after an error before retrying, in seconds.
:param stop_after_delay: The time limit after which retries will cease,
in seconds.
"""
def wrapper(func: Callable[P, T]) -> Callable[P, T]:
@functools.wraps(func)
def retry_wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
# The performance counter is monotonic on all platforms we care
# about and has much better resolution than time.monotonic().
start_time = perf_counter()
while True:
try:
return func(*args, **kwargs)
except Exception:
if perf_counter() - start_time > stop_after_delay:
raise
sleep(wait)
return retry_wrapped
return wrapper

View file

@ -0,0 +1,146 @@
import sys
import textwrap
from typing import List, Optional, Sequence
# Shim to wrap setup.py invocation with setuptools
# Note that __file__ is handled via two {!r} *and* %r, to ensure that paths on
# Windows are correctly handled (it should be "C:\\Users" not "C:\Users").
_SETUPTOOLS_SHIM = textwrap.dedent(
"""
exec(compile('''
# This is <pip-setuptools-caller> -- a caller that pip uses to run setup.py
#
# - It imports setuptools before invoking setup.py, to enable projects that directly
# import from `distutils.core` to work with newer packaging standards.
# - It provides a clear error message when setuptools is not installed.
# - It sets `sys.argv[0]` to the underlying `setup.py`, when invoking `setup.py` so
# setuptools doesn't think the script is `-c`. This avoids the following warning:
# manifest_maker: standard file '-c' not found".
# - It generates a shim setup.py, for handling setup.cfg-only projects.
import os, sys, tokenize
try:
import setuptools
except ImportError as error:
print(
"ERROR: Can not execute `setup.py` since setuptools is not available in "
"the build environment.",
file=sys.stderr,
)
sys.exit(1)
__file__ = %r
sys.argv[0] = __file__
if os.path.exists(__file__):
filename = __file__
with tokenize.open(__file__) as f:
setup_py_code = f.read()
else:
filename = "<auto-generated setuptools caller>"
setup_py_code = "from setuptools import setup; setup()"
exec(compile(setup_py_code, filename, "exec"))
''' % ({!r},), "<pip-setuptools-caller>", "exec"))
"""
).rstrip()
def make_setuptools_shim_args(
setup_py_path: str,
global_options: Optional[Sequence[str]] = None,
no_user_config: bool = False,
unbuffered_output: bool = False,
) -> List[str]:
"""
Get setuptools command arguments with shim wrapped setup file invocation.
:param setup_py_path: The path to setup.py to be wrapped.
:param global_options: Additional global options.
:param no_user_config: If True, disables personal user configuration.
:param unbuffered_output: If True, adds the unbuffered switch to the
argument list.
"""
args = [sys.executable]
if unbuffered_output:
args += ["-u"]
args += ["-c", _SETUPTOOLS_SHIM.format(setup_py_path)]
if global_options:
args += global_options
if no_user_config:
args += ["--no-user-cfg"]
return args
def make_setuptools_bdist_wheel_args(
setup_py_path: str,
global_options: Sequence[str],
build_options: Sequence[str],
destination_dir: str,
) -> List[str]:
# NOTE: Eventually, we'd want to also -S to the flags here, when we're
# isolating. Currently, it breaks Python in virtualenvs, because it
# relies on site.py to find parts of the standard library outside the
# virtualenv.
args = make_setuptools_shim_args(
setup_py_path, global_options=global_options, unbuffered_output=True
)
args += ["bdist_wheel", "-d", destination_dir]
args += build_options
return args
def make_setuptools_clean_args(
setup_py_path: str,
global_options: Sequence[str],
) -> List[str]:
args = make_setuptools_shim_args(
setup_py_path, global_options=global_options, unbuffered_output=True
)
args += ["clean", "--all"]
return args
def make_setuptools_develop_args(
setup_py_path: str,
*,
global_options: Sequence[str],
no_user_config: bool,
prefix: Optional[str],
home: Optional[str],
use_user_site: bool,
) -> List[str]:
assert not (use_user_site and prefix)
args = make_setuptools_shim_args(
setup_py_path,
global_options=global_options,
no_user_config=no_user_config,
)
args += ["develop", "--no-deps"]
if prefix:
args += ["--prefix", prefix]
if home is not None:
args += ["--install-dir", home]
if use_user_site:
args += ["--user", "--prefix="]
return args
def make_setuptools_egg_info_args(
setup_py_path: str,
egg_info_dir: Optional[str],
no_user_config: bool,
) -> List[str]:
args = make_setuptools_shim_args(setup_py_path, no_user_config=no_user_config)
args += ["egg_info"]
if egg_info_dir:
args += ["--egg-base", egg_info_dir]
return args

View file

@ -1,11 +1,17 @@
from __future__ import annotations
import logging
import os
import shlex
import subprocess
from collections.abc import Iterable, Mapping
from typing import Any, Callable, Literal, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Iterable,
List,
Mapping,
Optional,
Union,
)
from pip._vendor.rich.markup import escape
@ -14,10 +20,16 @@ from pip._internal.exceptions import InstallationSubprocessError
from pip._internal.utils.logging import VERBOSE, subprocess_logger
from pip._internal.utils.misc import HiddenText
CommandArgs = list[Union[str, HiddenText]]
if TYPE_CHECKING:
# Literal was introduced in Python 3.8.
#
# TODO: Remove `if TYPE_CHECKING` when dropping support for Python 3.7.
from typing import Literal
CommandArgs = List[Union[str, HiddenText]]
def make_command(*args: str | HiddenText | CommandArgs) -> CommandArgs:
def make_command(*args: Union[str, HiddenText, CommandArgs]) -> CommandArgs:
"""
Create a CommandArgs object.
"""
@ -34,7 +46,7 @@ def make_command(*args: str | HiddenText | CommandArgs) -> CommandArgs:
return command_args
def format_command_args(args: list[str] | CommandArgs) -> str:
def format_command_args(args: Union[List[str], CommandArgs]) -> str:
"""
Format command arguments for display.
"""
@ -49,7 +61,7 @@ def format_command_args(args: list[str] | CommandArgs) -> str:
)
def reveal_command_args(args: list[str] | CommandArgs) -> list[str]:
def reveal_command_args(args: Union[List[str], CommandArgs]) -> List[str]:
"""
Return the arguments in their raw, unredacted form.
"""
@ -57,16 +69,16 @@ def reveal_command_args(args: list[str] | CommandArgs) -> list[str]:
def call_subprocess(
cmd: list[str] | CommandArgs,
cmd: Union[List[str], CommandArgs],
show_stdout: bool = False,
cwd: str | None = None,
on_returncode: Literal["raise", "warn", "ignore"] = "raise",
extra_ok_returncodes: Iterable[int] | None = None,
extra_environ: Mapping[str, Any] | None = None,
unset_environ: Iterable[str] | None = None,
spinner: SpinnerInterface | None = None,
log_failed_cmd: bool | None = True,
stdout_only: bool | None = False,
cwd: Optional[str] = None,
on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
extra_ok_returncodes: Optional[Iterable[int]] = None,
extra_environ: Optional[Mapping[str, Any]] = None,
unset_environ: Optional[Iterable[str]] = None,
spinner: Optional[SpinnerInterface] = None,
log_failed_cmd: Optional[bool] = True,
stdout_only: Optional[bool] = False,
*,
command_desc: str,
) -> str:
@ -232,9 +244,9 @@ def runner_with_spinner_message(message: str) -> Callable[..., None]:
"""
def runner(
cmd: list[str],
cwd: str | None = None,
extra_environ: Mapping[str, Any] | None = None,
cmd: List[str],
cwd: Optional[str] = None,
extra_environ: Optional[Mapping[str, Any]] = None,
) -> None:
with open_spinner(message) as spinner:
call_subprocess(

View file

@ -1,18 +1,20 @@
from __future__ import annotations
import errno
import itertools
import logging
import os.path
import tempfile
import traceback
from collections.abc import Generator
from contextlib import ExitStack, contextmanager
from pathlib import Path
from typing import (
Any,
Callable,
Dict,
Generator,
List,
Optional,
TypeVar,
Union,
)
from pip._internal.utils.misc import enum, rmtree
@ -31,7 +33,7 @@ tempdir_kinds = enum(
)
_tempdir_manager: ExitStack | None = None
_tempdir_manager: Optional[ExitStack] = None
@contextmanager
@ -49,7 +51,7 @@ class TempDirectoryTypeRegistry:
"""Manages temp directory behavior"""
def __init__(self) -> None:
self._should_delete: dict[str, bool] = {}
self._should_delete: Dict[str, bool] = {}
def set_delete(self, kind: str, value: bool) -> None:
"""Indicate whether a TempDirectory of the given kind should be
@ -64,7 +66,7 @@ class TempDirectoryTypeRegistry:
return self._should_delete.get(kind, True)
_tempdir_registry: TempDirectoryTypeRegistry | None = None
_tempdir_registry: Optional[TempDirectoryTypeRegistry] = None
@contextmanager
@ -111,8 +113,8 @@ class TempDirectory:
def __init__(
self,
path: str | None = None,
delete: bool | None | _Default = _default,
path: Optional[str] = None,
delete: Union[bool, None, _Default] = _default,
kind: str = "temp",
globally_managed: bool = False,
ignore_cleanup_errors: bool = True,
@ -182,7 +184,7 @@ class TempDirectory:
if not os.path.exists(self._path):
return
errors: list[BaseException] = []
errors: List[BaseException] = []
def onerror(
func: Callable[..., Any],
@ -206,7 +208,7 @@ class TempDirectory:
if self.ignore_cleanup_errors:
try:
# first try with @retry; retrying to handle ephemeral errors
# first try with tenacity; retrying to handle ephemeral errors
rmtree(self._path, ignore_errors=False)
except OSError:
# last pass ignore/log all errors
@ -243,7 +245,7 @@ class AdjacentTempDirectory(TempDirectory):
# with leading '-' and invalid metadata
LEADING_CHARS = "-~.=%0123456789"
def __init__(self, original: str, delete: bool | None = None) -> None:
def __init__(self, original: str, delete: Optional[bool] = None) -> None:
self.original = original.rstrip("/\\")
super().__init__(delete=delete)

View file

@ -1,15 +1,13 @@
"""Utilities related archives."""
from __future__ import annotations
"""Utilities related archives.
"""
import logging
import os
import shutil
import stat
import sys
import tarfile
import zipfile
from collections.abc import Iterable
from typing import Iterable, List, Optional
from zipfile import ZipInfo
from pip._internal.exceptions import InstallationError
@ -49,7 +47,7 @@ def current_umask() -> int:
return mask
def split_leading_dir(path: str) -> list[str]:
def split_leading_dir(path: str) -> List[str]:
path = path.lstrip("/").lstrip("\\")
if "/" in path and (
("\\" in path and path.find("/") < path.find("\\")) or "\\" not in path
@ -87,16 +85,12 @@ def is_within_directory(directory: str, target: str) -> bool:
return prefix == abs_directory
def _get_default_mode_plus_executable() -> int:
return 0o777 & ~current_umask() | 0o111
def set_extracted_file_to_default_mode_plus_executable(path: str) -> None:
"""
Make file present at path have execute for user/group/world
(chmod +x) is no-op on windows per python docs
"""
os.chmod(path, _get_default_mode_plus_executable())
os.chmod(path, (0o777 & ~current_umask() | 0o111))
def zip_item_is_executable(info: ZipInfo) -> bool:
@ -133,7 +127,7 @@ def unzip_file(filename: str, location: str, flatten: bool = True) -> None:
"outside target directory ({})"
)
raise InstallationError(message.format(filename, fn, location))
if fn.endswith(("/", "\\")):
if fn.endswith("/") or fn.endswith("\\"):
# A directory
ensure_dir(fn)
else:
@ -157,8 +151,8 @@ def untar_file(filename: str, location: str) -> None:
Untar the file (with path `filename`) to the destination `location`.
All files are written based on system defaults and umask (i.e. permissions
are not preserved), except that regular file members with any execute
permissions (user, group, or world) have "chmod +x" applied on top of the
default. Note that for windows, any execute changes using os.chmod are
permissions (user, group, or world) have "chmod +x" applied after being
written. Note that for windows, any execute changes using os.chmod are
no-ops per the python docs.
"""
ensure_dir(location)
@ -176,165 +170,66 @@ def untar_file(filename: str, location: str) -> None:
filename,
)
mode = "r:*"
tar = tarfile.open(filename, mode, encoding="utf-8") # type: ignore
tar = tarfile.open(filename, mode, encoding="utf-8")
try:
leading = has_leading_dir([member.name for member in tar.getmembers()])
# PEP 706 added `tarfile.data_filter`, and made some other changes to
# Python's tarfile module (see below). The features were backported to
# security releases.
try:
data_filter = tarfile.data_filter
except AttributeError:
_untar_without_filter(filename, location, tar, leading)
else:
default_mode_plus_executable = _get_default_mode_plus_executable()
for member in tar.getmembers():
fn = member.name
if leading:
# Strip the leading directory from all files in the archive,
# including hardlink targets (which are relative to the
# unpack location).
for member in tar.getmembers():
name_lead, name_rest = split_leading_dir(member.name)
member.name = name_rest
if member.islnk():
lnk_lead, lnk_rest = split_leading_dir(member.linkname)
if lnk_lead == name_lead:
member.linkname = lnk_rest
def pip_filter(member: tarfile.TarInfo, path: str) -> tarfile.TarInfo:
orig_mode = member.mode
try:
try:
member = data_filter(member, location)
except tarfile.LinkOutsideDestinationError:
if sys.version_info[:3] in {
(3, 9, 17),
(3, 10, 12),
(3, 11, 4),
}:
# The tarfile filter in specific Python versions
# raises LinkOutsideDestinationError on valid input
# (https://github.com/python/cpython/issues/107845)
# Ignore the error there, but do use the
# more lax `tar_filter`
member = tarfile.tar_filter(member, location)
else:
raise
except tarfile.TarError as exc:
message = "Invalid member in the tar file {}: {}"
# Filter error messages mention the member name.
# No need to add it here.
raise InstallationError(
message.format(
filename,
exc,
)
)
if member.isfile() and orig_mode & 0o111:
member.mode = default_mode_plus_executable
else:
# See PEP 706 note above.
# The PEP changed this from `int` to `Optional[int]`,
# where None means "use the default". Mypy doesn't
# know this yet.
member.mode = None # type: ignore [assignment]
return member
tar.extractall(location, filter=pip_filter)
finally:
tar.close()
def is_symlink_target_in_tar(tar: tarfile.TarFile, tarinfo: tarfile.TarInfo) -> bool:
"""Check if the file pointed to by the symbolic link is in the tar archive"""
linkname = os.path.join(os.path.dirname(tarinfo.name), tarinfo.linkname)
linkname = os.path.normpath(linkname)
linkname = linkname.replace("\\", "/")
try:
tar.getmember(linkname)
return True
except KeyError:
return False
def _untar_without_filter(
filename: str,
location: str,
tar: tarfile.TarFile,
leading: bool,
) -> None:
"""Fallback for Python without tarfile.data_filter"""
# NOTE: This function can be removed once pip requires CPython ≥ 3.12.
# PEP 706 added tarfile.data_filter, made tarfile extraction operations more secure.
# This feature is fully supported from CPython 3.12 onward.
for member in tar.getmembers():
fn = member.name
if leading:
fn = split_leading_dir(fn)[1]
path = os.path.join(location, fn)
if not is_within_directory(location, path):
message = (
"The tar file ({}) has a file ({}) trying to install "
"outside target directory ({})"
)
raise InstallationError(message.format(filename, path, location))
if member.isdir():
ensure_dir(path)
elif member.issym():
if not is_symlink_target_in_tar(tar, member):
fn = split_leading_dir(fn)[1]
path = os.path.join(location, fn)
if not is_within_directory(location, path):
message = (
"The tar file ({}) has a file ({}) trying to install "
"outside target directory ({})"
)
raise InstallationError(
message.format(filename, member.name, member.linkname)
)
try:
tar._extract_member(member, path)
except Exception as exc:
# Some corrupt tar files seem to produce this
# (specifically bad symlinks)
logger.warning(
"In the tar file %s the member %s is invalid: %s",
filename,
member.name,
exc,
)
continue
else:
try:
fp = tar.extractfile(member)
except (KeyError, AttributeError) as exc:
# Some corrupt tar files seem to produce this
# (specifically bad symlinks)
logger.warning(
"In the tar file %s the member %s is invalid: %s",
filename,
member.name,
exc,
)
continue
ensure_dir(os.path.dirname(path))
assert fp is not None
with open(path, "wb") as destfp:
shutil.copyfileobj(fp, destfp)
fp.close()
# Update the timestamp (useful for cython compiled files)
tar.utime(member, path)
# member have any execute permissions for user/group/world?
if member.mode & 0o111:
set_extracted_file_to_default_mode_plus_executable(path)
raise InstallationError(message.format(filename, path, location))
if member.isdir():
ensure_dir(path)
elif member.issym():
try:
tar._extract_member(member, path)
except Exception as exc:
# Some corrupt tar files seem to produce this
# (specifically bad symlinks)
logger.warning(
"In the tar file %s the member %s is invalid: %s",
filename,
member.name,
exc,
)
continue
else:
try:
fp = tar.extractfile(member)
except (KeyError, AttributeError) as exc:
# Some corrupt tar files seem to produce this
# (specifically bad symlinks)
logger.warning(
"In the tar file %s the member %s is invalid: %s",
filename,
member.name,
exc,
)
continue
ensure_dir(os.path.dirname(path))
assert fp is not None
with open(path, "wb") as destfp:
shutil.copyfileobj(fp, destfp)
fp.close()
# Update the timestamp (useful for cython compiled files)
tar.utime(member, path)
# member have any execute permissions for user/group/world?
if member.mode & 0o111:
set_extracted_file_to_default_mode_plus_executable(path)
finally:
tar.close()
def unpack_file(
filename: str,
location: str,
content_type: str | None = None,
content_type: Optional[str] = None,
) -> None:
filename = os.path.realpath(filename)
if (

View file

@ -2,17 +2,24 @@ import os
import string
import urllib.parse
import urllib.request
from typing import Optional
from .compat import WINDOWS
def get_url_scheme(url: str) -> Optional[str]:
if ":" not in url:
return None
return url.split(":", 1)[0].lower()
def path_to_url(path: str) -> str:
"""
Convert a path to a file: URL. The path will be made absolute and have
quoted path parts.
"""
path = os.path.normpath(os.path.abspath(path))
url = urllib.parse.urljoin("file://", urllib.request.pathname2url(path))
url = urllib.parse.urljoin("file:", urllib.request.pathname2url(path))
return url

View file

@ -1,10 +1,9 @@
from __future__ import annotations
import logging
import os
import re
import site
import sys
from typing import List, Optional
logger = logging.getLogger(__name__)
_INCLUDE_SYSTEM_SITE_PACKAGES_REGEX = re.compile(
@ -34,7 +33,7 @@ def running_under_virtualenv() -> bool:
return _running_under_venv() or _running_under_legacy_virtualenv()
def _get_pyvenv_cfg_lines() -> list[str] | None:
def _get_pyvenv_cfg_lines() -> Optional[List[str]]:
"""Reads {sys.prefix}/pyvenv.cfg and returns its contents as list of lines
Returns None, if it could not read/access the file.

View file

@ -1,8 +1,10 @@
"""Support functions for working with wheel files."""
"""Support functions for working with wheel files.
"""
import logging
from email.message import Message
from email.parser import Parser
from typing import Tuple
from zipfile import BadZipFile, ZipFile
from pip._vendor.packaging.utils import canonicalize_name
@ -15,7 +17,7 @@ VERSION_COMPATIBLE = (1, 0)
logger = logging.getLogger(__name__)
def parse_wheel(wheel_zip: ZipFile, name: str) -> tuple[str, Message]:
def parse_wheel(wheel_zip: ZipFile, name: str) -> Tuple[str, Message]:
"""Extract information from the provided wheel, ensuring it meets basic
standards.
@ -26,7 +28,7 @@ def parse_wheel(wheel_zip: ZipFile, name: str) -> tuple[str, Message]:
metadata = wheel_metadata(wheel_zip, info_dir)
version = wheel_version(metadata)
except UnsupportedWheel as e:
raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
raise UnsupportedWheel(f"{name} has an invalid wheel, {str(e)}")
check_compatibility(version, name)
@ -92,7 +94,7 @@ def wheel_metadata(source: ZipFile, dist_info_dir: str) -> Message:
return Parser().parsestr(wheel_text)
def wheel_version(wheel_data: Message) -> tuple[int, ...]:
def wheel_version(wheel_data: Message) -> Tuple[int, ...]:
"""Given WHEEL metadata, return the parsed Wheel-Version.
Otherwise, raise UnsupportedWheel.
"""
@ -108,7 +110,7 @@ def wheel_version(wheel_data: Message) -> tuple[int, ...]:
raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}")
def check_compatibility(version: tuple[int, ...], name: str) -> None:
def check_compatibility(version: Tuple[int, ...], name: str) -> None:
"""Raises errors or warns if called with an incompatible Wheel-Version.
pip should refuse to install a Wheel-Version that's a major series