Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ux): better error report when no links found #10126

Merged
merged 3 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,25 @@ Without `--` this command will fail if `${GITLAB_JOB_TOKEN}` starts with a hyphe
* `--local`: Set/Get settings that are specific to a project (in the local configuration file `poetry.toml`).
* `--migrate`: Migrate outdated configuration settings.

## debug

The `debug` command groups subcommands that are useful for, as the name suggests, debugging issues you might have
when using Poetry with your projects.

### debug info

The `debug info` command shows debug information about Poetry and your project's virtual environment.

### debug resolve

The `debug resolve` command helps when debugging dependency resolution issues. The command attempts to resolve your
dependencies and list the chosen packages and versions.

### debug tags

The `debug tags` command is useful when you want to see the supported packaging tags for your project's active
virtual environment. This is useful when Poetry cannot install any known binary distributions for a dependency.

## env

The `env` command groups subcommands to interact with the virtualenvs
Expand Down
1 change: 1 addition & 0 deletions src/poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def _load() -> Command:
# Debug commands
"debug info",
"debug resolve",
"debug tags",
# Env commands
"env activate",
"env info",
Expand Down
17 changes: 17 additions & 0 deletions src/poetry/console/commands/debug/tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations

from poetry.console.commands.env_command import EnvCommand


class DebugTagsCommand(EnvCommand):
name = "debug tags"
description = "Shows compatible tags for your project's current active environment."

def handle(self) -> int:
for tag in self.env.get_supported_tags():
self.io.write_line(
f"<c1>{tag.interpreter}</>"
f"-<c2>{tag.abi}</>"
f"-<fg=yellow;options=bold>{tag.platform}</>"
)
return 0
5 changes: 4 additions & 1 deletion src/poetry/console/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,10 @@ def get_text(
text += f"{indent}{message_text}\n{indent}\n"

if has_skipped_debug:
text += f"{indent}You can also run your <c1>poetry</> command with <c1>-v</> to see more information.\n{indent}\n"
message = ConsoleMessage(
f"{indent}You can also run your <c1>poetry</> command with <c1>-v</> to see more information.\n{indent}\n"
)
text += message.stripped if strip else message.text

return text.rstrip(f"{indent}\n")

Expand Down
84 changes: 83 additions & 1 deletion src/poetry/installation/chooser.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,16 @@ def choose_for(self, package: Package) -> Link:
Return the url of the selected archive for a given package.
"""
links = []

# these are used only for providing insightful errors to the user
unsupported_wheels = set()
links_seen = 0
wheels_skipped = 0
sdists_skipped = 0

for link in self._get_links(package):
links_seen += 1

if link.is_wheel:
if not self._no_binary_policy.allows(package.name):
logger.debug(
Expand All @@ -59,6 +68,7 @@ def choose_for(self, package: Package) -> Link:
link.filename,
package.name,
)
wheels_skipped += 1
continue

if not Wheel(link.filename).is_supported_by_environment(self._env):
Expand All @@ -67,6 +77,7 @@ def choose_for(self, package: Package) -> Link:
" environment",
link.filename,
)
unsupported_wheels.add(link.filename)
continue

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

links.append(link)

if not links:
raise RuntimeError(f"Unable to find installation candidates for {package}")
raise self._no_links_found_error(
package, links_seen, wheels_skipped, sdists_skipped, unsupported_wheels
)

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

return chosen

def _no_links_found_error(
self,
package: Package,
links_seen: int,
wheels_skipped: int,
sdists_skipped: int,
unsupported_wheels: set[str],
) -> PoetryRuntimeError:
messages = []
info = (
f"This is likely not a Poetry issue.\n\n"
f" - {links_seen} candidate(s) were identified for the package\n"
)

if wheels_skipped > 0:
info += f" - {wheels_skipped} wheel(s) were skipped due to your <c1>installer.no-binary</> policy\n"

if sdists_skipped > 0:
info += f" - {sdists_skipped} source distribution(s) were skipped due to your <c1>installer.only-binary</> policy\n"

if unsupported_wheels:
info += (
f" - {len(unsupported_wheels)} wheel(s) were skipped as your project's environment does not support "
f"the identified abi tags\n"
)

messages.append(ConsoleMessage(info.strip()))

if unsupported_wheels:
messages += [
ConsoleMessage(
"The following wheel(s) were skipped as the current project environment does not support them "
"due to abi compatibility issues.",
debug=True,
),
ConsoleMessage("\n".join(unsupported_wheels), debug=True)
.indent(" - ")
.wrap("warning"),
ConsoleMessage(
"If you would like to see the supported tags in your project environment, you can execute "
"the following command:\n\n"
" <c1>poetry debug tags</>",
debug=True,
),
]

source_hint = ""
if package.source_type and package.source_reference:
source_hint += f" ({package.source_reference})"

messages.append(
ConsoleMessage(
f"Make sure the lockfile is up-to-date. You can try one of the following;\n\n"
f" 1. <b>Regenerate lockfile: </><fg=yellow>poetry lock --no-cache --regenerate</>\n"
f" 2. <b>Update package : </><fg=yellow>poetry update --no-cache {package.name}</>\n\n"
f"If neither works, please first check to verify that the {package.name} has published wheels "
f"available from your configured source{source_hint} that are compatible with your environment"
f"- ie. operating system, architecture (x86_64, arm64 etc.), python interpreter."
)
.make_section("Solutions")
.wrap("info")
)

return PoetryRuntimeError(
reason=f"Unable to find installation candidates for {package}",
messages=messages,
)

def _get_links(self, package: Package) -> list[Link]:
if package.source_type:
assert package.source_reference is not None
Expand Down
34 changes: 34 additions & 0 deletions tests/installation/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from __future__ import annotations

import pytest

from packaging.tags import Tag

from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.repository_pool import RepositoryPool
from poetry.utils.env import MockEnv


@pytest.fixture()
def env() -> MockEnv:
return MockEnv(
supported_tags=[
Tag("cp37", "cp37", "macosx_10_15_x86_64"),
Tag("py3", "none", "any"),
]
)


@pytest.fixture()
def pool(legacy_repository: LegacyRepository) -> RepositoryPool:
pool = RepositoryPool()

pool.add_repository(PyPiRepository(disable_cache=True))
pool.add_repository(
LegacyRepository("foo", "https://legacy.foo.bar/simple/", disable_cache=True)
)
pool.add_repository(
LegacyRepository("foo2", "https://legacy.foo2.bar/simple/", disable_cache=True)
)
return pool
29 changes: 2 additions & 27 deletions tests/installation/test_chooser.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
from poetry.console.exceptions import PoetryRuntimeError
from poetry.installation.chooser import Chooser
from poetry.repositories.legacy_repository import LegacyRepository
from poetry.repositories.pypi_repository import PyPiRepository
from poetry.repositories.repository_pool import RepositoryPool
from poetry.utils.env import MockEnv


if TYPE_CHECKING:
from poetry.repositories.repository_pool import RepositoryPool
from tests.conftest import Config
from tests.types import DistributionHashGetter
from tests.types import SpecializedLegacyRepositoryMocker
Expand All @@ -28,30 +27,6 @@
LEGACY_FIXTURES = Path(__file__).parent.parent / "repositories" / "fixtures" / "legacy"


@pytest.fixture()
def env() -> MockEnv:
return MockEnv(
supported_tags=[
Tag("cp37", "cp37", "macosx_10_15_x86_64"),
Tag("py3", "none", "any"),
]
)


@pytest.fixture()
def pool(legacy_repository: LegacyRepository) -> RepositoryPool:
pool = RepositoryPool()

pool.add_repository(PyPiRepository(disable_cache=True))
pool.add_repository(
LegacyRepository("foo", "https://legacy.foo.bar/simple/", disable_cache=True)
)
pool.add_repository(
LegacyRepository("foo2", "https://legacy.foo2.bar/simple/", disable_cache=True)
)
return pool


def check_chosen_link_filename(
env: MockEnv,
source_type: str,
Expand All @@ -75,7 +50,7 @@ def check_chosen_link_filename(

try:
link = chooser.choose_for(package)
except RuntimeError as e:
except PoetryRuntimeError as e:
if filename is None:
assert (
str(e)
Expand Down
90 changes: 90 additions & 0 deletions tests/installation/test_chooser_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from poetry.core.packages.package import Package

from poetry.installation.chooser import Chooser


if TYPE_CHECKING:
from poetry.repositories.repository_pool import RepositoryPool
from poetry.utils.env import MockEnv


def test_chooser_no_links_found_error(env: MockEnv, pool: RepositoryPool) -> None:
chooser = Chooser(pool, env)
package = Package(
"demo",
"0.1.0",
source_type="legacy",
source_reference="foo",
source_url="https://legacy.foo.bar/simple/",
)

unsupported_wheels = {"demo-0.1.0-py3-none-any.whl"}
error = chooser._no_links_found_error(
package=package,
links_seen=4,
wheels_skipped=3,
sdists_skipped=1,
unsupported_wheels=unsupported_wheels,
)
assert (
error.get_text(debug=True, strip=True)
== f"""\
Unable to find installation candidates for {package.name} ({package.version})

This is likely not a Poetry issue.

- 4 candidate(s) were identified for the package
- 3 wheel(s) were skipped due to your installer.no-binary policy
- 1 source distribution(s) were skipped due to your installer.only-binary policy
- 1 wheel(s) were skipped as your project's environment does not support the identified abi tags

The following wheel(s) were skipped as the current project environment does not support them due to abi compatibility \
issues.

- {" -".join(unsupported_wheels)}

If you would like to see the supported tags in your project environment, you can execute the following command:

poetry debug tags

Solutions:
Make sure the lockfile is up-to-date. You can try one of the following;

1. Regenerate lockfile: poetry lock --no-cache --regenerate
2. Update package : poetry update --no-cache {package.name}

If neither works, please first check to verify that the {package.name} has published wheels available from your configured \
source ({package.source_reference}) that are compatible with your environment- ie. operating system, architecture \
(x86_64, arm64 etc.), python interpreter.\
"""
)

assert (
error.get_text(debug=False, strip=True)
== f"""\
Unable to find installation candidates for {package.name} ({package.version})

This is likely not a Poetry issue.

- 4 candidate(s) were identified for the package
- 3 wheel(s) were skipped due to your installer.no-binary policy
- 1 source distribution(s) were skipped due to your installer.only-binary policy
- 1 wheel(s) were skipped as your project's environment does not support the identified abi tags

Solutions:
Make sure the lockfile is up-to-date. You can try one of the following;

1. Regenerate lockfile: poetry lock --no-cache --regenerate
2. Update package : poetry update --no-cache {package.name}

If neither works, please first check to verify that the {package.name} has published wheels available from your configured \
source ({package.source_reference}) that are compatible with your environment- ie. operating system, architecture \
(x86_64, arm64 etc.), python interpreter.

You can also run your poetry command with -v to see more information.\
"""
)