From 081146956349e24d901c5187685df345e6621a98 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Randy=20D=C3=B6ring?=
<30527984+radoering@users.noreply.github.com>
Date: Fri, 29 Sep 2023 11:07:38 +0200
Subject: [PATCH] installer: report more details on build failures (#8479)
---
src/poetry/installation/chef.py | 25 ++++++-
src/poetry/installation/executor.py | 23 +++++--
tests/installation/test_chef.py | 43 ++++++++++++
tests/installation/test_executor.py | 102 ++++++++++++++++++++++++++--
4 files changed, 179 insertions(+), 14 deletions(-)
diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py
index 18e99ae49bf..d734d0dcf83 100644
--- a/src/poetry/installation/chef.py
+++ b/src/poetry/installation/chef.py
@@ -36,6 +36,23 @@ class ChefError(Exception): ...
class ChefBuildError(ChefError): ...
+class ChefInstallError(ChefError):
+ def __init__(self, requirements: Collection[str], output: str, error: str) -> None:
+ message = "\n\n".join(
+ (
+ f"Failed to install {', '.join(requirements)}.",
+ f"Output:\n{output}",
+ f"Error:\n{error}",
+ )
+ )
+ super().__init__(message)
+ self._requirements = requirements
+
+ @property
+ def requirements(self) -> Collection[str]:
+ return self._requirements
+
+
class IsolatedEnv(BaseIsolatedEnv):
def __init__(self, env: Env, pool: RepositoryPool) -> None:
self._env = env
@@ -57,7 +74,7 @@ def make_extra_environ(self) -> dict[str, str]:
}
def install(self, requirements: Collection[str]) -> None:
- from cleo.io.null_io import NullIO
+ from cleo.io.buffered_io import BufferedIO
from poetry.core.packages.dependency import Dependency
from poetry.core.packages.project_package import ProjectPackage
@@ -73,8 +90,9 @@ def install(self, requirements: Collection[str]) -> None:
dependency = Dependency.create_from_pep_508(requirement)
package.add_dependency(dependency)
+ io = BufferedIO()
installer = Installer(
- NullIO(),
+ io,
self._env,
package,
Locker(self._env.path.joinpath("poetry.lock"), {}),
@@ -83,7 +101,8 @@ def install(self, requirements: Collection[str]) -> None:
InstalledRepository.load(self._env),
)
installer.update(True)
- installer.run()
+ if installer.run() != 0:
+ raise ChefInstallError(requirements, io.fetch_output(), io.fetch_error())
class Chef:
diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py
index 171616d6d00..a0ebcbcf910 100644
--- a/src/poetry/installation/executor.py
+++ b/src/poetry/installation/executor.py
@@ -20,6 +20,7 @@
from poetry.installation.chef import Chef
from poetry.installation.chef import ChefBuildError
+from poetry.installation.chef import ChefInstallError
from poetry.installation.chooser import Chooser
from poetry.installation.operations import Install
from poetry.installation.operations import Uninstall
@@ -314,8 +315,8 @@ def _execute_operation(self, operation: Operation) -> None:
with self._lock:
trace = ExceptionTrace(e)
trace.render(io)
+ pkg = operation.package
if isinstance(e, ChefBuildError):
- pkg = operation.package
pip_command = "pip wheel --no-cache-dir --use-pep517"
if pkg.develop:
requirement = pkg.source_url
@@ -324,8 +325,7 @@ def _execute_operation(self, operation: Operation) -> None:
requirement = (
pkg.to_dependency().to_pep_508().split(";")[0].strip()
)
- io.write_line("")
- io.write_line(
+ message = (
""
"Note: This error originates from the build backend,"
" and is likely not a problem with poetry"
@@ -334,19 +334,30 @@ def _execute_operation(self, operation: Operation) -> None:
f" running '{pip_command} \"{requirement}\"'."
""
)
+ elif isinstance(e, ChefInstallError):
+ message = (
+ ""
+ "Cannot install build-system.requires"
+ f" for {pkg.pretty_name}."
+ ""
+ )
elif isinstance(e, SolverProblemError):
- pkg = operation.package
- io.write_line("")
- io.write_line(
+ message = (
""
"Cannot resolve build-system.requires"
f" for {pkg.pretty_name}."
""
)
+ else:
+ message = f"Cannot install {pkg.pretty_name}."
+
+ io.write_line("")
+ io.write_line(message)
io.write_line("")
finally:
with self._lock:
self._shutdown = True
+
except KeyboardInterrupt:
try:
message = (
diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py
index d134b90f9c0..fb26057a72e 100644
--- a/tests/installation/test_chef.py
+++ b/tests/installation/test_chef.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import os
+import sys
import tempfile
from pathlib import Path
@@ -13,12 +14,19 @@
from poetry.factory import Factory
from poetry.installation.chef import Chef
+from poetry.installation.chef import ChefInstallError
+from poetry.installation.chef import IsolatedEnv
+from poetry.puzzle.exceptions import SolverProblemError
+from poetry.puzzle.provider import IncompatibleConstraintsError
from poetry.repositories import RepositoryPool
from poetry.utils.env import EnvManager
+from poetry.utils.env import ephemeral_environment
from tests.repositories.test_pypi_repository import MockRepository
if TYPE_CHECKING:
+ from collections.abc import Collection
+
from pytest_mock import MockerFixture
from poetry.utils.cache import ArtifactCache
@@ -40,6 +48,41 @@ def setup(mocker: MockerFixture, pool: RepositoryPool) -> None:
mocker.patch.object(Factory, "create_pool", return_value=pool)
+def test_isolated_env_install_success(pool: RepositoryPool) -> None:
+ with ephemeral_environment(Path(sys.executable)) as venv:
+ env = IsolatedEnv(venv, pool)
+ assert "poetry-core" not in venv.run("pip", "freeze")
+ env.install({"poetry-core"})
+ assert "poetry-core" in venv.run("pip", "freeze")
+
+
+@pytest.mark.parametrize(
+ ("requirements", "exception"),
+ [
+ ({"poetry-core==1.5.0", "poetry-core==1.6.0"}, IncompatibleConstraintsError),
+ ({"black==19.10b0", "attrs==17.4.0"}, SolverProblemError),
+ ],
+)
+def test_isolated_env_install_error(
+ requirements: Collection[str], exception: type[Exception], pool: RepositoryPool
+) -> None:
+ with ephemeral_environment(Path(sys.executable)) as venv:
+ env = IsolatedEnv(venv, pool)
+ with pytest.raises(exception):
+ env.install(requirements)
+
+
+def test_isolated_env_install_failure(
+ pool: RepositoryPool, mocker: MockerFixture
+) -> None:
+ mocker.patch("poetry.installation.installer.Installer.run", return_value=1)
+ with ephemeral_environment(Path(sys.executable)) as venv:
+ env = IsolatedEnv(venv, pool)
+ with pytest.raises(ChefInstallError) as e:
+ env.install({"a", "b>1"})
+ assert e.value.requirements == {"a", "b>1"}
+
+
def test_prepare_sdist(
config: Config,
config_cache_dir: Path,
diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py
index 1f08861feb2..4e0ad9cb091 100644
--- a/tests/installation/test_executor.py
+++ b/tests/installation/test_executor.py
@@ -1378,11 +1378,7 @@ def test_build_system_requires_not_available(
.as_posix(),
)
- return_code = executor.execute(
- [
- Install(directory_package),
- ]
- )
+ return_code = executor.execute([Install(directory_package)])
assert return_code == 1
@@ -1402,3 +1398,99 @@ def test_build_system_requires_not_available(
output = io.fetch_output().strip()
assert output.startswith(expected_start)
assert output.endswith(expected_end)
+
+
+def test_build_system_requires_install_failure(
+ mocker: MockerFixture,
+ config: Config,
+ pool: RepositoryPool,
+ io: BufferedIO,
+ mock_file_downloads: None,
+ env: MockEnv,
+ fixture_dir: FixtureDirGetter,
+) -> None:
+ mocker.patch("poetry.installation.installer.Installer.run", return_value=1)
+ mocker.patch("cleo.io.buffered_io.BufferedIO.fetch_output", return_value="output")
+ mocker.patch("cleo.io.buffered_io.BufferedIO.fetch_error", return_value="error")
+ io.set_verbosity(Verbosity.NORMAL)
+
+ executor = Executor(env, pool, config, io)
+
+ package_name = "simple-project"
+ package_version = "1.2.3"
+ directory_package = Package(
+ package_name,
+ package_version,
+ source_type="directory",
+ source_url=fixture_dir("simple_project").resolve().as_posix(),
+ )
+
+ return_code = executor.execute([Install(directory_package)])
+
+ assert return_code == 1
+
+ package_url = directory_package.source_url
+ expected_start = f"""\
+Package operations: 1 install, 0 updates, 0 removals
+
+ • Installing {package_name} ({package_version} {package_url})
+
+ ChefInstallError
+
+ Failed to install poetry-core>=1.1.0a7.
+ \
+
+ Output:
+ output
+ \
+
+ Error:
+ error
+
+"""
+ expected_end = "Cannot install build-system.requires for simple-project."
+
+ mocker.stopall() # to get real output
+ output = io.fetch_output().strip()
+ assert output.startswith(expected_start)
+ assert output.endswith(expected_end)
+
+
+def test_other_error(
+ config: Config,
+ pool: RepositoryPool,
+ io: BufferedIO,
+ mock_file_downloads: None,
+ env: MockEnv,
+ fixture_dir: FixtureDirGetter,
+) -> None:
+ io.set_verbosity(Verbosity.NORMAL)
+
+ executor = Executor(env, pool, config, io)
+
+ package_name = "simple-project"
+ package_version = "1.2.3"
+ directory_package = Package(
+ package_name,
+ package_version,
+ source_type="directory",
+ source_url=fixture_dir("non-existing").resolve().as_posix(),
+ )
+
+ return_code = executor.execute([Install(directory_package)])
+
+ assert return_code == 1
+
+ package_url = directory_package.source_url
+ expected_start = f"""\
+Package operations: 1 install, 0 updates, 0 removals
+
+ • Installing {package_name} ({package_version} {package_url})
+
+ FileNotFoundError
+"""
+ expected_end = "Cannot install simple-project."
+
+ output = io.fetch_output().strip()
+ assert output.startswith(expected_start)
+ assert output.endswith(expected_end)