mirror of https://github.com/pypa/pip
Merge pull request #10795 from pradyunsg/better-subprocess-errors
This commit is contained in:
commit
1cda23bd6b
|
@ -0,0 +1 @@
|
|||
Improve presentation of errors from subprocesses.
|
|
@ -189,7 +189,8 @@ class BuildEnvironment:
|
|||
finder: "PackageFinder",
|
||||
requirements: Iterable[str],
|
||||
prefix_as_string: str,
|
||||
message: str,
|
||||
*,
|
||||
kind: str,
|
||||
) -> None:
|
||||
prefix = self._prefixes[prefix_as_string]
|
||||
assert not prefix.setup
|
||||
|
@ -203,7 +204,7 @@ class BuildEnvironment:
|
|||
finder,
|
||||
requirements,
|
||||
prefix,
|
||||
message,
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
@ -212,7 +213,8 @@ class BuildEnvironment:
|
|||
finder: "PackageFinder",
|
||||
requirements: Iterable[str],
|
||||
prefix: _Prefix,
|
||||
message: str,
|
||||
*,
|
||||
kind: str,
|
||||
) -> None:
|
||||
args: List[str] = [
|
||||
sys.executable,
|
||||
|
@ -254,8 +256,13 @@ class BuildEnvironment:
|
|||
args.append("--")
|
||||
args.extend(requirements)
|
||||
extra_environ = {"_PIP_STANDALONE_CERT": where()}
|
||||
with open_spinner(message) as spinner:
|
||||
call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
|
||||
with open_spinner(f"Installing {kind}") as spinner:
|
||||
call_subprocess(
|
||||
args,
|
||||
command_desc=f"pip subprocess to install {kind}",
|
||||
spinner=spinner,
|
||||
extra_environ=extra_environ,
|
||||
)
|
||||
|
||||
|
||||
class NoOpBuildEnvironment(BuildEnvironment):
|
||||
|
@ -283,6 +290,7 @@ class NoOpBuildEnvironment(BuildEnvironment):
|
|||
finder: "PackageFinder",
|
||||
requirements: Iterable[str],
|
||||
prefix_as_string: str,
|
||||
message: str,
|
||||
*,
|
||||
kind: str,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
|
|
@ -166,7 +166,7 @@ class Command(CommandContextMixIn):
|
|||
assert isinstance(status, int)
|
||||
return status
|
||||
except DiagnosticPipError as exc:
|
||||
logger.error("[present-diagnostic]", exc)
|
||||
logger.error("[present-diagnostic] %s", exc)
|
||||
logger.debug("Exception information:", exc_info=True)
|
||||
|
||||
return ERROR
|
||||
|
|
|
@ -10,9 +10,9 @@ 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
|
||||
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
import warnings
|
||||
from functools import partial
|
||||
from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values
|
||||
from textwrap import dedent
|
||||
|
@ -30,6 +30,8 @@ from pip._internal.models.target_python import TargetPython
|
|||
from pip._internal.utils.hashes import STRONG_HASHES
|
||||
from pip._internal.utils.misc import strtobool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def raise_option_error(parser: OptionParser, option: Option, msg: str) -> None:
|
||||
"""
|
||||
|
@ -76,10 +78,9 @@ def check_install_build_global(
|
|||
if any(map(getname, names)):
|
||||
control = options.format_control
|
||||
control.disallow_binaries()
|
||||
warnings.warn(
|
||||
logger.warning(
|
||||
"Disabling all use of wheels due to the use of --build-option "
|
||||
"/ --global-option / --install-option.",
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ class SourceDistribution(AbstractDistribution):
|
|||
|
||||
self.req.build_env = BuildEnvironment()
|
||||
self.req.build_env.install_requirements(
|
||||
finder, pyproject_requires, "overlay", "Installing build dependencies"
|
||||
finder, pyproject_requires, "overlay", kind="build dependencies"
|
||||
)
|
||||
conflicting, missing = self.req.build_env.check_requirements(
|
||||
self.req.requirements_to_check
|
||||
|
@ -106,7 +106,7 @@ class SourceDistribution(AbstractDistribution):
|
|||
if conflicting:
|
||||
self._raise_conflicts("the backend dependencies", conflicting)
|
||||
self.req.build_env.install_requirements(
|
||||
finder, missing, "normal", "Installing backend dependencies"
|
||||
finder, missing, "normal", kind="backend dependencies"
|
||||
)
|
||||
|
||||
def _raise_conflicts(
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
"""Exceptions used throughout package"""
|
||||
"""Exceptions used throughout package.
|
||||
|
||||
This module MUST NOT try to import from anything within `pip._internal` to
|
||||
operate. This is expected to be importable from any/all files within the
|
||||
subpackage and, thus, should not depend on them.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
import re
|
||||
|
@ -347,18 +352,78 @@ class MetadataInconsistent(InstallationError):
|
|||
return template.format(self.ireq, self.field, self.f_val, self.m_val)
|
||||
|
||||
|
||||
class InstallationSubprocessError(InstallationError):
|
||||
"""A subprocess call failed during installation."""
|
||||
class LegacyInstallFailure(DiagnosticPipError):
|
||||
"""Error occurred while executing `setup.py install`"""
|
||||
|
||||
def __init__(self, returncode: int, description: str) -> None:
|
||||
self.returncode = returncode
|
||||
self.description = description
|
||||
reference = "legacy-install-failure"
|
||||
|
||||
def __init__(self, package_details: str) -> None:
|
||||
super().__init__(
|
||||
message="Encountered error while trying to install package.",
|
||||
context=package_details,
|
||||
hint_stmt="See above for output from the failure.",
|
||||
note_stmt="This is an issue with the package mentioned above, not pip.",
|
||||
)
|
||||
|
||||
|
||||
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
|
||||
"""A subprocess call failed."""
|
||||
|
||||
reference = "subprocess-exited-with-error"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
command_description: str,
|
||||
exit_code: int,
|
||||
output_lines: Optional[List[str]],
|
||||
) -> None:
|
||||
if output_lines is None:
|
||||
output_prompt = Text("See above for output.")
|
||||
else:
|
||||
output_prompt = (
|
||||
Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
|
||||
+ Text("".join(output_lines))
|
||||
+ Text.from_markup(R"[red]\[end of output][/]")
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
message=(
|
||||
f"[green]{escape(command_description)}[/] did not run successfully.\n"
|
||||
f"exit code: {exit_code}"
|
||||
),
|
||||
context=output_prompt,
|
||||
hint_stmt=None,
|
||||
note_stmt=(
|
||||
"This error originates from a subprocess, and is likely not a "
|
||||
"problem with pip."
|
||||
),
|
||||
)
|
||||
|
||||
self.command_description = command_description
|
||||
self.exit_code = exit_code
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (
|
||||
"Command errored out with exit status {}: {} "
|
||||
"Check the logs for full command output."
|
||||
).format(self.returncode, self.description)
|
||||
return f"{self.command_description} exited with {self.exit_code}"
|
||||
|
||||
|
||||
class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
|
||||
reference = "metadata-generation-failed"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
package_details: str,
|
||||
) -> None:
|
||||
super(InstallationSubprocessError, self).__init__(
|
||||
message="Encountered error while generating package metadata.",
|
||||
context=escape(package_details),
|
||||
hint_stmt="See above for details.",
|
||||
note_stmt="This is an issue with the package mentioned above, not pip.",
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "metadata generation failed"
|
||||
|
||||
|
||||
class HashErrors(InstallationError):
|
||||
|
|
|
@ -6,11 +6,17 @@ import os
|
|||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.exceptions import (
|
||||
InstallationSubprocessError,
|
||||
MetadataGenerationFailed,
|
||||
)
|
||||
from pip._internal.utils.subprocess import runner_with_spinner_message
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
|
||||
def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) -> str:
|
||||
def generate_metadata(
|
||||
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
|
||||
) -> str:
|
||||
"""Generate metadata using mechanisms described in PEP 517.
|
||||
|
||||
Returns the generated metadata directory.
|
||||
|
@ -25,6 +31,9 @@ def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) ->
|
|||
# consider the possibility that this hook doesn't exist.
|
||||
runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")
|
||||
with backend.subprocess_runner(runner):
|
||||
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
|
||||
try:
|
||||
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
|
||||
except InstallationSubprocessError as error:
|
||||
raise MetadataGenerationFailed(package_details=details) from error
|
||||
|
||||
return os.path.join(metadata_dir, distinfo_dir)
|
||||
|
|
|
@ -6,12 +6,16 @@ import os
|
|||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.exceptions import (
|
||||
InstallationSubprocessError,
|
||||
MetadataGenerationFailed,
|
||||
)
|
||||
from pip._internal.utils.subprocess import runner_with_spinner_message
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
|
||||
|
||||
def generate_editable_metadata(
|
||||
build_env: BuildEnvironment, backend: Pep517HookCaller
|
||||
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
|
||||
) -> str:
|
||||
"""Generate metadata using mechanisms described in PEP 660.
|
||||
|
||||
|
@ -29,6 +33,9 @@ def generate_editable_metadata(
|
|||
"Preparing editable metadata (pyproject.toml)"
|
||||
)
|
||||
with backend.subprocess_runner(runner):
|
||||
distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
|
||||
try:
|
||||
distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
|
||||
except InstallationSubprocessError as error:
|
||||
raise MetadataGenerationFailed(package_details=details) from error
|
||||
|
||||
return os.path.join(metadata_dir, distinfo_dir)
|
||||
|
|
|
@ -6,7 +6,11 @@ import os
|
|||
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.cli.spinners import open_spinner
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.exceptions import (
|
||||
InstallationError,
|
||||
InstallationSubprocessError,
|
||||
MetadataGenerationFailed,
|
||||
)
|
||||
from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args
|
||||
from pip._internal.utils.subprocess import call_subprocess
|
||||
from pip._internal.utils.temp_dir import TempDirectory
|
||||
|
@ -56,12 +60,15 @@ def generate_metadata(
|
|||
|
||||
with build_env:
|
||||
with open_spinner("Preparing metadata (setup.py)") as spinner:
|
||||
call_subprocess(
|
||||
args,
|
||||
cwd=source_dir,
|
||||
command_desc="python setup.py egg_info",
|
||||
spinner=spinner,
|
||||
)
|
||||
try:
|
||||
call_subprocess(
|
||||
args,
|
||||
cwd=source_dir,
|
||||
command_desc="python setup.py egg_info",
|
||||
spinner=spinner,
|
||||
)
|
||||
except InstallationSubprocessError as error:
|
||||
raise MetadataGenerationFailed(package_details=details) from error
|
||||
|
||||
# Return the .egg-info directory.
|
||||
return _find_egg_info(egg_info_dir)
|
||||
|
|
|
@ -4,11 +4,7 @@ from typing import List, Optional
|
|||
|
||||
from pip._internal.cli.spinners import open_spinner
|
||||
from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args
|
||||
from pip._internal.utils.subprocess import (
|
||||
LOG_DIVIDER,
|
||||
call_subprocess,
|
||||
format_command_args,
|
||||
)
|
||||
from pip._internal.utils.subprocess import call_subprocess, format_command_args
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -28,7 +24,7 @@ def format_command_result(
|
|||
else:
|
||||
if not command_output.endswith("\n"):
|
||||
command_output += "\n"
|
||||
text += f"Command output:\n{command_output}{LOG_DIVIDER}"
|
||||
text += f"Command output:\n{command_output}"
|
||||
|
||||
return text
|
||||
|
||||
|
@ -86,6 +82,7 @@ def build_wheel_legacy(
|
|||
try:
|
||||
output = call_subprocess(
|
||||
wheel_args,
|
||||
command_desc="python setup.py bdist_wheel",
|
||||
cwd=source_dir,
|
||||
spinner=spinner,
|
||||
)
|
||||
|
|
|
@ -42,5 +42,6 @@ def install_editable(
|
|||
with build_env:
|
||||
call_subprocess(
|
||||
args,
|
||||
command_desc="python setup.py develop",
|
||||
cwd=unpacked_source_directory,
|
||||
)
|
||||
|
|
|
@ -7,9 +7,8 @@ from distutils.util import change_root
|
|||
from typing import List, Optional, Sequence
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
|
||||
from pip._internal.models.scheme import Scheme
|
||||
from pip._internal.utils.logging import indent_log
|
||||
from pip._internal.utils.misc import ensure_dir
|
||||
from pip._internal.utils.setuptools_build import make_setuptools_install_args
|
||||
from pip._internal.utils.subprocess import runner_with_spinner_message
|
||||
|
@ -18,10 +17,6 @@ from pip._internal.utils.temp_dir import TempDirectory
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LegacyInstallFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def write_installed_files_from_setuptools_record(
|
||||
record_lines: List[str],
|
||||
root: Optional[str],
|
||||
|
@ -98,7 +93,7 @@ def install(
|
|||
runner = runner_with_spinner_message(
|
||||
f"Running setup.py install for {req_name}"
|
||||
)
|
||||
with indent_log(), build_env:
|
||||
with build_env:
|
||||
runner(
|
||||
cmd=install_args,
|
||||
cwd=unpacked_source_directory,
|
||||
|
@ -111,7 +106,7 @@ def install(
|
|||
|
||||
except Exception as e:
|
||||
# Signal to the caller that we didn't install the new package
|
||||
raise LegacyInstallFailure from e
|
||||
raise LegacyInstallFailure(package_details=req_name) from e
|
||||
|
||||
# At this point, we have successfully installed the requirement.
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from pip._vendor.packaging.version import parse as parse_version
|
|||
from pip._vendor.pep517.wrappers import Pep517HookCaller
|
||||
|
||||
from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
|
||||
from pip._internal.exceptions import InstallationError
|
||||
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
|
||||
from pip._internal.locations import get_scheme
|
||||
from pip._internal.metadata import (
|
||||
BaseDistribution,
|
||||
|
@ -35,7 +35,6 @@ from pip._internal.operations.build.metadata_legacy import (
|
|||
from pip._internal.operations.install.editable_legacy import (
|
||||
install_editable as install_editable_legacy,
|
||||
)
|
||||
from pip._internal.operations.install.legacy import LegacyInstallFailure
|
||||
from pip._internal.operations.install.legacy import install as install_legacy
|
||||
from pip._internal.operations.install.wheel import install_wheel
|
||||
from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path
|
||||
|
@ -505,6 +504,7 @@ class InstallRequirement:
|
|||
Under legacy processing, call setup.py egg-info.
|
||||
"""
|
||||
assert self.source_dir
|
||||
details = self.name or f"from {self.link}"
|
||||
|
||||
if self.use_pep517:
|
||||
assert self.pep517_backend is not None
|
||||
|
@ -516,11 +516,13 @@ class InstallRequirement:
|
|||
self.metadata_directory = generate_editable_metadata(
|
||||
build_env=self.build_env,
|
||||
backend=self.pep517_backend,
|
||||
details=details,
|
||||
)
|
||||
else:
|
||||
self.metadata_directory = generate_metadata(
|
||||
build_env=self.build_env,
|
||||
backend=self.pep517_backend,
|
||||
details=details,
|
||||
)
|
||||
else:
|
||||
self.metadata_directory = generate_metadata_legacy(
|
||||
|
@ -528,7 +530,7 @@ class InstallRequirement:
|
|||
setup_py_path=self.setup_py_path,
|
||||
source_dir=self.unpacked_source_directory,
|
||||
isolated=self.isolated,
|
||||
details=self.name or f"from {self.link}",
|
||||
details=details,
|
||||
)
|
||||
|
||||
# Act on the newly generated metadata, based on the name and version.
|
||||
|
@ -806,7 +808,7 @@ class InstallRequirement:
|
|||
)
|
||||
except LegacyInstallFailure as exc:
|
||||
self.install_succeeded = False
|
||||
raise exc.__cause__
|
||||
raise exc
|
||||
except Exception:
|
||||
self.install_succeeded = True
|
||||
raise
|
||||
|
|
|
@ -5,7 +5,11 @@ from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Uni
|
|||
from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
|
||||
from pip._vendor.packaging.version import Version
|
||||
|
||||
from pip._internal.exceptions import HashError, MetadataInconsistent
|
||||
from pip._internal.exceptions import (
|
||||
HashError,
|
||||
InstallationSubprocessError,
|
||||
MetadataInconsistent,
|
||||
)
|
||||
from pip._internal.metadata import BaseDistribution
|
||||
from pip._internal.models.link import Link, links_equivalent
|
||||
from pip._internal.models.wheel import Wheel
|
||||
|
@ -227,6 +231,11 @@ class _InstallRequirementBackedCandidate(Candidate):
|
|||
# offending line to the user.
|
||||
e.req = self._ireq
|
||||
raise
|
||||
except InstallationSubprocessError as exc:
|
||||
# The output has been presented already, so don't duplicate it.
|
||||
exc.context = "See above for output."
|
||||
raise
|
||||
|
||||
self._check_metadata_consistency(dist)
|
||||
return dist
|
||||
|
||||
|
|
|
@ -191,7 +191,12 @@ class Factory:
|
|||
version=version,
|
||||
)
|
||||
except (InstallationSubprocessError, MetadataInconsistent) as e:
|
||||
logger.warning("Discarding %s. %s", link, e)
|
||||
logger.info(
|
||||
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
|
||||
link,
|
||||
e,
|
||||
extra={"markup": True},
|
||||
)
|
||||
self._build_failures[link] = e
|
||||
return None
|
||||
base: BaseCandidate = self._editable_candidate_cache[link]
|
||||
|
@ -206,7 +211,12 @@ class Factory:
|
|||
version=version,
|
||||
)
|
||||
except (InstallationSubprocessError, MetadataInconsistent) as e:
|
||||
logger.warning("Discarding %s. %s", link, e)
|
||||
logger.info(
|
||||
"Discarding [blue underline]%s[/]: [yellow]%s[reset]",
|
||||
link,
|
||||
e,
|
||||
extra={"markup": True},
|
||||
)
|
||||
self._build_failures[link] = e
|
||||
return None
|
||||
base = self._link_candidate_cache[link]
|
||||
|
|
|
@ -152,7 +152,7 @@ class RichPipStreamHandler(RichHandler):
|
|||
style: Optional[Style] = None
|
||||
|
||||
# If we are given a diagnostic error to present, present it with indentation.
|
||||
if record.msg == "[present-diagnostic]" and len(record.args) == 1:
|
||||
if record.msg == "[present-diagnostic] %s" and len(record.args) == 1:
|
||||
diagnostic_error: DiagnosticPipError = record.args[0] # type: ignore[index]
|
||||
assert isinstance(diagnostic_error, DiagnosticPipError)
|
||||
|
||||
|
|
|
@ -1,21 +1,49 @@
|
|||
import sys
|
||||
import textwrap
|
||||
from typing import List, Optional, Sequence
|
||||
|
||||
# Shim to wrap setup.py invocation with setuptools
|
||||
#
|
||||
# We set sys.argv[0] to the path to the underlying setup.py file so
|
||||
# setuptools / distutils don't take the path to the setup.py to be "-c" when
|
||||
# invoking via the shim. This avoids e.g. the following manifest_maker
|
||||
# warning: "warning: manifest_maker: standard file '-c' not found".
|
||||
_SETUPTOOLS_SHIM = (
|
||||
"import io, os, sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};"
|
||||
"f = getattr(tokenize, 'open', open)(__file__) "
|
||||
"if os.path.exists(__file__) "
|
||||
"else io.StringIO('from setuptools import setup; setup()');"
|
||||
"code = f.read().replace('\\r\\n', '\\n');"
|
||||
"f.close();"
|
||||
"exec(compile(code, __file__, 'exec'))"
|
||||
)
|
||||
# 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(
|
||||
|
|
|
@ -13,6 +13,8 @@ from typing import (
|
|||
Union,
|
||||
)
|
||||
|
||||
from pip._vendor.rich.markup import escape
|
||||
|
||||
from pip._internal.cli.spinners import SpinnerInterface, open_spinner
|
||||
from pip._internal.exceptions import InstallationSubprocessError
|
||||
from pip._internal.utils.logging import VERBOSE, subprocess_logger
|
||||
|
@ -27,9 +29,6 @@ if TYPE_CHECKING:
|
|||
CommandArgs = List[Union[str, HiddenText]]
|
||||
|
||||
|
||||
LOG_DIVIDER = "----------------------------------------"
|
||||
|
||||
|
||||
def make_command(*args: Union[str, HiddenText, CommandArgs]) -> CommandArgs:
|
||||
"""
|
||||
Create a CommandArgs object.
|
||||
|
@ -69,53 +68,19 @@ def reveal_command_args(args: Union[List[str], CommandArgs]) -> List[str]:
|
|||
return [arg.secret if isinstance(arg, HiddenText) else arg for arg in args]
|
||||
|
||||
|
||||
def make_subprocess_output_error(
|
||||
cmd_args: Union[List[str], CommandArgs],
|
||||
cwd: Optional[str],
|
||||
lines: List[str],
|
||||
exit_status: int,
|
||||
) -> str:
|
||||
"""
|
||||
Create and return the error message to use to log a subprocess error
|
||||
with command output.
|
||||
|
||||
:param lines: A list of lines, each ending with a newline.
|
||||
"""
|
||||
command = format_command_args(cmd_args)
|
||||
|
||||
# We know the joined output value ends in a newline.
|
||||
output = "".join(lines)
|
||||
msg = (
|
||||
# Use a unicode string to avoid "UnicodeEncodeError: 'ascii'
|
||||
# codec can't encode character ..." in Python 2 when a format
|
||||
# argument (e.g. `output`) has a non-ascii character.
|
||||
"Command errored out with exit status {exit_status}:\n"
|
||||
" command: {command_display}\n"
|
||||
" cwd: {cwd_display}\n"
|
||||
"Complete output ({line_count} lines):\n{output}{divider}"
|
||||
).format(
|
||||
exit_status=exit_status,
|
||||
command_display=command,
|
||||
cwd_display=cwd,
|
||||
line_count=len(lines),
|
||||
output=output,
|
||||
divider=LOG_DIVIDER,
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
def call_subprocess(
|
||||
cmd: Union[List[str], CommandArgs],
|
||||
show_stdout: bool = False,
|
||||
cwd: Optional[str] = None,
|
||||
on_returncode: 'Literal["raise", "warn", "ignore"]' = "raise",
|
||||
extra_ok_returncodes: Optional[Iterable[int]] = None,
|
||||
command_desc: Optional[str] = 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:
|
||||
"""
|
||||
Args:
|
||||
|
@ -166,9 +131,6 @@ def call_subprocess(
|
|||
# and we have a spinner.
|
||||
use_spinner = not showing_subprocess and spinner is not None
|
||||
|
||||
if command_desc is None:
|
||||
command_desc = format_command_args(cmd)
|
||||
|
||||
log_subprocess("Running command %s", command_desc)
|
||||
env = os.environ.copy()
|
||||
if extra_environ:
|
||||
|
@ -241,17 +203,25 @@ def call_subprocess(
|
|||
spinner.finish("done")
|
||||
if proc_had_error:
|
||||
if on_returncode == "raise":
|
||||
if not showing_subprocess and log_failed_cmd:
|
||||
# Then the subprocess streams haven't been logged to the
|
||||
# console yet.
|
||||
msg = make_subprocess_output_error(
|
||||
cmd_args=cmd,
|
||||
cwd=cwd,
|
||||
lines=all_output,
|
||||
exit_status=proc.returncode,
|
||||
error = InstallationSubprocessError(
|
||||
command_description=command_desc,
|
||||
exit_code=proc.returncode,
|
||||
output_lines=all_output if not showing_subprocess else None,
|
||||
)
|
||||
if log_failed_cmd:
|
||||
subprocess_logger.error("[present-diagnostic] %s", error)
|
||||
subprocess_logger.verbose(
|
||||
"[bold magenta]full command[/]: [blue]%s[/]",
|
||||
escape(format_command_args(cmd)),
|
||||
extra={"markup": True},
|
||||
)
|
||||
subprocess_logger.error(msg)
|
||||
raise InstallationSubprocessError(proc.returncode, command_desc)
|
||||
subprocess_logger.verbose(
|
||||
"[bold magenta]cwd[/]: %s",
|
||||
escape(cwd or "[inherit]"),
|
||||
extra={"markup": True},
|
||||
)
|
||||
|
||||
raise error
|
||||
elif on_returncode == "warn":
|
||||
subprocess_logger.warning(
|
||||
'Command "%s" had error code %s in %s',
|
||||
|
@ -281,6 +251,7 @@ def runner_with_spinner_message(message: str) -> Callable[..., None]:
|
|||
with open_spinner(message) as spinner:
|
||||
call_subprocess(
|
||||
cmd,
|
||||
command_desc=message,
|
||||
cwd=cwd,
|
||||
extra_environ=extra_environ,
|
||||
spinner=spinner,
|
||||
|
|
|
@ -91,7 +91,12 @@ class Git(VersionControl):
|
|||
return not is_tag_or_branch
|
||||
|
||||
def get_git_version(self) -> Tuple[int, ...]:
|
||||
version = self.run_command(["version"], show_stdout=False, stdout_only=True)
|
||||
version = self.run_command(
|
||||
["version"],
|
||||
command_desc="git version",
|
||||
show_stdout=False,
|
||||
stdout_only=True,
|
||||
)
|
||||
match = GIT_VERSION_REGEX.match(version)
|
||||
if not match:
|
||||
logger.warning("Can't parse git version: %s", version)
|
||||
|
|
|
@ -31,7 +31,12 @@ from pip._internal.utils.misc import (
|
|||
is_installable_dir,
|
||||
rmtree,
|
||||
)
|
||||
from pip._internal.utils.subprocess import CommandArgs, call_subprocess, make_command
|
||||
from pip._internal.utils.subprocess import (
|
||||
CommandArgs,
|
||||
call_subprocess,
|
||||
format_command_args,
|
||||
make_command,
|
||||
)
|
||||
from pip._internal.utils.urls import get_url_scheme
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -639,6 +644,8 @@ class VersionControl:
|
|||
command name, and checks that the VCS is available
|
||||
"""
|
||||
cmd = make_command(cls.name, *cmd)
|
||||
if command_desc is None:
|
||||
command_desc = format_command_args(cmd)
|
||||
try:
|
||||
return call_subprocess(
|
||||
cmd,
|
||||
|
|
|
@ -310,7 +310,9 @@ def _clean_one_legacy(req: InstallRequirement, global_options: List[str]) -> boo
|
|||
|
||||
logger.info("Running setup.py clean for %s", req.name)
|
||||
try:
|
||||
call_subprocess(clean_args, cwd=req.source_dir)
|
||||
call_subprocess(
|
||||
clean_args, command_desc="python setup.py clean", cwd=req.source_dir
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
logger.error("Failed cleaning build dir for %s", req.name)
|
||||
|
|
|
@ -6,8 +6,10 @@ import sys
|
|||
from setuptools import setup
|
||||
|
||||
print(f"HELLO FROM CHATTYMODULE {sys.argv[1]}")
|
||||
print(os.environ)
|
||||
print(sys.argv)
|
||||
print(sys.executable)
|
||||
print(sys.version)
|
||||
|
||||
if "--fail" in sys.argv:
|
||||
print("I DIE, I DIE")
|
||||
sys.exit(1)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from setuptools import setup
|
||||
|
||||
# This is to get an error that originates from setuptools, which generates a
|
||||
# decently sized output.
|
||||
setup(
|
||||
cmdclass={
|
||||
"egg_info": "<make-me-fail>",
|
||||
"install": "<make-me-fail>",
|
||||
"bdist_wheel": "<make-me-fail>",
|
||||
}
|
||||
)
|
|
@ -81,7 +81,7 @@ def test_build_env_allow_empty_requirements_install() -> None:
|
|||
build_env = BuildEnvironment()
|
||||
for prefix in ("normal", "overlay"):
|
||||
build_env.install_requirements(
|
||||
finder, [], prefix, "Installing build dependencies"
|
||||
finder, [], prefix, kind="Installing build dependencies"
|
||||
)
|
||||
|
||||
|
||||
|
@ -92,15 +92,15 @@ def test_build_env_allow_only_one_install(script: PipTestEnvironment) -> None:
|
|||
build_env = BuildEnvironment()
|
||||
for prefix in ("normal", "overlay"):
|
||||
build_env.install_requirements(
|
||||
finder, ["foo"], prefix, f"installing foo in {prefix}"
|
||||
finder, ["foo"], prefix, kind=f"installing foo in {prefix}"
|
||||
)
|
||||
with pytest.raises(AssertionError):
|
||||
build_env.install_requirements(
|
||||
finder, ["bar"], prefix, f"installing bar in {prefix}"
|
||||
finder, ["bar"], prefix, kind=f"installing bar in {prefix}"
|
||||
)
|
||||
with pytest.raises(AssertionError):
|
||||
build_env.install_requirements(
|
||||
finder, [], prefix, f"installing in {prefix}"
|
||||
finder, [], prefix, kind=f"installing in {prefix}"
|
||||
)
|
||||
|
||||
|
||||
|
@ -131,7 +131,7 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
|
|||
script,
|
||||
"""
|
||||
build_env.install_requirements(finder, ['foo', 'bar==3.0'], 'normal',
|
||||
'installing foo in normal')
|
||||
kind='installing foo in normal')
|
||||
|
||||
r = build_env.check_requirements(['foo', 'bar', 'other'])
|
||||
assert r == (set(), {'other'}), repr(r)
|
||||
|
@ -148,9 +148,9 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
|
|||
script,
|
||||
"""
|
||||
build_env.install_requirements(finder, ['foo', 'bar==3.0'], 'normal',
|
||||
'installing foo in normal')
|
||||
kind='installing foo in normal')
|
||||
build_env.install_requirements(finder, ['bar==1.0'], 'overlay',
|
||||
'installing foo in overlay')
|
||||
kind='installing foo in overlay')
|
||||
|
||||
r = build_env.check_requirements(['foo', 'bar', 'other'])
|
||||
assert r == (set(), {'other'}), repr(r)
|
||||
|
@ -172,9 +172,9 @@ def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> No
|
|||
script,
|
||||
"""
|
||||
build_env.install_requirements(finder, ['pkg==2.0'], 'overlay',
|
||||
'installing pkg==2.0 in overlay')
|
||||
kind='installing pkg==2.0 in overlay')
|
||||
build_env.install_requirements(finder, ['pkg==4.3'], 'normal',
|
||||
'installing pkg==4.3 in normal')
|
||||
kind='installing pkg==4.3 in normal')
|
||||
""",
|
||||
"""
|
||||
print(__import__('pkg').__version__)
|
||||
|
|
|
@ -1739,7 +1739,7 @@ def test_install_editable_with_wrong_egg_name(
|
|||
"fragments."
|
||||
) in result.stderr
|
||||
if resolver_variant == "2020-resolver":
|
||||
assert "has inconsistent" in result.stderr, str(result)
|
||||
assert "has inconsistent" in result.stdout, str(result)
|
||||
else:
|
||||
assert "Successfully installed pkga" in str(result), str(result)
|
||||
|
||||
|
|
|
@ -351,6 +351,7 @@ def test_install_option_in_requirements_file_overrides_cli(
|
|||
"-r",
|
||||
str(reqs_file),
|
||||
"--install-option=-O1",
|
||||
allow_stderr_warning=True,
|
||||
)
|
||||
simple_args = simple_sdist.args()
|
||||
assert "install" in simple_args
|
||||
|
@ -790,6 +791,7 @@ def test_install_options_local_to_package(
|
|||
str(simple1_sdist.sdist_path.parent),
|
||||
"-r",
|
||||
reqs_file,
|
||||
allow_stderr_warning=True,
|
||||
)
|
||||
|
||||
simple1_args = simple1_sdist.args()
|
||||
|
|
|
@ -347,6 +347,7 @@ def test_git_with_tag_name_and_update(script: PipTestEnvironment, tmpdir: Path)
|
|||
"--global-option=--version",
|
||||
"-e",
|
||||
new_local_url,
|
||||
allow_stderr_warning=True,
|
||||
)
|
||||
assert "0.1.2" in result.stdout
|
||||
|
||||
|
@ -380,7 +381,12 @@ def test_git_with_non_editable_unpacking(
|
|||
rev="0.1.2",
|
||||
egg="pip-test-package",
|
||||
)
|
||||
result = script.pip("install", "--global-option=--version", local_url)
|
||||
result = script.pip(
|
||||
"install",
|
||||
"--global-option=--version",
|
||||
local_url,
|
||||
allow_stderr_warning=True,
|
||||
)
|
||||
assert "0.1.2" in result.stdout
|
||||
|
||||
|
||||
|
|
|
@ -1362,7 +1362,7 @@ def test_new_resolver_skip_inconsistent_metadata(script: PipTestEnvironment) ->
|
|||
|
||||
assert (
|
||||
" inconsistent version: filename has '3', but metadata has '2'"
|
||||
) in result.stderr, str(result)
|
||||
) in result.stdout, str(result)
|
||||
script.assert_installed(a="1")
|
||||
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ def test_backend(tmpdir: Path, data: TestData) -> None:
|
|||
req.load_pyproject_toml()
|
||||
env = BuildEnvironment()
|
||||
finder = make_test_finder(find_links=[data.backends])
|
||||
env.install_requirements(finder, ["dummy_backend"], "normal", "Installing")
|
||||
env.install_requirements(finder, ["dummy_backend"], "normal", kind="Installing")
|
||||
conflicting, missing = env.check_requirements(["dummy_backend"])
|
||||
assert not conflicting and not missing
|
||||
assert hasattr(req.pep517_backend, "build_wheel")
|
||||
|
@ -83,7 +83,7 @@ def test_backend_path_and_dep(tmpdir: Path, data: TestData) -> None:
|
|||
req.load_pyproject_toml()
|
||||
env = BuildEnvironment()
|
||||
finder = make_test_finder(find_links=[data.backends])
|
||||
env.install_requirements(finder, ["dummy_backend"], "normal", "Installing")
|
||||
env.install_requirements(finder, ["dummy_backend"], "normal", kind="Installing")
|
||||
|
||||
assert hasattr(req.pep517_backend, "build_wheel")
|
||||
with env:
|
||||
|
@ -300,6 +300,7 @@ def test_pep517_and_build_options(
|
|||
"-f",
|
||||
common_wheels,
|
||||
project_dir,
|
||||
allow_stderr_warning=True,
|
||||
)
|
||||
assert "Ignoring --build-option when building" in result.stderr
|
||||
assert "using PEP 517" in result.stderr
|
||||
|
@ -320,6 +321,7 @@ def test_pep517_and_global_options(
|
|||
"-f",
|
||||
common_wheels,
|
||||
project_dir,
|
||||
allow_stderr_warning=True,
|
||||
)
|
||||
assert "Ignoring --global-option when building" in result.stderr
|
||||
assert "using PEP 517" in result.stderr
|
||||
|
|
|
@ -943,11 +943,14 @@ def test_make_setuptools_shim_args() -> None:
|
|||
)
|
||||
|
||||
assert args[1:3] == ["-u", "-c"]
|
||||
# Spot-check key aspects of the command string.
|
||||
assert "sys.argv[0] = '/dir/path/setup.py'" in args[3]
|
||||
assert "__file__='/dir/path/setup.py'" in args[3]
|
||||
assert args[4:] == ["--some", "--option", "--no-user-cfg"]
|
||||
|
||||
shim = args[3]
|
||||
# Spot-check key aspects of the command string.
|
||||
assert "import setuptools" in shim
|
||||
assert "'/dir/path/setup.py'" in args[3]
|
||||
assert "sys.argv[0] = __file__" in args[3]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("global_options", [None, [], ["--some", "--option"]])
|
||||
def test_make_setuptools_shim_args__global_options(
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import locale
|
||||
import sys
|
||||
from logging import DEBUG, ERROR, INFO, WARNING
|
||||
from textwrap import dedent
|
||||
from typing import List, Optional, Tuple, Type
|
||||
|
||||
import pytest
|
||||
|
@ -15,7 +14,6 @@ from pip._internal.utils.subprocess import (
|
|||
call_subprocess,
|
||||
format_command_args,
|
||||
make_command,
|
||||
make_subprocess_output_error,
|
||||
subprocess_logger,
|
||||
)
|
||||
|
||||
|
@ -40,104 +38,6 @@ def test_format_command_args(args: CommandArgs, expected: str) -> None:
|
|||
assert actual == expected
|
||||
|
||||
|
||||
def test_make_subprocess_output_error() -> None:
|
||||
cmd_args = ["test", "has space"]
|
||||
cwd = "/path/to/cwd"
|
||||
lines = ["line1\n", "line2\n", "line3\n"]
|
||||
actual = make_subprocess_output_error(
|
||||
cmd_args=cmd_args,
|
||||
cwd=cwd,
|
||||
lines=lines,
|
||||
exit_status=3,
|
||||
)
|
||||
expected = dedent(
|
||||
"""\
|
||||
Command errored out with exit status 3:
|
||||
command: test 'has space'
|
||||
cwd: /path/to/cwd
|
||||
Complete output (3 lines):
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
----------------------------------------"""
|
||||
)
|
||||
assert actual == expected, f"actual: {actual}"
|
||||
|
||||
|
||||
def test_make_subprocess_output_error__non_ascii_command_arg(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""
|
||||
Test a command argument with a non-ascii character.
|
||||
"""
|
||||
cmd_args = ["foo", "déf"]
|
||||
|
||||
# We need to monkeypatch so the encoding will be correct on Windows.
|
||||
monkeypatch.setattr(locale, "getpreferredencoding", lambda: "utf-8")
|
||||
actual = make_subprocess_output_error(
|
||||
cmd_args=cmd_args,
|
||||
cwd="/path/to/cwd",
|
||||
lines=[],
|
||||
exit_status=1,
|
||||
)
|
||||
expected = dedent(
|
||||
"""\
|
||||
Command errored out with exit status 1:
|
||||
command: foo 'déf'
|
||||
cwd: /path/to/cwd
|
||||
Complete output (0 lines):
|
||||
----------------------------------------"""
|
||||
)
|
||||
assert actual == expected, f"actual: {actual}"
|
||||
|
||||
|
||||
def test_make_subprocess_output_error__non_ascii_cwd_python_3() -> None:
|
||||
"""
|
||||
Test a str (text) cwd with a non-ascii character in Python 3.
|
||||
"""
|
||||
cmd_args = ["test"]
|
||||
cwd = "/path/to/cwd/déf"
|
||||
actual = make_subprocess_output_error(
|
||||
cmd_args=cmd_args,
|
||||
cwd=cwd,
|
||||
lines=[],
|
||||
exit_status=1,
|
||||
)
|
||||
expected = dedent(
|
||||
"""\
|
||||
Command errored out with exit status 1:
|
||||
command: test
|
||||
cwd: /path/to/cwd/déf
|
||||
Complete output (0 lines):
|
||||
----------------------------------------"""
|
||||
)
|
||||
assert actual == expected, f"actual: {actual}"
|
||||
|
||||
|
||||
# This test is mainly important for checking unicode in Python 2.
|
||||
def test_make_subprocess_output_error__non_ascii_line() -> None:
|
||||
"""
|
||||
Test a line with a non-ascii character.
|
||||
"""
|
||||
lines = ["curly-quote: \u2018\n"]
|
||||
actual = make_subprocess_output_error(
|
||||
cmd_args=["test"],
|
||||
cwd="/path/to/cwd",
|
||||
lines=lines,
|
||||
exit_status=1,
|
||||
)
|
||||
expected = dedent(
|
||||
"""\
|
||||
Command errored out with exit status 1:
|
||||
command: test
|
||||
cwd: /path/to/cwd
|
||||
Complete output (1 lines):
|
||||
curly-quote: \u2018
|
||||
----------------------------------------"""
|
||||
)
|
||||
assert actual == expected, f"actual: {actual}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("stdout_only", "expected"),
|
||||
[
|
||||
|
@ -163,6 +63,7 @@ def test_call_subprocess_stdout_only(
|
|||
"-c",
|
||||
"import sys; sys.stdout.write('out\\n'); sys.stderr.write('err\\n')",
|
||||
],
|
||||
command_desc="test stdout_only",
|
||||
stdout_only=stdout_only,
|
||||
)
|
||||
assert out in expected
|
||||
|
@ -271,12 +172,16 @@ class TestCallSubprocess:
|
|||
"""
|
||||
log_level = DEBUG
|
||||
args, spinner = self.prepare_call(caplog, log_level)
|
||||
result = call_subprocess(args, spinner=spinner)
|
||||
result = call_subprocess(
|
||||
args,
|
||||
command_desc="test debug logging",
|
||||
spinner=spinner,
|
||||
)
|
||||
|
||||
expected = (
|
||||
["Hello", "world"],
|
||||
[
|
||||
("pip.subprocessor", VERBOSE, "Running command "),
|
||||
("pip.subprocessor", VERBOSE, "Running "),
|
||||
("pip.subprocessor", VERBOSE, "Hello"),
|
||||
("pip.subprocessor", VERBOSE, "world"),
|
||||
],
|
||||
|
@ -301,7 +206,11 @@ class TestCallSubprocess:
|
|||
"""
|
||||
log_level = INFO
|
||||
args, spinner = self.prepare_call(caplog, log_level)
|
||||
result = call_subprocess(args, spinner=spinner)
|
||||
result = call_subprocess(
|
||||
args,
|
||||
command_desc="test info logging",
|
||||
spinner=spinner,
|
||||
)
|
||||
|
||||
expected: Tuple[List[str], List[Tuple[str, int, str]]] = (
|
||||
["Hello", "world"],
|
||||
|
@ -331,16 +240,29 @@ class TestCallSubprocess:
|
|||
args, spinner = self.prepare_call(caplog, log_level, command=command)
|
||||
|
||||
with pytest.raises(InstallationSubprocessError) as exc:
|
||||
call_subprocess(args, spinner=spinner)
|
||||
call_subprocess(
|
||||
args,
|
||||
command_desc="test info logging with subprocess error",
|
||||
spinner=spinner,
|
||||
)
|
||||
result = None
|
||||
exc_message = str(exc.value)
|
||||
assert exc_message.startswith("Command errored out with exit status 1: ")
|
||||
assert exc_message.endswith("Check the logs for full command output.")
|
||||
exception = exc.value
|
||||
assert exception.reference == "subprocess-exited-with-error"
|
||||
assert "exit code: 1" in exception.message
|
||||
assert exception.note_stmt
|
||||
assert "not a problem with pip" in exception.note_stmt
|
||||
# Check that the process outout is captured, and would be shown.
|
||||
assert exception.context
|
||||
assert "Hello\n" in exception.context
|
||||
assert "fail\n" in exception.context
|
||||
assert "world\n" in exception.context
|
||||
|
||||
expected = (
|
||||
None,
|
||||
[
|
||||
("pip.subprocessor", ERROR, "Complete output (3 lines):\n"),
|
||||
# pytest's caplog overrides th formatter, which means that we
|
||||
# won't see the message formatted through our formatters.
|
||||
("pip.subprocessor", ERROR, "[present-diagnostic]"),
|
||||
],
|
||||
)
|
||||
# The spinner should spin three times in this case since the
|
||||
|
@ -355,33 +277,6 @@ class TestCallSubprocess:
|
|||
expected_spinner=(3, "error"),
|
||||
)
|
||||
|
||||
# Do some further checking on the captured log records to confirm
|
||||
# that the subprocess output was logged.
|
||||
last_record = caplog.record_tuples[-1]
|
||||
last_message = last_record[2]
|
||||
lines = last_message.splitlines()
|
||||
|
||||
# We have to sort before comparing the lines because we can't
|
||||
# guarantee the order in which stdout and stderr will appear.
|
||||
# For example, we observed the stderr lines coming before stdout
|
||||
# in CI for PyPy 2.7 even though stdout happens first chronologically.
|
||||
actual = sorted(lines)
|
||||
# Test the "command" line separately because we can't test an
|
||||
# exact match.
|
||||
command_line = actual.pop(1)
|
||||
assert actual == [
|
||||
" cwd: None",
|
||||
"----------------------------------------",
|
||||
"Command errored out with exit status 1:",
|
||||
"Complete output (3 lines):",
|
||||
"Hello",
|
||||
"fail",
|
||||
"world",
|
||||
], f"lines: {actual}" # Show the full output on failure.
|
||||
|
||||
assert command_line.startswith(" command: ")
|
||||
assert command_line.endswith('print("world"); exit("fail")\'')
|
||||
|
||||
def test_info_logging_with_show_stdout_true(
|
||||
self, capfd: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
|
@ -390,12 +285,17 @@ class TestCallSubprocess:
|
|||
"""
|
||||
log_level = INFO
|
||||
args, spinner = self.prepare_call(caplog, log_level)
|
||||
result = call_subprocess(args, spinner=spinner, show_stdout=True)
|
||||
result = call_subprocess(
|
||||
args,
|
||||
command_desc="test info logging with show_stdout",
|
||||
spinner=spinner,
|
||||
show_stdout=True,
|
||||
)
|
||||
|
||||
expected = (
|
||||
["Hello", "world"],
|
||||
[
|
||||
("pip.subprocessor", INFO, "Running command "),
|
||||
("pip.subprocessor", INFO, "Running "),
|
||||
("pip.subprocessor", INFO, "Hello"),
|
||||
("pip.subprocessor", INFO, "world"),
|
||||
],
|
||||
|
@ -456,6 +356,7 @@ class TestCallSubprocess:
|
|||
try:
|
||||
call_subprocess(
|
||||
args,
|
||||
command_desc="spinner go spinny",
|
||||
show_stdout=show_stdout,
|
||||
extra_ok_returncodes=extra_ok_returncodes,
|
||||
spinner=spinner,
|
||||
|
@ -474,6 +375,7 @@ class TestCallSubprocess:
|
|||
call_subprocess(
|
||||
[sys.executable, "-c", "input()"],
|
||||
show_stdout=True,
|
||||
command_desc="stdin reader",
|
||||
)
|
||||
|
||||
|
||||
|
@ -487,9 +389,10 @@ def test_unicode_decode_error(caplog: pytest.LogCaptureFixture) -> None:
|
|||
"-c",
|
||||
"import sys; sys.stdout.buffer.write(b'\\xff')",
|
||||
],
|
||||
command_desc="invalid decode output",
|
||||
show_stdout=True,
|
||||
)
|
||||
|
||||
assert len(caplog.records) == 2
|
||||
# First log record is "Running command ..."
|
||||
# First log record is "Running ..."
|
||||
assert caplog.record_tuples[1] == ("pip.subprocessor", INFO, "\\xff")
|
||||
|
|
|
@ -222,7 +222,6 @@ def test_format_command_result__DEBUG(
|
|||
"Command output:",
|
||||
"output line 1",
|
||||
"output line 2",
|
||||
"----------------------------------------",
|
||||
]
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue