Skip to content

Commit 08754fe

Browse files
committed
fix(ux): better error report when no links found
1 parent 8b13bb8 commit 08754fe

File tree

4 files changed

+209
-28
lines changed

4 files changed

+209
-28
lines changed

src/poetry/installation/chooser.py

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ def choose_for(self, package: Package) -> Link:
5050
Return the url of the selected archive for a given package.
5151
"""
5252
links = []
53+
54+
# these are used only for providing insightful errors to the user
55+
unsupported_wheels = set()
56+
links_seen = 0
57+
wheels_skipped = 0
58+
sdists_skipped = 0
59+
5360
for link in self._get_links(package):
61+
links_seen += 1
62+
5463
if link.is_wheel:
5564
if not self._no_binary_policy.allows(package.name):
5665
logger.debug(
@@ -59,6 +68,7 @@ def choose_for(self, package: Package) -> Link:
5968
link.filename,
6069
package.name,
6170
)
71+
wheels_skipped += 1
6272
continue
6373

6474
if not Wheel(link.filename).is_supported_by_environment(self._env):
@@ -67,6 +77,7 @@ def choose_for(self, package: Package) -> Link:
6777
" environment",
6878
link.filename,
6979
)
80+
unsupported_wheels.add(link.filename)
7081
continue
7182

7283
if link.ext in {".egg", ".exe", ".msi", ".rpm", ".srpm"}:
@@ -80,18 +91,89 @@ def choose_for(self, package: Package) -> Link:
8091
link.filename,
8192
package.name,
8293
)
94+
sdists_skipped += 1
8395
continue
8496

8597
links.append(link)
8698

8799
if not links:
88-
raise RuntimeError(f"Unable to find installation candidates for {package}")
100+
raise self._no_links_found_error(
101+
package, links_seen, wheels_skipped, sdists_skipped, unsupported_wheels
102+
)
89103

90104
# Get the best link
91105
chosen = max(links, key=lambda link: self._sort_key(package, link))
92106

93107
return chosen
94108

109+
def _no_links_found_error(
110+
self,
111+
package: Package,
112+
links_seen: int,
113+
wheels_skipped: int,
114+
sdists_skipped: int,
115+
unsupported_wheels: set[str],
116+
) -> PoetryRuntimeError:
117+
messages = []
118+
info = (
119+
f"This is likely not a Poetry issue.\n\n"
120+
f" - {links_seen} candidate(s) were identified for the package\n"
121+
)
122+
123+
if wheels_skipped > 0:
124+
info += f" - {wheels_skipped} wheel(s) were skipped due to your <c1>installer.no-binary</> policy\n"
125+
126+
if sdists_skipped > 0:
127+
info += f" - {sdists_skipped} source distribution(s) were skipped due to your <c1>installer.only-binary</> policy\n"
128+
129+
if unsupported_wheels:
130+
info += (
131+
f" - {len(unsupported_wheels)} wheel(s) were skipped as your project's environment does not support "
132+
f"the identified abi tags\n"
133+
)
134+
135+
messages.append(ConsoleMessage(info.strip()))
136+
137+
if unsupported_wheels:
138+
messages += [
139+
ConsoleMessage(
140+
"The following wheel(s) were skipped as the current project environment does not support them "
141+
"due to abi compatibility issues.",
142+
debug=True,
143+
),
144+
ConsoleMessage("\n".join(unsupported_wheels), debug=True)
145+
.indent(" - ")
146+
.wrap("warning"),
147+
ConsoleMessage(
148+
"If you would like to see the supported tags in your project environment, you can execute "
149+
"the following command:\n\n"
150+
" <c1>poetry debug tags</>",
151+
debug=True,
152+
),
153+
]
154+
155+
source_hint = ""
156+
if package.source_type and package.source_reference:
157+
source_hint += f" ({package.source_reference})"
158+
159+
messages.append(
160+
ConsoleMessage(
161+
f"Make sure the lockfile is up-to-date. You can try one of the following;\n\n"
162+
f" 1. <b>Regenerate lockfile: </><fg=yellow>poetry lock --no-cache --regenerate</>\n"
163+
f" 2. <b>Update package : </><fg=yellow>poetry update --no-cache {package.name}</>\n\n"
164+
f"If neither works, please first check to verify that the {package.name} has published wheels "
165+
f"available from your configured source{source_hint} that are compatible with your environment"
166+
f"- ie. operating system, architecture (x86_64, arm64 etc.), python interpreter."
167+
)
168+
.make_section("Solutions")
169+
.wrap("info")
170+
)
171+
172+
return PoetryRuntimeError(
173+
reason=f"Unable to find installation candidates for {package}",
174+
messages=messages,
175+
)
176+
95177
def _get_links(self, package: Package) -> list[Link]:
96178
if package.source_type:
97179
assert package.source_reference is not None

tests/installation/conftest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from packaging.tags import Tag
6+
7+
from poetry.repositories.legacy_repository import LegacyRepository
8+
from poetry.repositories.pypi_repository import PyPiRepository
9+
from poetry.repositories.repository_pool import RepositoryPool
10+
from poetry.utils.env import MockEnv
11+
12+
13+
@pytest.fixture()
14+
def env() -> MockEnv:
15+
return MockEnv(
16+
supported_tags=[
17+
Tag("cp37", "cp37", "macosx_10_15_x86_64"),
18+
Tag("py3", "none", "any"),
19+
]
20+
)
21+
22+
23+
@pytest.fixture()
24+
def pool(legacy_repository: LegacyRepository) -> RepositoryPool:
25+
pool = RepositoryPool()
26+
27+
pool.add_repository(PyPiRepository(disable_cache=True))
28+
pool.add_repository(
29+
LegacyRepository("foo", "https://legacy.foo.bar/simple/", disable_cache=True)
30+
)
31+
pool.add_repository(
32+
LegacyRepository("foo2", "https://legacy.foo2.bar/simple/", disable_cache=True)
33+
)
34+
return pool

tests/installation/test_chooser.py

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
from poetry.console.exceptions import PoetryRuntimeError
1212
from poetry.installation.chooser import Chooser
1313
from poetry.repositories.legacy_repository import LegacyRepository
14-
from poetry.repositories.pypi_repository import PyPiRepository
15-
from poetry.repositories.repository_pool import RepositoryPool
1614
from poetry.utils.env import MockEnv
1715

1816

1917
if TYPE_CHECKING:
18+
from poetry.repositories.repository_pool import RepositoryPool
2019
from tests.conftest import Config
2120
from tests.types import DistributionHashGetter
2221
from tests.types import SpecializedLegacyRepositoryMocker
@@ -28,30 +27,6 @@
2827
LEGACY_FIXTURES = Path(__file__).parent.parent / "repositories" / "fixtures" / "legacy"
2928

3029

31-
@pytest.fixture()
32-
def env() -> MockEnv:
33-
return MockEnv(
34-
supported_tags=[
35-
Tag("cp37", "cp37", "macosx_10_15_x86_64"),
36-
Tag("py3", "none", "any"),
37-
]
38-
)
39-
40-
41-
@pytest.fixture()
42-
def pool(legacy_repository: LegacyRepository) -> RepositoryPool:
43-
pool = RepositoryPool()
44-
45-
pool.add_repository(PyPiRepository(disable_cache=True))
46-
pool.add_repository(
47-
LegacyRepository("foo", "https://legacy.foo.bar/simple/", disable_cache=True)
48-
)
49-
pool.add_repository(
50-
LegacyRepository("foo2", "https://legacy.foo2.bar/simple/", disable_cache=True)
51-
)
52-
return pool
53-
54-
5530
def check_chosen_link_filename(
5631
env: MockEnv,
5732
source_type: str,
@@ -75,7 +50,7 @@ def check_chosen_link_filename(
7550

7651
try:
7752
link = chooser.choose_for(package)
78-
except RuntimeError as e:
53+
except PoetryRuntimeError as e:
7954
if filename is None:
8055
assert (
8156
str(e)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from poetry.core.packages.package import Package
6+
7+
from poetry.installation.chooser import Chooser
8+
9+
10+
if TYPE_CHECKING:
11+
from poetry.repositories.repository_pool import RepositoryPool
12+
from poetry.utils.env import MockEnv
13+
14+
15+
def test_chooser_no_links_found_error(env: MockEnv, pool: RepositoryPool) -> None:
16+
chooser = Chooser(pool, env)
17+
package = Package(
18+
"demo",
19+
"0.1.0",
20+
source_type="legacy",
21+
source_reference="foo",
22+
source_url="https://legacy.foo.bar/simple/",
23+
)
24+
25+
unsupported_wheels = {"demo-0.1.0-py3-none-any.whl"}
26+
error = chooser._no_links_found_error(
27+
package=package,
28+
links_seen=4,
29+
wheels_skipped=3,
30+
sdists_skipped=1,
31+
unsupported_wheels=unsupported_wheels,
32+
)
33+
assert (
34+
error.get_text(debug=True, strip=True)
35+
== f"""\
36+
Unable to find installation candidates for {package.name} ({package.version})
37+
38+
This is likely not a Poetry issue.
39+
40+
- 4 candidate(s) were identified for the package
41+
- 3 wheel(s) were skipped due to your installer.no-binary policy
42+
- 1 source distribution(s) were skipped due to your installer.only-binary policy
43+
- 1 wheel(s) were skipped as your project's environment does not support the identified abi tags
44+
45+
The following wheel(s) were skipped as the current project environment does not support them due to abi compatibility \
46+
issues.
47+
48+
- {" -".join(unsupported_wheels)}
49+
50+
If you would like to see the supported tags in your project environment, you can execute the following command:
51+
52+
poetry debug tags
53+
54+
Solutions:
55+
Make sure the lockfile is up-to-date. You can try one of the following;
56+
57+
1. Regenerate lockfile: poetry lock --no-cache --regenerate
58+
2. Update package : poetry update --no-cache {package.name}
59+
60+
If neither works, please first check to verify that the {package.name} has published wheels available from your configured \
61+
source ({package.source_reference}) that are compatible with your environment- ie. operating system, architecture \
62+
(x86_64, arm64 etc.), python interpreter.\
63+
"""
64+
)
65+
66+
assert (
67+
error.get_text(debug=False, strip=True)
68+
== f"""\
69+
Unable to find installation candidates for {package.name} ({package.version})
70+
71+
This is likely not a Poetry issue.
72+
73+
- 4 candidate(s) were identified for the package
74+
- 3 wheel(s) were skipped due to your installer.no-binary policy
75+
- 1 source distribution(s) were skipped due to your installer.only-binary policy
76+
- 1 wheel(s) were skipped as your project's environment does not support the identified abi tags
77+
78+
Solutions:
79+
Make sure the lockfile is up-to-date. You can try one of the following;
80+
81+
1. Regenerate lockfile: poetry lock --no-cache --regenerate
82+
2. Update package : poetry update --no-cache {package.name}
83+
84+
If neither works, please first check to verify that the {package.name} has published wheels available from your configured \
85+
source ({package.source_reference}) that are compatible with your environment- ie. operating system, architecture \
86+
(x86_64, arm64 etc.), python interpreter.
87+
88+
You can also run your poetry command with -v to see more information.\
89+
"""
90+
)

0 commit comments

Comments
 (0)