Skip to content

Commit

Permalink
installer: report more details on build failures (#8479)
Browse files Browse the repository at this point in the history
  • Loading branch information
radoering authored Sep 29, 2023
1 parent 2ffc820 commit 0811469
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 14 deletions.
25 changes: 22 additions & 3 deletions src/poetry/installation/chef.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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"), {}),
Expand All @@ -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:
Expand Down
23 changes: 17 additions & 6 deletions src/poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 = (
"<info>"
"Note: This error originates from the build backend,"
" and is likely not a problem with poetry"
Expand All @@ -334,19 +334,30 @@ def _execute_operation(self, operation: Operation) -> None:
f" running '{pip_command} \"{requirement}\"'."
"</info>"
)
elif isinstance(e, ChefInstallError):
message = (
"<error>"
"Cannot install build-system.requires"
f" for {pkg.pretty_name}."
"</error>"
)
elif isinstance(e, SolverProblemError):
pkg = operation.package
io.write_line("")
io.write_line(
message = (
"<error>"
"Cannot resolve build-system.requires"
f" for {pkg.pretty_name}."
"</error>"
)
else:
message = f"<error>Cannot install {pkg.pretty_name}.</error>"

io.write_line("")
io.write_line(message)
io.write_line("")
finally:
with self._lock:
self._shutdown = True

except KeyboardInterrupt:
try:
message = (
Expand Down
43 changes: 43 additions & 0 deletions tests/installation/test_chef.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import sys
import tempfile

from pathlib import Path
Expand All @@ -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
Expand All @@ -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,
Expand Down
102 changes: 97 additions & 5 deletions tests/installation/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)

0 comments on commit 0811469

Please sign in to comment.