Skip to content

Commit 1cda23b

Browse files
authored
Merge pull request #10795 from pradyunsg/better-subprocess-errors
2 parents dec279e + 723b2df commit 1cda23b

32 files changed

+334
-281
lines changed

news/10705.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve presentation of errors from subprocesses.

src/pip/_internal/build_env.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ def install_requirements(
189189
finder: "PackageFinder",
190190
requirements: Iterable[str],
191191
prefix_as_string: str,
192-
message: str,
192+
*,
193+
kind: str,
193194
) -> None:
194195
prefix = self._prefixes[prefix_as_string]
195196
assert not prefix.setup
@@ -203,7 +204,7 @@ def install_requirements(
203204
finder,
204205
requirements,
205206
prefix,
206-
message,
207+
kind=kind,
207208
)
208209

209210
@staticmethod
@@ -212,7 +213,8 @@ def _install_requirements(
212213
finder: "PackageFinder",
213214
requirements: Iterable[str],
214215
prefix: _Prefix,
215-
message: str,
216+
*,
217+
kind: str,
216218
) -> None:
217219
args: List[str] = [
218220
sys.executable,
@@ -254,8 +256,13 @@ def _install_requirements(
254256
args.append("--")
255257
args.extend(requirements)
256258
extra_environ = {"_PIP_STANDALONE_CERT": where()}
257-
with open_spinner(message) as spinner:
258-
call_subprocess(args, spinner=spinner, extra_environ=extra_environ)
259+
with open_spinner(f"Installing {kind}") as spinner:
260+
call_subprocess(
261+
args,
262+
command_desc=f"pip subprocess to install {kind}",
263+
spinner=spinner,
264+
extra_environ=extra_environ,
265+
)
259266

260267

261268
class NoOpBuildEnvironment(BuildEnvironment):
@@ -283,6 +290,7 @@ def install_requirements(
283290
finder: "PackageFinder",
284291
requirements: Iterable[str],
285292
prefix_as_string: str,
286-
message: str,
293+
*,
294+
kind: str,
287295
) -> None:
288296
raise NotImplementedError()

src/pip/_internal/cli/base_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def exc_logging_wrapper(*args: Any) -> int:
166166
assert isinstance(status, int)
167167
return status
168168
except DiagnosticPipError as exc:
169-
logger.error("[present-diagnostic]", exc)
169+
logger.error("[present-diagnostic] %s", exc)
170170
logger.debug("Exception information:", exc_info=True)
171171

172172
return ERROR

src/pip/_internal/cli/cmdoptions.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
# The following comment should be removed at some point in the future.
1111
# mypy: strict-optional=False
1212

13+
import logging
1314
import os
1415
import textwrap
15-
import warnings
1616
from functools import partial
1717
from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values
1818
from textwrap import dedent
@@ -30,6 +30,8 @@
3030
from pip._internal.utils.hashes import STRONG_HASHES
3131
from pip._internal.utils.misc import strtobool
3232

33+
logger = logging.getLogger(__name__)
34+
3335

3436
def raise_option_error(parser: OptionParser, option: Option, msg: str) -> None:
3537
"""
@@ -76,10 +78,9 @@ def getname(n: str) -> Optional[Any]:
7678
if any(map(getname, names)):
7779
control = options.format_control
7880
control.disallow_binaries()
79-
warnings.warn(
81+
logger.warning(
8082
"Disabling all use of wheels due to the use of --build-option "
8183
"/ --global-option / --install-option.",
82-
stacklevel=2,
8384
)
8485

8586

src/pip/_internal/distributions/sdist.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def _prepare_build_backend(self, finder: PackageFinder) -> None:
5454

5555
self.req.build_env = BuildEnvironment()
5656
self.req.build_env.install_requirements(
57-
finder, pyproject_requires, "overlay", "Installing build dependencies"
57+
finder, pyproject_requires, "overlay", kind="build dependencies"
5858
)
5959
conflicting, missing = self.req.build_env.check_requirements(
6060
self.req.requirements_to_check
@@ -106,7 +106,7 @@ def _install_build_reqs(self, finder: PackageFinder) -> None:
106106
if conflicting:
107107
self._raise_conflicts("the backend dependencies", conflicting)
108108
self.req.build_env.install_requirements(
109-
finder, missing, "normal", "Installing backend dependencies"
109+
finder, missing, "normal", kind="backend dependencies"
110110
)
111111

112112
def _raise_conflicts(

src/pip/_internal/exceptions.py

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
"""Exceptions used throughout package"""
1+
"""Exceptions used throughout package.
2+
3+
This module MUST NOT try to import from anything within `pip._internal` to
4+
operate. This is expected to be importable from any/all files within the
5+
subpackage and, thus, should not depend on them.
6+
"""
27

38
import configparser
49
import re
@@ -347,18 +352,78 @@ def __str__(self) -> str:
347352
return template.format(self.ireq, self.field, self.f_val, self.m_val)
348353

349354

350-
class InstallationSubprocessError(InstallationError):
351-
"""A subprocess call failed during installation."""
355+
class LegacyInstallFailure(DiagnosticPipError):
356+
"""Error occurred while executing `setup.py install`"""
357+
358+
reference = "legacy-install-failure"
352359

353-
def __init__(self, returncode: int, description: str) -> None:
354-
self.returncode = returncode
355-
self.description = description
360+
def __init__(self, package_details: str) -> None:
361+
super().__init__(
362+
message="Encountered error while trying to install package.",
363+
context=package_details,
364+
hint_stmt="See above for output from the failure.",
365+
note_stmt="This is an issue with the package mentioned above, not pip.",
366+
)
367+
368+
369+
class InstallationSubprocessError(DiagnosticPipError, InstallationError):
370+
"""A subprocess call failed."""
371+
372+
reference = "subprocess-exited-with-error"
373+
374+
def __init__(
375+
self,
376+
*,
377+
command_description: str,
378+
exit_code: int,
379+
output_lines: Optional[List[str]],
380+
) -> None:
381+
if output_lines is None:
382+
output_prompt = Text("See above for output.")
383+
else:
384+
output_prompt = (
385+
Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n")
386+
+ Text("".join(output_lines))
387+
+ Text.from_markup(R"[red]\[end of output][/]")
388+
)
389+
390+
super().__init__(
391+
message=(
392+
f"[green]{escape(command_description)}[/] did not run successfully.\n"
393+
f"exit code: {exit_code}"
394+
),
395+
context=output_prompt,
396+
hint_stmt=None,
397+
note_stmt=(
398+
"This error originates from a subprocess, and is likely not a "
399+
"problem with pip."
400+
),
401+
)
402+
403+
self.command_description = command_description
404+
self.exit_code = exit_code
356405

357406
def __str__(self) -> str:
358-
return (
359-
"Command errored out with exit status {}: {} "
360-
"Check the logs for full command output."
361-
).format(self.returncode, self.description)
407+
return f"{self.command_description} exited with {self.exit_code}"
408+
409+
410+
class MetadataGenerationFailed(InstallationSubprocessError, InstallationError):
411+
reference = "metadata-generation-failed"
412+
413+
def __init__(
414+
self,
415+
*,
416+
package_details: str,
417+
) -> None:
418+
super(InstallationSubprocessError, self).__init__(
419+
message="Encountered error while generating package metadata.",
420+
context=escape(package_details),
421+
hint_stmt="See above for details.",
422+
note_stmt="This is an issue with the package mentioned above, not pip.",
423+
)
424+
425+
def __str__(self) -> str:
426+
return "metadata generation failed"
362427

363428

364429
class HashErrors(InstallationError):

src/pip/_internal/operations/build/metadata.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
from pip._vendor.pep517.wrappers import Pep517HookCaller
77

88
from pip._internal.build_env import BuildEnvironment
9+
from pip._internal.exceptions import (
10+
InstallationSubprocessError,
11+
MetadataGenerationFailed,
12+
)
913
from pip._internal.utils.subprocess import runner_with_spinner_message
1014
from pip._internal.utils.temp_dir import TempDirectory
1115

1216

13-
def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) -> str:
17+
def generate_metadata(
18+
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
19+
) -> str:
1420
"""Generate metadata using mechanisms described in PEP 517.
1521
1622
Returns the generated metadata directory.
@@ -25,6 +31,9 @@ def generate_metadata(build_env: BuildEnvironment, backend: Pep517HookCaller) ->
2531
# consider the possibility that this hook doesn't exist.
2632
runner = runner_with_spinner_message("Preparing metadata (pyproject.toml)")
2733
with backend.subprocess_runner(runner):
28-
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
34+
try:
35+
distinfo_dir = backend.prepare_metadata_for_build_wheel(metadata_dir)
36+
except InstallationSubprocessError as error:
37+
raise MetadataGenerationFailed(package_details=details) from error
2938

3039
return os.path.join(metadata_dir, distinfo_dir)

src/pip/_internal/operations/build/metadata_editable.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
from pip._vendor.pep517.wrappers import Pep517HookCaller
77

88
from pip._internal.build_env import BuildEnvironment
9+
from pip._internal.exceptions import (
10+
InstallationSubprocessError,
11+
MetadataGenerationFailed,
12+
)
913
from pip._internal.utils.subprocess import runner_with_spinner_message
1014
from pip._internal.utils.temp_dir import TempDirectory
1115

1216

1317
def generate_editable_metadata(
14-
build_env: BuildEnvironment, backend: Pep517HookCaller
18+
build_env: BuildEnvironment, backend: Pep517HookCaller, details: str
1519
) -> str:
1620
"""Generate metadata using mechanisms described in PEP 660.
1721
@@ -29,6 +33,9 @@ def generate_editable_metadata(
2933
"Preparing editable metadata (pyproject.toml)"
3034
)
3135
with backend.subprocess_runner(runner):
32-
distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
36+
try:
37+
distinfo_dir = backend.prepare_metadata_for_build_editable(metadata_dir)
38+
except InstallationSubprocessError as error:
39+
raise MetadataGenerationFailed(package_details=details) from error
3340

3441
return os.path.join(metadata_dir, distinfo_dir)

src/pip/_internal/operations/build/metadata_legacy.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
from pip._internal.build_env import BuildEnvironment
88
from pip._internal.cli.spinners import open_spinner
9-
from pip._internal.exceptions import InstallationError
9+
from pip._internal.exceptions import (
10+
InstallationError,
11+
InstallationSubprocessError,
12+
MetadataGenerationFailed,
13+
)
1014
from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args
1115
from pip._internal.utils.subprocess import call_subprocess
1216
from pip._internal.utils.temp_dir import TempDirectory
@@ -56,12 +60,15 @@ def generate_metadata(
5660

5761
with build_env:
5862
with open_spinner("Preparing metadata (setup.py)") as spinner:
59-
call_subprocess(
60-
args,
61-
cwd=source_dir,
62-
command_desc="python setup.py egg_info",
63-
spinner=spinner,
64-
)
63+
try:
64+
call_subprocess(
65+
args,
66+
cwd=source_dir,
67+
command_desc="python setup.py egg_info",
68+
spinner=spinner,
69+
)
70+
except InstallationSubprocessError as error:
71+
raise MetadataGenerationFailed(package_details=details) from error
6572

6673
# Return the .egg-info directory.
6774
return _find_egg_info(egg_info_dir)

src/pip/_internal/operations/build/wheel_legacy.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@
44

55
from pip._internal.cli.spinners import open_spinner
66
from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args
7-
from pip._internal.utils.subprocess import (
8-
LOG_DIVIDER,
9-
call_subprocess,
10-
format_command_args,
11-
)
7+
from pip._internal.utils.subprocess import call_subprocess, format_command_args
128

139
logger = logging.getLogger(__name__)
1410

@@ -28,7 +24,7 @@ def format_command_result(
2824
else:
2925
if not command_output.endswith("\n"):
3026
command_output += "\n"
31-
text += f"Command output:\n{command_output}{LOG_DIVIDER}"
27+
text += f"Command output:\n{command_output}"
3228

3329
return text
3430

@@ -86,6 +82,7 @@ def build_wheel_legacy(
8682
try:
8783
output = call_subprocess(
8884
wheel_args,
85+
command_desc="python setup.py bdist_wheel",
8986
cwd=source_dir,
9087
spinner=spinner,
9188
)

src/pip/_internal/operations/install/editable_legacy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ def install_editable(
4242
with build_env:
4343
call_subprocess(
4444
args,
45+
command_desc="python setup.py develop",
4546
cwd=unpacked_source_directory,
4647
)

src/pip/_internal/operations/install/legacy.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
from typing import List, Optional, Sequence
88

99
from pip._internal.build_env import BuildEnvironment
10-
from pip._internal.exceptions import InstallationError
10+
from pip._internal.exceptions import InstallationError, LegacyInstallFailure
1111
from pip._internal.models.scheme import Scheme
12-
from pip._internal.utils.logging import indent_log
1312
from pip._internal.utils.misc import ensure_dir
1413
from pip._internal.utils.setuptools_build import make_setuptools_install_args
1514
from pip._internal.utils.subprocess import runner_with_spinner_message
@@ -18,10 +17,6 @@
1817
logger = logging.getLogger(__name__)
1918

2019

21-
class LegacyInstallFailure(Exception):
22-
pass
23-
24-
2520
def write_installed_files_from_setuptools_record(
2621
record_lines: List[str],
2722
root: Optional[str],
@@ -98,7 +93,7 @@ def install(
9893
runner = runner_with_spinner_message(
9994
f"Running setup.py install for {req_name}"
10095
)
101-
with indent_log(), build_env:
96+
with build_env:
10297
runner(
10398
cmd=install_args,
10499
cwd=unpacked_source_directory,
@@ -111,7 +106,7 @@ def install(
111106

112107
except Exception as e:
113108
# Signal to the caller that we didn't install the new package
114-
raise LegacyInstallFailure from e
109+
raise LegacyInstallFailure(package_details=req_name) from e
115110

116111
# At this point, we have successfully installed the requirement.
117112

0 commit comments

Comments
 (0)