diff --git a/docs/cli.md b/docs/cli.md index a5a4cb244ee..93ab69902ca 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index e4c3c3f06f2..cc300cf2ae5 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -75,6 +75,7 @@ def _load() -> Command: # Debug commands "debug info", "debug resolve", + "debug tags", # Env commands "env activate", "env info", diff --git a/src/poetry/console/commands/debug/tags.py b/src/poetry/console/commands/debug/tags.py new file mode 100644 index 00000000000..0d1fe23eb52 --- /dev/null +++ b/src/poetry/console/commands/debug/tags.py @@ -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"{tag.interpreter}" + f"-{tag.abi}" + f"-{tag.platform}" + ) + return 0 diff --git a/src/poetry/console/exceptions.py b/src/poetry/console/exceptions.py index 21385251715..cb6612ad477 100644 --- a/src/poetry/console/exceptions.py +++ b/src/poetry/console/exceptions.py @@ -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 poetry command with -v to see more information.\n{indent}\n" + message = ConsoleMessage( + f"{indent}You can also run your poetry command with -v to see more information.\n{indent}\n" + ) + text += message.stripped if strip else message.text return text.rstrip(f"{indent}\n") diff --git a/src/poetry/installation/chooser.py b/src/poetry/installation/chooser.py index 0beea8774b2..200965abb76 100644 --- a/src/poetry/installation/chooser.py +++ b/src/poetry/installation/chooser.py @@ -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( @@ -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): @@ -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"}: @@ -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 installer.no-binary policy\n" + + if sdists_skipped > 0: + info += f" - {sdists_skipped} source distribution(s) were skipped due to your 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" + " 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. Regenerate lockfile: poetry lock --no-cache --regenerate\n" + f" 2. Update package : 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 diff --git a/tests/installation/conftest.py b/tests/installation/conftest.py index e69de29bb2d..316a2756d74 100644 --- a/tests/installation/conftest.py +++ b/tests/installation/conftest.py @@ -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 diff --git a/tests/installation/test_chooser.py b/tests/installation/test_chooser.py index cfb5a74d8d7..6282165f589 100644 --- a/tests/installation/test_chooser.py +++ b/tests/installation/test_chooser.py @@ -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 @@ -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, @@ -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) diff --git a/tests/installation/test_chooser_errors.py b/tests/installation/test_chooser_errors.py new file mode 100644 index 00000000000..d4e898de4fc --- /dev/null +++ b/tests/installation/test_chooser_errors.py @@ -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.\ +""" + )