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

@ -1,3 +1,4 @@
"""Subpackage containing all of pip's command line interface related code"""
"""Subpackage containing all of pip's command line interface related code
"""
# This file intentionally does not import submodules

View file

@ -1,13 +1,11 @@
"""Logic that powers autocompletion installed by ``pip completion``."""
from __future__ import annotations
"""Logic that powers autocompletion installed by ``pip completion``.
"""
import optparse
import os
import sys
from collections.abc import Iterable
from itertools import chain
from typing import Any
from typing import Any, Iterable, List, Optional
from pip._internal.cli.main_parser import create_main_parser
from pip._internal.commands import commands_dict, create_command
@ -19,10 +17,6 @@ def autocomplete() -> None:
# Don't complete if user hasn't sourced bash_completion file.
if "PIP_AUTO_COMPLETE" not in os.environ:
return
# Don't complete if autocompletion environment variables
# are not present
if not os.environ.get("COMP_WORDS") or not os.environ.get("COMP_CWORD"):
return
cwords = os.environ["COMP_WORDS"].split()[1:]
cword = int(os.environ["COMP_CWORD"])
try:
@ -35,7 +29,7 @@ def autocomplete() -> None:
options = []
# subcommand
subcommand_name: str | None = None
subcommand_name: Optional[str] = None
for word in cwords:
if word in subcommands:
subcommand_name = word
@ -103,12 +97,6 @@ def autocomplete() -> None:
if option[1] and option[0][:2] == "--":
opt_label += "="
print(opt_label)
# Complete sub-commands (unless one is already given).
if not any(name in cwords for name in subcommand.handler_map()):
for handler_name in subcommand.handler_map():
if handler_name.startswith(current):
print(handler_name)
else:
# show main parser options only when necessary
@ -130,8 +118,8 @@ def autocomplete() -> None:
def get_path_completion_type(
cwords: list[str], cword: int, opts: Iterable[Any]
) -> str | None:
cwords: List[str], cword: int, opts: Iterable[Any]
) -> Optional[str]:
"""Get the type of path completion (``file``, ``dir``, ``path`` or None)
:param cwords: same as the environmental variable ``COMP_WORDS``

View file

@ -1,7 +1,6 @@
"""Base Command class, and related routines"""
from __future__ import annotations
import functools
import logging
import logging.config
import optparse
@ -9,9 +8,8 @@ import os
import sys
import traceback
from optparse import Values
from typing import Callable
from typing import Any, Callable, List, Optional, Tuple
from pip._vendor.rich import reconfigure
from pip._vendor.rich import traceback as rich_traceback
from pip._internal.cli import cmdoptions
@ -30,6 +28,7 @@ from pip._internal.exceptions import (
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
UninstallationError,
)
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
@ -62,7 +61,7 @@ class Command(CommandContextMixIn):
isolated=isolated,
)
self.tempdir_registry: TempDirRegistry | None = None
self.tempdir_registry: Optional[TempDirRegistry] = None
# Commands should add options to this option group
optgroup_name = f"{self.name.capitalize()} Options"
@ -89,78 +88,21 @@ class Command(CommandContextMixIn):
# are present.
assert not hasattr(options, "no_index")
def run(self, options: Values, args: list[str]) -> int:
def run(self, options: Values, args: List[str]) -> int:
raise NotImplementedError
def _run_wrapper(self, level_number: int, options: Values, args: list[str]) -> int:
def _inner_run() -> int:
try:
return self.run(options, args)
finally:
self.handle_pip_version_check(options)
if options.debug_mode:
rich_traceback.install(show_locals=True)
return _inner_run()
try:
status = _inner_run()
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("%s", exc, extra={"rich": True})
logger.debug("Exception information:", exc_info=True)
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
InstallationError,
BadCommand,
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
return UNKNOWN_ERROR
def parse_args(self, args: list[str]) -> tuple[Values, list[str]]:
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
# factored out for testability
return self.parser.parse_args(args)
def main(self, args: list[str]) -> int:
def main(self, args: List[str]) -> int:
try:
with self.main_context():
return self._main(args)
finally:
logging.shutdown()
def _main(self, args: list[str]) -> int:
def _main(self, args: List[str]) -> int:
# We must initialize this before the tempdir manager, otherwise the
# configuration would not be accessible by the time we clean up the
# tempdir manager.
@ -173,13 +115,7 @@ class Command(CommandContextMixIn):
# Set verbosity so that it can be used elsewhere.
self.verbosity = options.verbose - options.quiet
if options.debug_mode:
self.verbosity = 2
if hasattr(options, "progress_bar") and options.progress_bar == "auto":
options.progress_bar = "on" if self.verbosity >= 0 else "off"
reconfigure(no_color=options.no_color)
level_number = setup_logging(
verbosity=self.verbosity,
no_color=options.no_color,
@ -235,10 +171,66 @@ class Command(CommandContextMixIn):
)
options.cache_dir = None
return self._run_wrapper(level_number, options, args)
def intercepts_unhandled_exc(
run_func: Callable[..., int]
) -> Callable[..., int]:
@functools.wraps(run_func)
def exc_logging_wrapper(*args: Any) -> int:
try:
status = run_func(*args)
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("%s", exc, extra={"rich": True})
logger.debug("Exception information:", exc_info=True)
def handler_map(self) -> dict[str, Callable[[Values, list[str]], None]]:
"""
map of names to handler actions for commands with sub-actions
"""
return {}
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
InstallationError,
UninstallationError,
BadCommand,
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
return UNKNOWN_ERROR
return exc_logging_wrapper
try:
if not options.debug_mode:
run = intercepts_unhandled_exc(self.run)
else:
run = self.run
rich_traceback.install(show_locals=True)
return run(options, args)
finally:
self.handle_pip_version_check(options)

View file

@ -9,16 +9,15 @@ pass on state. To be consistent, all options will follow this design.
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
from __future__ import annotations
import importlib.util
import logging
import os
import pathlib
import textwrap
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values
from textwrap import dedent
from typing import Any, Callable
from typing import Any, Callable, Dict, Optional, Tuple
from pip._vendor.packaging.utils import canonicalize_name
@ -48,7 +47,7 @@ def raise_option_error(parser: OptionParser, option: Option, msg: str) -> None:
parser.error(msg)
def make_option_group(group: dict[str, Any], parser: ConfigOptionParser) -> OptionGroup:
def make_option_group(group: Dict[str, Any], parser: ConfigOptionParser) -> OptionGroup:
"""
Return an OptionGroup object
group -- assumed to be dict with 'name' and 'options' keys
@ -100,29 +99,6 @@ def check_dist_restriction(options: Values, check_target: bool = False) -> None:
)
def check_build_constraints(options: Values) -> None:
"""Function for validating build constraints options.
:param options: The OptionParser options.
"""
if hasattr(options, "build_constraints") and options.build_constraints:
if not options.build_isolation:
raise CommandError(
"--build-constraint cannot be used with --no-build-isolation."
)
# Import here to avoid circular imports
from pip._internal.network.session import PipSession
from pip._internal.req.req_file import get_file_content
# Eagerly check build constraints file contents
# is valid so that we don't fail in when trying
# to check constraints in isolated build process
with PipSession() as session:
for constraint_file in options.build_constraints:
get_file_content(constraint_file, session)
def _path_option_check(option: Option, opt: str, value: str) -> str:
return os.path.expanduser(value)
@ -183,7 +159,8 @@ require_virtualenv: Callable[..., Option] = partial(
action="store_true",
default=False,
help=(
"Allow pip to only run in a virtual environment; exit with an error otherwise."
"Allow pip to only run in a virtual environment; "
"exit with an error otherwise."
),
)
@ -249,13 +226,9 @@ progress_bar: Callable[..., Option] = partial(
"--progress-bar",
dest="progress_bar",
type="choice",
choices=["auto", "on", "off", "raw"],
default="auto",
help=(
"Specify whether the progress bar should be used. In 'auto'"
" mode, --quiet will suppress all progress bars."
" [auto, on, off, raw] (default: auto)"
),
choices=["on", "off"],
default="on",
help="Specify whether the progress bar should be used [on, off] (default: on)",
)
log: Callable[..., Option] = partial(
@ -287,8 +260,8 @@ keyring_provider: Callable[..., Option] = partial(
default="auto",
help=(
"Enable the credential lookup via the keyring library if user input is allowed."
" Specify which mechanism to use [auto, disabled, import, subprocess]."
" (default: %default)"
" Specify which mechanism to use [disabled, import, subprocess]."
" (default: disabled)"
),
)
@ -307,17 +280,8 @@ retries: Callable[..., Option] = partial(
dest="retries",
type="int",
default=5,
help="Maximum attempts to establish a new HTTP connection. (default: %default)",
)
resume_retries: Callable[..., Option] = partial(
Option,
"--resume-retries",
dest="resume_retries",
type="int",
default=5,
help="Maximum attempts to resume or restart an incomplete download. "
"(default: %default)",
help="Maximum number of retries each connection should attempt "
"(default %default times).",
)
timeout: Callable[..., Option] = partial(
@ -451,21 +415,6 @@ def constraints() -> Option:
)
def build_constraints() -> Option:
return Option(
"--build-constraint",
dest="build_constraints",
action="append",
type="str",
default=[],
metavar="file",
help=(
"Constrain build dependencies using the given constraints file. "
"This option can be used multiple times."
),
)
def requirements() -> Option:
return Option(
"-r",
@ -596,7 +545,7 @@ platforms: Callable[..., Option] = partial(
# This was made a separate function for unit-testing purposes.
def _convert_python_version(value: str) -> tuple[tuple[int, ...], str | None]:
def _convert_python_version(value: str) -> Tuple[Tuple[int, ...], Optional[str]]:
"""
Convert a version string like "3", "37", or "3.7.3" into a tuple of ints.
@ -784,46 +733,6 @@ no_deps: Callable[..., Option] = partial(
help="Don't install package dependencies.",
)
def _handle_dependency_group(
option: Option, opt: str, value: str, parser: OptionParser
) -> None:
"""
Process a value provided for the --group option.
Splits on the rightmost ":", and validates that the path (if present) ends
in `pyproject.toml`. Defaults the path to `pyproject.toml` when one is not given.
`:` cannot appear in dependency group names, so this is a safe and simple parse.
This is an optparse.Option callback for the dependency_groups option.
"""
path, sep, groupname = value.rpartition(":")
if not sep:
path = "pyproject.toml"
else:
# check for 'pyproject.toml' filenames using pathlib
if pathlib.PurePath(path).name != "pyproject.toml":
msg = "group paths use 'pyproject.toml' filenames"
raise_option_error(parser, option=option, msg=msg)
parser.values.dependency_groups.append((path, groupname))
dependency_groups: Callable[..., Option] = partial(
Option,
"--group",
dest="dependency_groups",
default=[],
type=str,
action="callback",
callback=_handle_dependency_group,
metavar="[path:]group",
help='Install a named dependency-group from a "pyproject.toml" file. '
'If a path is given, the name of the file must be "pyproject.toml". '
'Defaults to using "pyproject.toml" in the current directory.',
)
ignore_requires_python: Callable[..., Option] = partial(
Option,
"--ignore-requires-python",
@ -849,16 +758,62 @@ check_build_deps: Callable[..., Option] = partial(
dest="check_build_deps",
action="store_true",
default=False,
help="Check the build dependencies.",
help="Check the build dependencies when PEP517 is used.",
)
def _handle_no_use_pep517(
option: Option, opt: str, value: str, parser: OptionParser
) -> None:
"""
Process a value provided for the --no-use-pep517 option.
This is an optparse.Option callback for the no_use_pep517 option.
"""
# Since --no-use-pep517 doesn't accept arguments, the value argument
# will be None if --no-use-pep517 is passed via the command-line.
# However, the value can be non-None if the option is triggered e.g.
# by an environment variable, for example "PIP_NO_USE_PEP517=true".
if value is not None:
msg = """A value was passed for --no-use-pep517,
probably using either the PIP_NO_USE_PEP517 environment variable
or the "no-use-pep517" config file option. Use an appropriate value
of the PIP_USE_PEP517 environment variable or the "use-pep517"
config file option instead.
"""
raise_option_error(parser, option=option, msg=msg)
# If user doesn't wish to use pep517, we check if setuptools and wheel are installed
# and raise error if it is not.
packages = ("setuptools", "wheel")
if not all(importlib.util.find_spec(package) for package in packages):
msg = (
f"It is not possible to use --no-use-pep517 "
f"without {' and '.join(packages)} installed."
)
raise_option_error(parser, option=option, msg=msg)
# Otherwise, --no-use-pep517 was passed via the command-line.
parser.values.use_pep517 = False
use_pep517: Any = partial(
Option,
"--use-pep517",
dest="use_pep517",
action="store_true",
default=True,
default=None,
help="Use PEP 517 for building source distributions "
"(use --no-use-pep517 to force legacy behaviour).",
)
no_use_pep517: Any = partial(
Option,
"--no-use-pep517",
dest="use_pep517",
action="callback",
callback=_handle_no_use_pep517,
default=None,
help=SUPPRESS_HELP,
)
@ -891,11 +846,30 @@ config_settings: Callable[..., Option] = partial(
action="callback",
callback=_handle_config_settings,
metavar="settings",
help="Configuration settings to be passed to the build backend. "
help="Configuration settings to be passed to the PEP 517 build backend. "
"Settings take the form KEY=VALUE. Use multiple --config-settings options "
"to pass multiple keys to the backend.",
)
build_options: Callable[..., Option] = partial(
Option,
"--build-option",
dest="build_options",
metavar="options",
action="append",
help="Extra arguments to be supplied to 'setup.py bdist_wheel'.",
)
global_options: Callable[..., Option] = partial(
Option,
"--global-option",
dest="global_options",
action="append",
metavar="options",
help="Extra global options to be supplied to the setup.py "
"call before the install or bdist_wheel command.",
)
no_clean: Callable[..., Option] = partial(
Option,
"--no-clean",
@ -913,20 +887,12 @@ pre: Callable[..., Option] = partial(
"pip only finds stable versions.",
)
json: Callable[..., Option] = partial(
Option,
"--json",
action="store_true",
default=False,
help="Output data in a machine-readable JSON format.",
)
disable_pip_version_check: Callable[..., Option] = partial(
Option,
"--disable-pip-version-check",
dest="disable_pip_version_check",
action="store_true",
default=False,
default=True,
help="Don't periodically check PyPI to determine whether a new version "
"of pip is available for download. Implied with --no-index.",
)
@ -937,7 +903,7 @@ root_user_action: Callable[..., Option] = partial(
dest="root_user_action",
default="warn",
choices=["warn", "ignore"],
help="Action if pip is run as a root user [warn, ignore] (default: warn)",
help="Action if pip is run as a root user. By default, a warning message is shown.",
)
@ -1024,13 +990,12 @@ no_python_version_warning: Callable[..., Option] = partial(
dest="no_python_version_warning",
action="store_true",
default=False,
help=SUPPRESS_HELP, # No-op, a hold-over from the Python 2->3 transition.
help="Silence deprecation warnings for upcoming unsupported Pythons.",
)
# Features that are now always on. A warning is printed if they are used.
ALWAYS_ENABLED_FEATURES = [
"truststore", # always on since 24.2
"no-binary-enable-wheel-cache", # always on since 23.1
]
@ -1043,7 +1008,7 @@ use_new_feature: Callable[..., Option] = partial(
default=[],
choices=[
"fast-deps",
"build-constraint",
"truststore",
]
+ ALWAYS_ENABLED_FEATURES,
help="Enable new functionality, that may be backward incompatible.",
@ -1058,16 +1023,16 @@ use_deprecated_feature: Callable[..., Option] = partial(
default=[],
choices=[
"legacy-resolver",
"legacy-certs",
],
help=("Enable deprecated functionality, that will be removed in the future."),
)
##########
# groups #
##########
general_group: dict[str, Any] = {
general_group: Dict[str, Any] = {
"name": "General Options",
"options": [
help_,
@ -1095,11 +1060,10 @@ general_group: dict[str, Any] = {
no_python_version_warning,
use_new_feature,
use_deprecated_feature,
resume_retries,
],
}
index_group: dict[str, Any] = {
index_group: Dict[str, Any] = {
"name": "Package Index Options",
"options": [
index_url,

View file

@ -1,6 +1,5 @@
from collections.abc import Generator
from contextlib import AbstractContextManager, ExitStack, contextmanager
from typing import TypeVar
from contextlib import ExitStack, contextmanager
from typing import ContextManager, Generator, TypeVar
_T = TypeVar("_T", covariant=True)
@ -22,7 +21,7 @@ class CommandContextMixIn:
finally:
self._in_main_context = False
def enter_context(self, context_provider: AbstractContextManager[_T]) -> _T:
def enter_context(self, context_provider: ContextManager[_T]) -> _T:
assert self._in_main_context
return self._main_context.enter_context(context_provider)

View file

@ -1,175 +0,0 @@
"""
Contains command classes which may interact with an index / the network.
Unlike its sister module, req_command, this module still uses lazy imports
so commands which don't always hit the network (e.g. list w/o --outdated or
--uptodate) don't need waste time importing PipSession and friends.
"""
from __future__ import annotations
import logging
import os
import sys
from functools import lru_cache
from optparse import Values
from typing import TYPE_CHECKING
from pip._vendor import certifi
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
if TYPE_CHECKING:
from ssl import SSLContext
from pip._internal.network.session import PipSession
logger = logging.getLogger(__name__)
@lru_cache
def _create_truststore_ssl_context() -> SSLContext | None:
if sys.version_info < (3, 10):
logger.debug("Disabling truststore because Python version isn't 3.10+")
return None
try:
import ssl
except ImportError:
logger.warning("Disabling truststore since ssl support is missing")
return None
try:
from pip._vendor import truststore
except ImportError:
logger.warning("Disabling truststore because platform isn't supported")
return None
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.load_verify_locations(certifi.where())
return ctx
class SessionCommandMixin(CommandContextMixIn):
"""
A class mixin for command classes needing _build_session().
"""
def __init__(self) -> None:
super().__init__()
self._session: PipSession | None = None
@classmethod
def _get_index_urls(cls, options: Values) -> list[str] | None:
"""Return a list of index urls from user-provided options."""
index_urls = []
if not getattr(options, "no_index", False):
url = getattr(options, "index_url", None)
if url:
index_urls.append(url)
urls = getattr(options, "extra_index_urls", None)
if urls:
index_urls.extend(urls)
# Return None rather than an empty list
return index_urls or None
def get_default_session(self, options: Values) -> PipSession:
"""Get a default-managed session."""
if self._session is None:
self._session = self.enter_context(self._build_session(options))
# there's no type annotation on requests.Session, so it's
# automatically ContextManager[Any] and self._session becomes Any,
# then https://github.com/python/mypy/issues/7696 kicks in
assert self._session is not None
return self._session
def _build_session(
self,
options: Values,
retries: int | None = None,
timeout: int | None = None,
) -> PipSession:
from pip._internal.network.session import PipSession
cache_dir = options.cache_dir
assert not cache_dir or os.path.isabs(cache_dir)
if "legacy-certs" not in options.deprecated_features_enabled:
ssl_context = _create_truststore_ssl_context()
else:
ssl_context = None
session = PipSession(
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
index_urls=self._get_index_urls(options),
ssl_context=ssl_context,
)
# Handle custom ca-bundles from the user
if options.cert:
session.verify = options.cert
# Handle SSL client certificate
if options.client_cert:
session.cert = options.client_cert
# Handle timeouts
if options.timeout or timeout:
session.timeout = timeout if timeout is not None else options.timeout
# Handle configured proxies
if options.proxy:
session.proxies = {
"http": options.proxy,
"https": options.proxy,
}
session.trust_env = False
session.pip_proxy = options.proxy
# Determine if we can prompt the user for authentication or not
session.auth.prompting = not options.no_input
session.auth.keyring_provider = options.keyring_provider
return session
def _pip_self_version_check(session: PipSession, options: Values) -> None:
from pip._internal.self_outdated_check import pip_self_version_check as check
check(session, options)
class IndexGroupCommand(Command, SessionCommandMixin):
"""
Abstract base class for commands with the index_group options.
This also corresponds to the commands that permit the pip version check.
"""
def handle_pip_version_check(self, options: Values) -> None:
"""
Do the pip version check if not disabled.
This overrides the default behavior of not doing the check.
"""
# Make sure the index_group options are present.
assert hasattr(options, "no_index")
if options.disable_pip_version_check or options.no_index:
return
try:
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options,
retries=0,
timeout=min(5, options.timeout),
)
with session:
_pip_self_version_check(session, options)
except Exception:
logger.warning("There was an error checking the latest version of pip.")
logger.debug("See below for error", exc_info=True)

View file

@ -1,12 +1,11 @@
"""Primary application entrypoint."""
from __future__ import annotations
"""Primary application entrypoint.
"""
import locale
import logging
import os
import sys
import warnings
from typing import List, Optional
from pip._internal.cli.autocompletion import autocomplete
from pip._internal.cli.main_parser import parse_command
@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
# main, this should not be an issue in practice.
def main(args: list[str] | None = None) -> int:
def main(args: Optional[List[str]] = None) -> int:
if args is None:
args = sys.argv[1:]

View file

@ -1,10 +1,10 @@
"""A single place for constructing and exposing the main parser"""
from __future__ import annotations
"""A single place for constructing and exposing the main parser
"""
import os
import subprocess
import sys
from typing import List, Optional, Tuple
from pip._internal.build_env import get_runnable_pip
from pip._internal.cli import cmdoptions
@ -47,7 +47,7 @@ def create_main_parser() -> ConfigOptionParser:
return parser
def identify_python_interpreter(python: str) -> str | None:
def identify_python_interpreter(python: str) -> Optional[str]:
# If the named file exists, use it.
# If it's a directory, assume it's a virtual environment and
# look for the environment's Python executable.
@ -66,7 +66,7 @@ def identify_python_interpreter(python: str) -> str | None:
return None
def parse_command(args: list[str]) -> tuple[str, list[str]]:
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this

View file

@ -1,15 +1,12 @@
"""Base option parser setup"""
from __future__ import annotations
import logging
import optparse
import shutil
import sys
import textwrap
from collections.abc import Generator
from contextlib import suppress
from typing import Any, NoReturn
from typing import Any, Dict, Generator, List, Tuple
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.configuration import Configuration, ConfigurationError
@ -70,7 +67,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " "))
return msg
def format_description(self, description: str | None) -> str:
def format_description(self, description: str) -> str:
# leave full control over description to us
if description:
if hasattr(self.parser, "main"):
@ -88,7 +85,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
else:
return ""
def format_epilog(self, epilog: str | None) -> str:
def format_epilog(self, epilog: str) -> str:
# leave full control over epilog to us
if epilog:
return epilog
@ -145,7 +142,7 @@ class CustomOptionParser(optparse.OptionParser):
return group
@property
def option_list_all(self) -> list[optparse.Option]:
def option_list_all(self) -> List[optparse.Option]:
"""Get a list of all options, including those in option groups."""
res = self.option_list[:]
for i in self.option_groups:
@ -180,34 +177,33 @@ class ConfigOptionParser(CustomOptionParser):
def _get_ordered_configuration_items(
self,
) -> Generator[tuple[str, Any], None, None]:
) -> Generator[Tuple[str, Any], None, None]:
# Configuration gives keys in an unordered manner. Order them.
override_order = ["global", self.name, ":env:"]
# Pool the options into different groups
section_items: dict[str, list[tuple[str, Any]]] = {
section_items: Dict[str, List[Tuple[str, Any]]] = {
name: [] for name in override_order
}
for section_key, val in self.config.items():
# ignore empty values
if not val:
logger.debug(
"Ignoring configuration key '%s' as it's value is empty.",
section_key,
)
continue
for _, value in self.config.items(): # noqa: PERF102
for section_key, val in value.items():
# ignore empty values
if not val:
logger.debug(
"Ignoring configuration key '%s' as its value is empty.",
section_key,
)
continue
section, key = section_key.split(".", 1)
if section in override_order:
section_items[section].append((key, val))
section, key = section_key.split(".", 1)
if section in override_order:
section_items[section].append((key, val))
# Yield each group in their override order
for section in override_order:
yield from section_items[section]
for key, val in section_items[section]:
yield key, val
def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
def _update_defaults(self, defaults: Dict[str, Any]) -> Dict[str, Any]:
"""Updates the given defaults with values from the config files and
the environ. Does a little special handling for certain types of
options (lists)."""
@ -293,6 +289,6 @@ class ConfigOptionParser(CustomOptionParser):
defaults[option.dest] = option.check_value(opt_str, default)
return optparse.Values(defaults)
def error(self, msg: str) -> NoReturn:
def error(self, msg: str) -> None:
self.print_usage(sys.stderr)
self.exit(UNKNOWN_ERROR, f"{msg}\n")

View file

@ -1,15 +1,10 @@
from __future__ import annotations
import functools
import sys
from collections.abc import Generator, Iterable, Iterator
from typing import Callable, Literal, TypeVar
from typing import Callable, Generator, Iterable, Iterator, Optional, Tuple
from pip._vendor.rich.progress import (
BarColumn,
DownloadColumn,
FileSizeColumn,
MofNCompleteColumn,
Progress,
ProgressColumn,
SpinnerColumn,
@ -19,27 +14,22 @@ from pip._vendor.rich.progress import (
TransferSpeedColumn,
)
from pip._internal.cli.spinners import RateLimiter
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.logging import get_console, get_indentation
from pip._internal.utils.logging import get_indentation
T = TypeVar("T")
ProgressRenderer = Callable[[Iterable[T]], Iterator[T]]
BarType = Literal["on", "off", "raw"]
DownloadProgressRenderer = Callable[[Iterable[bytes]], Iterator[bytes]]
def _rich_download_progress_bar(
def _rich_progress_bar(
iterable: Iterable[bytes],
*,
bar_type: BarType,
size: int | None,
initial_progress: int | None = None,
bar_type: str,
size: int,
) -> Generator[bytes, None, None]:
assert bar_type == "on", "This should only be used in the default mode."
if not size:
total = float("inf")
columns: tuple[ProgressColumn, ...] = (
columns: Tuple[ProgressColumn, ...] = (
TextColumn("[progress.description]{task.description}"),
SpinnerColumn("line", speed=1.5),
FileSizeColumn(),
@ -53,99 +43,26 @@ def _rich_download_progress_bar(
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TextColumn("{task.fields[time_description]}"),
TimeRemainingColumn(elapsed_when_finished=True),
TextColumn("eta"),
TimeRemainingColumn(),
)
progress = Progress(*columns, refresh_per_second=5)
task_id = progress.add_task(
" " * (get_indentation() + 2), total=total, time_description="eta"
)
if initial_progress is not None:
progress.update(task_id, advance=initial_progress)
progress = Progress(*columns, refresh_per_second=30)
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
with progress:
for chunk in iterable:
yield chunk
progress.update(task_id, advance=len(chunk))
progress.update(task_id, time_description="")
def _rich_install_progress_bar(
iterable: Iterable[InstallRequirement], *, total: int
) -> Iterator[InstallRequirement]:
columns = (
TextColumn("{task.fields[indent]}"),
BarColumn(),
MofNCompleteColumn(),
TextColumn("{task.description}"),
)
console = get_console()
bar = Progress(*columns, refresh_per_second=6, console=console, transient=True)
# Hiding the progress bar at initialization forces a refresh cycle to occur
# until the bar appears, avoiding very short flashes.
task = bar.add_task("", total=total, indent=" " * get_indentation(), visible=False)
with bar:
for req in iterable:
bar.update(task, description=rf"\[{req.name}]", visible=True)
yield req
bar.advance(task)
def _raw_progress_bar(
iterable: Iterable[bytes],
*,
size: int | None,
initial_progress: int | None = None,
) -> Generator[bytes, None, None]:
def write_progress(current: int, total: int) -> None:
sys.stdout.write(f"Progress {current} of {total}\n")
sys.stdout.flush()
current = initial_progress or 0
total = size or 0
rate_limiter = RateLimiter(0.25)
write_progress(current, total)
for chunk in iterable:
current += len(chunk)
if rate_limiter.ready() or current == total:
write_progress(current, total)
rate_limiter.reset()
yield chunk
def get_download_progress_renderer(
*, bar_type: BarType, size: int | None = None, initial_progress: int | None = None
) -> ProgressRenderer[bytes]:
*, bar_type: str, size: Optional[int] = None
) -> DownloadProgressRenderer:
"""Get an object that can be used to render the download progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
return functools.partial(
_rich_download_progress_bar,
bar_type=bar_type,
size=size,
initial_progress=initial_progress,
)
elif bar_type == "raw":
return functools.partial(
_raw_progress_bar,
size=size,
initial_progress=initial_progress,
)
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
else:
return iter # no-op, when passed an iterator
def get_install_progress_renderer(
*, bar_type: BarType, total: int
) -> ProgressRenderer[InstallRequirement]:
"""Get an object that can be used to render the install progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
return functools.partial(_rich_install_progress_bar, total=total)
else:
return iter

View file

@ -1,23 +1,21 @@
"""Contains the RequirementCommand base class.
"""Contains the Command base classes that depend on PipSession.
This class is in a separate module so the commands that do not always
need PackageFinder capability don't unnecessarily import the
The classes in this module are in a separate module so the commands not
needing download / PackageFinder capability don't unnecessarily import the
PackageFinder machinery and all its vendored dependencies, etc.
"""
from __future__ import annotations
import logging
import os
import sys
from functools import partial
from optparse import Values
from typing import Any, Callable, TypeVar
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
from pip._internal.build_env import SubprocessBuildEnvironmentInstaller
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.index_command import IndexGroupCommand
from pip._internal.cli.index_command import SessionCommandMixin as SessionCommandMixin
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
from pip._internal.exceptions import CommandError, PreviousBuildDirError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
@ -32,27 +30,162 @@ from pip._internal.req.constructors import (
install_req_from_parsed_requirement,
install_req_from_req_string,
)
from pip._internal.req.req_dependency_group import parse_dependency_groups
from pip._internal.req.req_file import parse_requirements
from pip._internal.req.req_install import InstallRequirement
from pip._internal.resolution.base import BaseResolver
from pip._internal.self_outdated_check import pip_self_version_check
from pip._internal.utils.temp_dir import (
TempDirectory,
TempDirectoryTypeRegistry,
tempdir_kinds,
)
from pip._internal.utils.virtualenv import running_under_virtualenv
if TYPE_CHECKING:
from ssl import SSLContext
logger = logging.getLogger(__name__)
def should_ignore_regular_constraints(options: Values) -> bool:
def _create_truststore_ssl_context() -> Optional["SSLContext"]:
if sys.version_info < (3, 10):
raise CommandError("The truststore feature is only available for Python 3.10+")
try:
import ssl
except ImportError:
logger.warning("Disabling truststore since ssl support is missing")
return None
try:
from pip._vendor import truststore
except ImportError as e:
raise CommandError(f"The truststore feature is unavailable: {e}")
return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
class SessionCommandMixin(CommandContextMixIn):
"""
Check if regular constraints should be ignored because
we are in a isolated build process and build constraints
feature is enabled but no build constraints were passed.
A class mixin for command classes needing _build_session().
"""
return os.environ.get("_PIP_IN_BUILD_IGNORE_CONSTRAINTS") == "1"
def __init__(self) -> None:
super().__init__()
self._session: Optional[PipSession] = None
@classmethod
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
"""Return a list of index urls from user-provided options."""
index_urls = []
if not getattr(options, "no_index", False):
url = getattr(options, "index_url", None)
if url:
index_urls.append(url)
urls = getattr(options, "extra_index_urls", None)
if urls:
index_urls.extend(urls)
# Return None rather than an empty list
return index_urls or None
def get_default_session(self, options: Values) -> PipSession:
"""Get a default-managed session."""
if self._session is None:
self._session = self.enter_context(self._build_session(options))
# there's no type annotation on requests.Session, so it's
# automatically ContextManager[Any] and self._session becomes Any,
# then https://github.com/python/mypy/issues/7696 kicks in
assert self._session is not None
return self._session
def _build_session(
self,
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
fallback_to_certifi: bool = False,
) -> PipSession:
cache_dir = options.cache_dir
assert not cache_dir or os.path.isabs(cache_dir)
if "truststore" in options.features_enabled:
try:
ssl_context = _create_truststore_ssl_context()
except Exception:
if not fallback_to_certifi:
raise
ssl_context = None
else:
ssl_context = None
session = PipSession(
cache=os.path.join(cache_dir, "http-v2") if cache_dir else None,
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
index_urls=self._get_index_urls(options),
ssl_context=ssl_context,
)
# Handle custom ca-bundles from the user
if options.cert:
session.verify = options.cert
# Handle SSL client certificate
if options.client_cert:
session.cert = options.client_cert
# Handle timeouts
if options.timeout or timeout:
session.timeout = timeout if timeout is not None else options.timeout
# Handle configured proxies
if options.proxy:
session.proxies = {
"http": options.proxy,
"https": options.proxy,
}
# Determine if we can prompt the user for authentication or not
session.auth.prompting = not options.no_input
session.auth.keyring_provider = options.keyring_provider
return session
class IndexGroupCommand(Command, SessionCommandMixin):
"""
Abstract base class for commands with the index_group options.
This also corresponds to the commands that permit the pip version check.
"""
def handle_pip_version_check(self, options: Values) -> None:
"""
Do the pip version check if not disabled.
This overrides the default behavior of not doing the check.
"""
# Make sure the index_group options are present.
assert hasattr(options, "no_index")
if options.disable_pip_version_check or options.no_index:
return
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options,
retries=0,
timeout=min(5, options.timeout),
# This is set to ensure the function does not fail when truststore is
# specified in use-feature but cannot be loaded. This usually raises a
# CommandError and shows a nice user-facing error, but this function is not
# called in that try-except block.
fallback_to_certifi=True,
)
with session:
pip_self_version_check(session, options)
KEEPABLE_TEMPDIR_TYPES = [
@ -62,12 +195,37 @@ KEEPABLE_TEMPDIR_TYPES = [
]
_CommandT = TypeVar("_CommandT", bound="RequirementCommand")
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. "
"It is recommended to use a virtual environment instead: "
"https://pip.pypa.io/warnings/venv"
)
def with_cleanup(
func: Callable[[_CommandT, Values, list[str]], int],
) -> Callable[[_CommandT, Values, list[str]], int]:
def with_cleanup(func: Any) -> Any:
"""Decorator for common logic related to managing temporary
directories.
"""
@ -76,7 +234,9 @@ def with_cleanup(
for t in KEEPABLE_TEMPDIR_TYPES:
registry.set_delete(t, False)
def wrapper(self: _CommandT, options: Values, args: list[str]) -> int:
def wrapper(
self: RequirementCommand, options: Values, args: List[Any]
) -> Optional[int]:
assert self.tempdir_registry is not None
if options.no_clean:
configure_tempdir_registry(self.tempdir_registry)
@ -97,7 +257,6 @@ class RequirementCommand(IndexGroupCommand):
def __init__(self, *args: Any, **kw: Any) -> None:
super().__init__(*args, **kw)
self.cmd_opts.add_option(cmdoptions.dependency_groups())
self.cmd_opts.add_option(cmdoptions.no_clean())
@staticmethod
@ -117,7 +276,7 @@ class RequirementCommand(IndexGroupCommand):
session: PipSession,
finder: PackageFinder,
use_user_site: bool,
download_dir: str | None = None,
download_dir: Optional[str] = None,
verbosity: int = 0,
) -> RequirementPreparer:
"""
@ -146,22 +305,11 @@ class RequirementCommand(IndexGroupCommand):
"fast-deps has no effect when used with the legacy resolver."
)
# Handle build constraints
build_constraints = getattr(options, "build_constraints", [])
build_constraint_feature_enabled = (
"build-constraint" in options.features_enabled
)
return RequirementPreparer(
build_dir=temp_build_dir_path,
src_dir=options.src_dir,
download_dir=download_dir,
build_isolation=options.build_isolation,
build_isolation_installer=SubprocessBuildEnvironmentInstaller(
finder,
build_constraints=build_constraints,
build_constraint_feature_enabled=build_constraint_feature_enabled,
),
check_build_deps=options.check_build_deps,
build_tracker=build_tracker,
session=session,
@ -172,7 +320,6 @@ class RequirementCommand(IndexGroupCommand):
lazy_wheel=lazy_wheel,
verbosity=verbosity,
legacy_resolver=legacy_resolver,
resume_retries=options.resume_retries,
)
@classmethod
@ -181,13 +328,14 @@ class RequirementCommand(IndexGroupCommand):
preparer: RequirementPreparer,
finder: PackageFinder,
options: Values,
wheel_cache: WheelCache | None = None,
wheel_cache: Optional[WheelCache] = None,
use_user_site: bool = False,
ignore_installed: bool = True,
ignore_requires_python: bool = False,
force_reinstall: bool = False,
upgrade_strategy: str = "to-satisfy-only",
py_version_info: tuple[int, ...] | None = None,
use_pep517: Optional[bool] = None,
py_version_info: Optional[Tuple[int, ...]] = None,
) -> BaseResolver:
"""
Create a Resolver instance for the given parameters.
@ -195,6 +343,7 @@ class RequirementCommand(IndexGroupCommand):
make_install_req = partial(
install_req_from_req_string,
isolated=options.isolated_mode,
use_pep517=use_pep517,
)
resolver_variant = cls.determine_resolver_variant(options)
# The long import name and duplicated invocation is needed to convince
@ -234,56 +383,47 @@ class RequirementCommand(IndexGroupCommand):
def get_requirements(
self,
args: list[str],
args: List[str],
options: Values,
finder: PackageFinder,
session: PipSession,
) -> list[InstallRequirement]:
) -> List[InstallRequirement]:
"""
Parse command-line arguments into the corresponding requirements.
"""
requirements: list[InstallRequirement] = []
if not should_ignore_regular_constraints(options):
for filename in options.constraints:
for parsed_req in parse_requirements(
filename,
constraint=True,
finder=finder,
options=options,
session=session,
):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode,
user_supplied=False,
)
requirements.append(req_to_add)
requirements: List[InstallRequirement] = []
for filename in options.constraints:
for parsed_req in parse_requirements(
filename,
constraint=True,
finder=finder,
options=options,
session=session,
):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode,
user_supplied=False,
)
requirements.append(req_to_add)
for req in args:
req_to_add = install_req_from_line(
req,
comes_from=None,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
user_supplied=True,
config_settings=getattr(options, "config_settings", None),
)
requirements.append(req_to_add)
if options.dependency_groups:
for req in parse_dependency_groups(options.dependency_groups):
req_to_add = install_req_from_req_string(
req,
isolated=options.isolated_mode,
user_supplied=True,
)
requirements.append(req_to_add)
for req in options.editables:
req_to_add = install_req_from_editable(
req,
user_supplied=True,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
config_settings=getattr(options, "config_settings", None),
)
requirements.append(req_to_add)
@ -296,12 +436,11 @@ class RequirementCommand(IndexGroupCommand):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
isolated=options.isolated_mode,
use_pep517=options.use_pep517,
user_supplied=True,
config_settings=(
parsed_req.options.get("config_settings")
if parsed_req.options
else None
),
config_settings=parsed_req.options.get("config_settings")
if parsed_req.options
else None,
)
requirements.append(req_to_add)
@ -309,12 +448,7 @@ class RequirementCommand(IndexGroupCommand):
if any(req.has_hash_options for req in requirements):
options.require_hashes = True
if not (
args
or options.editables
or options.requirements
or options.dependency_groups
):
if not (args or options.editables or options.requirements):
opts = {"name": self.name}
if options.find_links:
raise CommandError(
@ -346,8 +480,8 @@ class RequirementCommand(IndexGroupCommand):
self,
options: Values,
session: PipSession,
target_python: TargetPython | None = None,
ignore_requires_python: bool | None = None,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
) -> PackageFinder:
"""
Create a package finder appropriate to this requirement command.

View file

@ -1,31 +1,15 @@
from __future__ import annotations
import contextlib
import itertools
import logging
import sys
import time
from collections.abc import Generator
from typing import IO, Final
from pip._vendor.rich.console import (
Console,
ConsoleOptions,
RenderableType,
RenderResult,
)
from pip._vendor.rich.live import Live
from pip._vendor.rich.measure import Measurement
from pip._vendor.rich.text import Text
from typing import IO, Generator, Optional
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_console, get_indentation
from pip._internal.utils.logging import get_indentation
logger = logging.getLogger(__name__)
SPINNER_CHARS: Final = r"-\|/"
SPINS_PER_SECOND: Final = 8
class SpinnerInterface:
def spin(self) -> None:
@ -39,10 +23,10 @@ class InteractiveSpinner(SpinnerInterface):
def __init__(
self,
message: str,
file: IO[str] | None = None,
spin_chars: str = SPINNER_CHARS,
file: Optional[IO[str]] = None,
spin_chars: str = "-\\|/",
# Empirically, 8 updates/second looks nice
min_update_interval_seconds: float = 1 / SPINS_PER_SECOND,
min_update_interval_seconds: float = 0.125,
):
self._message = message
if file is None:
@ -152,66 +136,6 @@ def open_spinner(message: str) -> Generator[SpinnerInterface, None, None]:
spinner.finish("done")
class _PipRichSpinner:
"""
Custom rich spinner that matches the style of the legacy spinners.
(*) Updates will be handled in a background thread by a rich live panel
which will call render() automatically at the appropriate time.
"""
def __init__(self, label: str) -> None:
self.label = label
self._spin_cycle = itertools.cycle(SPINNER_CHARS)
self._spinner_text = ""
self._finished = False
self._indent = get_indentation() * " "
def __rich_console__(
self, console: Console, options: ConsoleOptions
) -> RenderResult:
yield self.render()
def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
text = self.render()
return Measurement.get(console, options, text)
def render(self) -> RenderableType:
if not self._finished:
self._spinner_text = next(self._spin_cycle)
return Text.assemble(self._indent, self.label, " ... ", self._spinner_text)
def finish(self, status: str) -> None:
"""Stop spinning and set a final status message."""
self._spinner_text = status
self._finished = True
@contextlib.contextmanager
def open_rich_spinner(label: str, console: Console | None = None) -> Generator[None]:
if not logger.isEnabledFor(logging.INFO):
# Don't show spinner if --quiet is given.
yield
return
console = console or get_console()
spinner = _PipRichSpinner(label)
with Live(spinner, refresh_per_second=SPINS_PER_SECOND, console=console):
try:
yield
except KeyboardInterrupt:
spinner.finish("canceled")
raise
except Exception:
spinner.finish("error")
raise
else:
spinner.finish("done")
HIDE_CURSOR = "\x1b[?25l"
SHOW_CURSOR = "\x1b[?25h"