Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/utils/subprocess.py
4804 views
import logging1import os2import shlex3import subprocess4from typing import (5TYPE_CHECKING,6Any,7Callable,8Iterable,9List,10Mapping,11Optional,12Union,13)1415from pip._vendor.rich.markup import escape1617from pip._internal.cli.spinners import SpinnerInterface, open_spinner18from pip._internal.exceptions import InstallationSubprocessError19from pip._internal.utils.logging import VERBOSE, subprocess_logger20from pip._internal.utils.misc import HiddenText2122if TYPE_CHECKING:23# Literal was introduced in Python 3.8.24#25# TODO: Remove `if TYPE_CHECKING` when dropping support for Python 3.7.26from typing import Literal2728CommandArgs = List[Union[str, HiddenText]]293031def make_command(*args: Union[str, HiddenText, CommandArgs]) -> CommandArgs:32"""33Create a CommandArgs object.34"""35command_args: CommandArgs = []36for arg in args:37# Check for list instead of CommandArgs since CommandArgs is38# only known during type-checking.39if isinstance(arg, list):40command_args.extend(arg)41else:42# Otherwise, arg is str or HiddenText.43command_args.append(arg)4445return command_args464748def format_command_args(args: Union[List[str], CommandArgs]) -> str:49"""50Format command arguments for display.51"""52# For HiddenText arguments, display the redacted form by calling str().53# Also, we don't apply str() to arguments that aren't HiddenText since54# this can trigger a UnicodeDecodeError in Python 2 if the argument55# has type unicode and includes a non-ascii character. (The type56# checker doesn't ensure the annotations are correct in all cases.)57return " ".join(58shlex.quote(str(arg)) if isinstance(arg, HiddenText) else shlex.quote(arg)59for arg in args60)616263def reveal_command_args(args: Union[List[str], CommandArgs]) -> List[str]:64"""65Return the arguments in their raw, unredacted form.66"""67return [arg.secret if isinstance(arg, HiddenText) else arg for arg in args]686970def call_subprocess(71cmd: Union[List[str], CommandArgs],72show_stdout: bool = False,73cwd: Optional[str] = None,74on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",75extra_ok_returncodes: Optional[Iterable[int]] = None,76extra_environ: Optional[Mapping[str, Any]] = None,77unset_environ: Optional[Iterable[str]] = None,78spinner: Optional[SpinnerInterface] = None,79log_failed_cmd: Optional[bool] = True,80stdout_only: Optional[bool] = False,81*,82command_desc: str,83) -> str:84"""85Args:86show_stdout: if true, use INFO to log the subprocess's stderr and87stdout streams. Otherwise, use DEBUG. Defaults to False.88extra_ok_returncodes: an iterable of integer return codes that are89acceptable, in addition to 0. Defaults to None, which means [].90unset_environ: an iterable of environment variable names to unset91prior to calling subprocess.Popen().92log_failed_cmd: if false, failed commands are not logged, only raised.93stdout_only: if true, return only stdout, else return both. When true,94logging of both stdout and stderr occurs when the subprocess has95terminated, else logging occurs as subprocess output is produced.96"""97if extra_ok_returncodes is None:98extra_ok_returncodes = []99if unset_environ is None:100unset_environ = []101# Most places in pip use show_stdout=False. What this means is--102#103# - We connect the child's output (combined stderr and stdout) to a104# single pipe, which we read.105# - We log this output to stderr at DEBUG level as it is received.106# - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't107# requested), then we show a spinner so the user can still see the108# subprocess is in progress.109# - If the subprocess exits with an error, we log the output to stderr110# at ERROR level if it hasn't already been displayed to the console111# (e.g. if --verbose logging wasn't enabled). This way we don't log112# the output to the console twice.113#114# If show_stdout=True, then the above is still done, but with DEBUG115# replaced by INFO.116if show_stdout:117# Then log the subprocess output at INFO level.118log_subprocess: Callable[..., None] = subprocess_logger.info119used_level = logging.INFO120else:121# Then log the subprocess output using VERBOSE. This also ensures122# it will be logged to the log file (aka user_log), if enabled.123log_subprocess = subprocess_logger.verbose124used_level = VERBOSE125126# Whether the subprocess will be visible in the console.127showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level128129# Only use the spinner if we're not showing the subprocess output130# and we have a spinner.131use_spinner = not showing_subprocess and spinner is not None132133log_subprocess("Running command %s", command_desc)134env = os.environ.copy()135if extra_environ:136env.update(extra_environ)137for name in unset_environ:138env.pop(name, None)139try:140proc = subprocess.Popen(141# Convert HiddenText objects to the underlying str.142reveal_command_args(cmd),143stdin=subprocess.PIPE,144stdout=subprocess.PIPE,145stderr=subprocess.STDOUT if not stdout_only else subprocess.PIPE,146cwd=cwd,147env=env,148errors="backslashreplace",149)150except Exception as exc:151if log_failed_cmd:152subprocess_logger.critical(153"Error %s while executing command %s",154exc,155command_desc,156)157raise158all_output = []159if not stdout_only:160assert proc.stdout161assert proc.stdin162proc.stdin.close()163# In this mode, stdout and stderr are in the same pipe.164while True:165line: str = proc.stdout.readline()166if not line:167break168line = line.rstrip()169all_output.append(line + "\n")170171# Show the line immediately.172log_subprocess(line)173# Update the spinner.174if use_spinner:175assert spinner176spinner.spin()177try:178proc.wait()179finally:180if proc.stdout:181proc.stdout.close()182output = "".join(all_output)183else:184# In this mode, stdout and stderr are in different pipes.185# We must use communicate() which is the only safe way to read both.186out, err = proc.communicate()187# log line by line to preserve pip log indenting188for out_line in out.splitlines():189log_subprocess(out_line)190all_output.append(out)191for err_line in err.splitlines():192log_subprocess(err_line)193all_output.append(err)194output = out195196proc_had_error = proc.returncode and proc.returncode not in extra_ok_returncodes197if use_spinner:198assert spinner199if proc_had_error:200spinner.finish("error")201else:202spinner.finish("done")203if proc_had_error:204if on_returncode == "raise":205error = InstallationSubprocessError(206command_description=command_desc,207exit_code=proc.returncode,208output_lines=all_output if not showing_subprocess else None,209)210if log_failed_cmd:211subprocess_logger.error("[present-rich] %s", error)212subprocess_logger.verbose(213"[bold magenta]full command[/]: [blue]%s[/]",214escape(format_command_args(cmd)),215extra={"markup": True},216)217subprocess_logger.verbose(218"[bold magenta]cwd[/]: %s",219escape(cwd or "[inherit]"),220extra={"markup": True},221)222223raise error224elif on_returncode == "warn":225subprocess_logger.warning(226'Command "%s" had error code %s in %s',227command_desc,228proc.returncode,229cwd,230)231elif on_returncode == "ignore":232pass233else:234raise ValueError(f"Invalid value: on_returncode={on_returncode!r}")235return output236237238def runner_with_spinner_message(message: str) -> Callable[..., None]:239"""Provide a subprocess_runner that shows a spinner message.240241Intended for use with for pep517's Pep517HookCaller. Thus, the runner has242an API that matches what's expected by Pep517HookCaller.subprocess_runner.243"""244245def runner(246cmd: List[str],247cwd: Optional[str] = None,248extra_environ: Optional[Mapping[str, Any]] = None,249) -> None:250with open_spinner(message) as spinner:251call_subprocess(252cmd,253command_desc=message,254cwd=cwd,255extra_environ=extra_environ,256spinner=spinner,257)258259return runner260261262