diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 3a246a1e349..98844e70287 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -169,10 +169,14 @@ def install( args.append("--prefer-binary") args.append("--") args.extend(requirements) + + identify_requirement = ( + f" for {for_req.name}" if for_req and for_req.name else "" + ) with open_spinner(f"Installing {kind}") as spinner: call_subprocess( args, - command_desc=f"pip subprocess to install {kind}", + command_desc=f"installing {kind}{identify_requirement}", spinner=spinner, ) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 98f95494c62..d6e9095fb55 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -190,6 +190,23 @@ class InstallationError(PipError): """General exception during installation""" +class FailedToPrepareCandidate(InstallationError): + """Raised when we fail to prepare a candidate (i.e. fetch and generate metadata). + + This is intentionally not a diagnostic error, since the output will be presented + above this error, when this occurs. This should instead present information to the + user. + """ + + def __init__( + self, *, package_name: str, requirement_chain: str, failed_step: str + ) -> None: + super().__init__(f"Failed to build '{package_name}' when {failed_step.lower()}") + self.package_name = package_name + self.requirement_chain = requirement_chain + self.failed_step = failed_step + + class MissingPyProjectBuildRequires(DiagnosticPipError): """Raised when pyproject.toml has `build-system`, but no `build-system.requires`.""" @@ -384,7 +401,7 @@ def __init__( output_lines: list[str] | None, ) -> None: if output_lines is None: - output_prompt = Text("See above for output.") + output_prompt = Text("No available output.") else: output_prompt = ( Text.from_markup(f"[red][{len(output_lines)} lines of output][/]\n") @@ -412,7 +429,7 @@ def __str__(self) -> str: return f"{self.command_description} exited with {self.exit_code}" -class MetadataGenerationFailed(InstallationSubprocessError, InstallationError): +class MetadataGenerationFailed(DiagnosticPipError, InstallationError): reference = "metadata-generation-failed" def __init__( @@ -420,7 +437,7 @@ def __init__( *, package_details: str, ) -> None: - super(InstallationSubprocessError, self).__init__( + super().__init__( message="Encountered error while generating package metadata.", context=escape(package_details), hint_stmt="See above for details.", diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a8315349791..920eb1b9e2f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -10,6 +10,7 @@ from pip._vendor.packaging.version import Version from pip._internal.exceptions import ( + FailedToPrepareCandidate, HashError, InstallationSubprocessError, InvalidInstalledPackage, @@ -244,9 +245,19 @@ def _prepare(self) -> BaseDistribution: 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 + if isinstance(self._ireq.comes_from, InstallRequirement): + request_chain = self._ireq.comes_from.from_path() + else: + request_chain = self._ireq.comes_from + + if request_chain is None: + request_chain = "directly requested" + + raise FailedToPrepareCandidate( + package_name=self._ireq.name or str(self._link), + requirement_chain=request_chain, + failed_step=exc.command_description, + ) self._check_metadata_consistency(dist) return dist