Skip to content

Commit 140130c

Browse files
committed
unify isolated build error generation
1 parent c305c6d commit 140130c

File tree

5 files changed

+118
-109
lines changed

5 files changed

+118
-109
lines changed

src/poetry/inspection/info.py

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import functools
55
import glob
66
import logging
7-
import subprocess
87
import tempfile
98

109
from pathlib import Path
@@ -13,7 +12,6 @@
1312

1413
import pkginfo
1514

16-
from build import BuildBackendException
1715
from poetry.core.constraints.version import Version
1816
from poetry.core.factory import Factory
1917
from poetry.core.packages.dependency import Dependency
@@ -25,6 +23,7 @@
2523
from poetry.core.version.requirements import InvalidRequirementError
2624

2725
from poetry.utils.helpers import extractall
26+
from poetry.utils.isolated_build import IsolatedBuildBackendError
2827
from poetry.utils.isolated_build import isolated_builder
2928

3029

@@ -541,35 +540,8 @@ def get_pep517_metadata(path: Path) -> PackageInfo:
541540
builder.metadata_path(dest)
542541

543542
info = PackageInfo.from_metadata_directory(dest)
544-
except BuildBackendException as e:
545-
logger.debug("PEP517 build failed: %s", e)
546-
547-
if isinstance(e.exception, subprocess.CalledProcessError):
548-
inner_traceback = (
549-
e.exception.output.decode()
550-
if type(e.exception.output) is bytes
551-
else e.exception.output
552-
)
553-
inner_reason = "\n | ".join(
554-
["", str(e.exception), "", *inner_traceback.split("\n")]
555-
)
556-
reasons = [
557-
f"<warning>{inner_reason}</warning>",
558-
(
559-
"<info>"
560-
"<options=bold>Note:</> This error originates from the build backend, and is likely not a "
561-
f"problem with poetry but with the package at {path}\n\n"
562-
" (a) not supporting PEP 517 builds\n"
563-
" (b) not specifying PEP 517 build requirements correctly; or\n"
564-
" (c) the build requirement not being successfully installed in your system environment.\n\n"
565-
f'You can verify this by running <c1>pip wheel --no-cache-dir --use-pep517 "{path}"</c1>.'
566-
"</info>"
567-
),
568-
]
569-
else:
570-
reasons = [str(e), "PEP517 build failed"]
571-
572-
raise PackageInfoError(path, *reasons) from None
543+
except IsolatedBuildBackendError as e:
544+
raise PackageInfoError(path, str(e)) from None
573545

574546
if info:
575547
return info

src/poetry/installation/chef.py

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@
55
from pathlib import Path
66
from typing import TYPE_CHECKING
77

8-
from build import BuildBackendException
98
from poetry.core.utils.helpers import temporary_directory
109

11-
from poetry.utils._compat import decode
1210
from poetry.utils.helpers import extractall
13-
from poetry.utils.isolated_build import IsolatedBuildError
1411
from poetry.utils.isolated_build import isolated_builder
1512

1613

@@ -48,38 +45,19 @@ def prepare(
4845
def _prepare(
4946
self, directory: Path, destination: Path, *, editable: bool = False
5047
) -> Path:
51-
from subprocess import CalledProcessError
52-
5348
distribution: DistributionType = "editable" if editable else "wheel"
54-
error: Exception | None = None
55-
56-
try:
57-
with isolated_builder(
58-
source=directory,
59-
distribution=distribution,
60-
python_executable=self._env.python,
61-
pool=self._pool,
62-
) as builder:
63-
return Path(
64-
builder.build(
65-
distribution,
66-
destination.as_posix(),
67-
)
49+
with isolated_builder(
50+
source=directory,
51+
distribution=distribution,
52+
python_executable=self._env.python,
53+
pool=self._pool,
54+
) as builder:
55+
return Path(
56+
builder.build(
57+
distribution,
58+
destination.as_posix(),
6859
)
69-
except BuildBackendException as e:
70-
message_parts = [str(e)]
71-
72-
if isinstance(e.exception, CalledProcessError):
73-
text = e.exception.stderr or e.exception.stdout
74-
if text is not None:
75-
message_parts.append(decode(text))
76-
else:
77-
message_parts.append(str(e.exception))
78-
79-
error = IsolatedBuildError("\n\n".join(message_parts))
80-
81-
if error is not None:
82-
raise error from None
60+
)
8361

8462
def _prepare_sdist(self, archive: Path, destination: Path | None = None) -> Path:
8563
from poetry.core.packages.utils.link import Link

src/poetry/installation/executor.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from poetry.utils.helpers import get_highest_priority_hash_type
3232
from poetry.utils.helpers import pluralize
3333
from poetry.utils.helpers import remove_directory
34-
from poetry.utils.isolated_build import IsolatedBuildError
34+
from poetry.utils.isolated_build import IsolatedBuildBackendError
3535
from poetry.utils.isolated_build import IsolatedBuildInstallError
3636
from poetry.vcs.git import Git
3737

@@ -300,10 +300,13 @@ def _execute_operation(self, operation: Operation) -> None:
300300
io = self._sections.get(id(operation), self._io)
301301

302302
with self._lock:
303-
trace = ExceptionTrace(e)
304-
trace.render(io)
305303
pkg = operation.package
306-
if isinstance(e, IsolatedBuildError):
304+
with_trace = True
305+
306+
if isinstance(e, IsolatedBuildBackendError):
307+
# TODO: Revisit once upstream fix is available https://github.com/python-poetry/cleo/issues/454
308+
# we disable trace here explicitly to workaround incorrect context detection by crashtest
309+
with_trace = False
307310
pip_command = "pip wheel --no-cache-dir --use-pep517"
308311
if pkg.develop:
309312
requirement = pkg.source_url
@@ -312,14 +315,9 @@ def _execute_operation(self, operation: Operation) -> None:
312315
requirement = (
313316
pkg.to_dependency().to_pep_508().split(";")[0].strip()
314317
)
315-
message = (
316-
"<info>"
317-
"Note: This error originates from the build backend,"
318-
" and is likely not a problem with poetry"
319-
f" but with {pkg.pretty_name} ({pkg.full_pretty_version})"
320-
" not supporting PEP 517 builds. You can verify this by"
321-
f" running '{pip_command} \"{requirement}\"'."
322-
"</info>"
318+
message = e.generate_message(
319+
source_string=f"{pkg.pretty_name} ({pkg.full_pretty_version})",
320+
build_command=f'{pip_command} "{requirement}"',
323321
)
324322
elif isinstance(e, IsolatedBuildInstallError):
325323
message = (
@@ -338,6 +336,9 @@ def _execute_operation(self, operation: Operation) -> None:
338336
else:
339337
message = f"<error>Cannot install {pkg.pretty_name}.</error>"
340338

339+
if with_trace:
340+
ExceptionTrace(e).render(io)
341+
341342
io.write_line("")
342343
io.write_line(message)
343344
io.write_line("")

src/poetry/utils/isolated_build.py

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from __future__ import annotations
22

33
import os
4+
import subprocess
45

56
from contextlib import contextmanager
67
from contextlib import redirect_stdout
78
from io import StringIO
89
from typing import TYPE_CHECKING
910

11+
from build import BuildBackendException
1012
from build.env import IsolatedEnv as BaseIsolatedEnv
1113

14+
from poetry.utils._compat import decode
1215
from poetry.utils.env import Env
1316
from poetry.utils.env import EnvManager
1417
from poetry.utils.env import ephemeral_environment
@@ -28,7 +31,47 @@
2831
class IsolatedBuildBaseError(Exception): ...
2932

3033

31-
class IsolatedBuildError(IsolatedBuildBaseError): ...
34+
class IsolatedBuildBackendError(IsolatedBuildBaseError):
35+
def __init__(self, source: Path, exception: BuildBackendException) -> None:
36+
super().__init__()
37+
self.source = source
38+
self.exception = exception
39+
40+
def generate_message(
41+
self, source_string: str | None = None, build_command: str | None = None
42+
) -> str:
43+
e = self.exception.exception
44+
source_string = source_string or self.source.as_posix()
45+
build_command = (
46+
build_command
47+
or f'pip wheel --no-cache-dir --use-pep517 "{self.source.as_posix()}"'
48+
)
49+
50+
reasons = ["PEP517 build of a dependency failed", str(self.exception)]
51+
52+
if isinstance(e, subprocess.CalledProcessError):
53+
inner_traceback = decode(e.output).strip()
54+
inner_reason = "\n | ".join(
55+
["", str(e), "", *inner_traceback.split("\n")]
56+
).lstrip("\n")
57+
reasons.append(f"<warning>{inner_reason}</warning>")
58+
59+
reasons.append(
60+
"<info>"
61+
"<options=bold>Note:</> This error originates from the build backend, and is likely not a "
62+
f"problem with poetry but one of the following issues with {source_string}\n\n"
63+
" - not supporting PEP 517 builds\n"
64+
" - not specifying PEP 517 build requirements correctly\n"
65+
" - the build requirements are incompatible with your operating system or Python versions\n"
66+
" - the build requirements are missing system dependencies (eg: compilers, libraries, headers).\n\n"
67+
f"You can verify this by running <c1>{build_command}</c1>."
68+
"</info>"
69+
)
70+
71+
return "\n\n".join(reasons)
72+
73+
def __str__(self) -> str:
74+
return self.generate_message()
3275

3376

3477
class IsolatedBuildInstallError(IsolatedBuildBaseError):
@@ -140,18 +183,20 @@ def isolated_builder(
140183
) as venv:
141184
env = IsolatedEnv(venv, pool)
142185
stdout = StringIO()
186+
try:
187+
builder = ProjectBuilder.from_isolated_env(
188+
env, source, runner=quiet_subprocess_runner
189+
)
143190

144-
builder = ProjectBuilder.from_isolated_env(
145-
env, source, runner=quiet_subprocess_runner
146-
)
147-
148-
with redirect_stdout(stdout):
149-
env.install(builder.build_system_requires)
191+
with redirect_stdout(stdout):
192+
env.install(builder.build_system_requires)
150193

151-
# we repeat the build system requirements to avoid poetry installer from removing them
152-
env.install(
153-
builder.build_system_requires
154-
| builder.get_requires_for_build(distribution)
155-
)
194+
# we repeat the build system requirements to avoid poetry installer from removing them
195+
env.install(
196+
builder.build_system_requires
197+
| builder.get_requires_for_build(distribution)
198+
)
156199

157-
yield builder
200+
yield builder
201+
except BuildBackendException as e:
202+
raise IsolatedBuildBackendError(source, e) from None

tests/installation/test_executor.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,18 +1292,6 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess(
12921292
assert return_code == 1
12931293

12941294
package_url = directory_package.source_url
1295-
expected_start = f"""
1296-
Package operations: 1 install, 0 updates, 0 removals
1297-
1298-
- Installing {package_name} ({package_version} {package_url})
1299-
1300-
IsolatedBuildError
1301-
1302-
hide the original error
1303-
\
1304-
1305-
original error
1306-
"""
13071295

13081296
assert directory_package.source_url is not None
13091297
if editable:
@@ -1313,16 +1301,41 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess(
13131301
else:
13141302
pip_command = "pip wheel --no-cache-dir --use-pep517"
13151303
requirement = f"{package_name} @ {path_to_url(directory_package.source_url)}"
1316-
expected_end = f"""
1317-
Note: This error originates from the build backend, and is likely not a problem with \
1318-
poetry but with {package_name} ({package_version} {package_url}) not supporting \
1319-
PEP 517 builds. You can verify this by running '{pip_command} "{requirement}"'.
13201304

1305+
expected_source_string = f"{package_name} ({package_version} {package_url})"
1306+
expected_pip_command = f'{pip_command} "{requirement}"'
1307+
1308+
expected_output = f"""
1309+
Package operations: 1 install, 0 updates, 0 removals
1310+
1311+
- Installing {package_name} ({package_version} {package_url})
1312+
1313+
PEP517 build of a dependency failed
1314+
1315+
hide the original error
13211316
"""
13221317

1323-
output = io.fetch_output()
1324-
assert output.startswith(expected_start)
1325-
assert output.endswith(expected_end)
1318+
if isinstance(exception, CalledProcessError):
1319+
expected_output += (
1320+
"\n | Command '['pip']' returned non-zero exit status 1."
1321+
"\n | "
1322+
"\n | original error"
1323+
"\n"
1324+
)
1325+
1326+
expected_output += f"""
1327+
Note: This error originates from the build backend, and is likely not a problem with poetry but one of the following issues with {expected_source_string}
1328+
1329+
- not supporting PEP 517 builds
1330+
- not specifying PEP 517 build requirements correctly
1331+
- the build requirements are incompatible with your operating system or Python versions
1332+
- the build requirements are missing system dependencies (eg: compilers, libraries, headers).
1333+
1334+
You can verify this by running {expected_pip_command}.
1335+
1336+
"""
1337+
1338+
assert io.fetch_output() == expected_output
13261339

13271340

13281341
@pytest.mark.parametrize("encoding", ["utf-8", "latin-1"])
@@ -1362,7 +1375,7 @@ def test_build_backend_errors_are_reported_correctly_if_caused_by_subprocess_enc
13621375
return_code = executor.execute([Install(directory_package)])
13631376

13641377
assert return_code == 1
1365-
assert (stderr or stdout) in io.fetch_output()
1378+
assert stderr or stdout in io.fetch_output()
13661379

13671380

13681381
def test_build_system_requires_not_available(

0 commit comments

Comments
 (0)