diff --git a/docs/managing-dependencies.md b/docs/managing-dependencies.md index 16622624857..14932d43df9 100644 --- a/docs/managing-dependencies.md +++ b/docs/managing-dependencies.md @@ -163,7 +163,7 @@ poetry install --only docs ``` {{% note %}} -If you only want to install the project's runtime dependencies, you can do so with the +If you only want to install the project's runtime dependencies, you can do so with the `--only main` notation: ```bash diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index 27438bf0c07..88b9a1ce39a 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -1,11 +1,13 @@ from __future__ import annotations +from collections import defaultdict from typing import TYPE_CHECKING from cleo.helpers import option from poetry.core.packages.dependency_group import MAIN_GROUP from poetry.console.commands.command import Command +from poetry.console.exceptions import GroupNotFound if TYPE_CHECKING: @@ -78,6 +80,7 @@ def activated_groups(self) -> set[str]: for groups in self.option(key, "") for group in groups.split(",") } + self._validate_group_options(groups) for opt, new, group in [ ("no-dev", "only", MAIN_GROUP), @@ -107,3 +110,22 @@ def project_with_activated_groups_only(self) -> ProjectPackage: return self.poetry.package.with_dependency_groups( list(self.activated_groups), only=True ) + + def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: + """ + Raises en error if it detects that a group is not part of pyproject.toml + """ + invalid_options = defaultdict(set) + for opt, groups in group_options.items(): + for group in groups: + if not self.poetry.package.has_dependency_group(group): + invalid_options[group].add(opt) + if invalid_options: + message_parts = [] + for group in sorted(invalid_options): + opts = ", ".join( + f"--{opt}" + for opt in sorted(invalid_options[group]) + ) + message_parts.append(f"{group} (via {opts})") + raise GroupNotFound(f"Group(s) not found: {', '.join(message_parts)}") diff --git a/src/poetry/console/exceptions.py b/src/poetry/console/exceptions.py index aadc8c17e7f..2cc359ddb75 100644 --- a/src/poetry/console/exceptions.py +++ b/src/poetry/console/exceptions.py @@ -5,3 +5,7 @@ class PoetryConsoleError(CleoError): pass + + +class GroupNotFound(PoetryConsoleError): + pass diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 0200a452bfa..f39081872d0 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -1,5 +1,7 @@ from __future__ import annotations +import re + from typing import TYPE_CHECKING import pytest @@ -7,6 +9,8 @@ from poetry.core.masonry.utils.module import ModuleOrPackageNotFound from poetry.core.packages.dependency_group import MAIN_GROUP +from poetry.console.exceptions import GroupNotFound + if TYPE_CHECKING: from cleo.testers.command_tester import CommandTester @@ -257,6 +261,48 @@ def test_only_root_conflicts_with_without_only( ) +@pytest.mark.parametrize( + ("options", "valid_groups", "should_raise"), + [ + ({"--with": MAIN_GROUP}, {MAIN_GROUP}, False), + ({"--with": "spam"}, set(), True), + ({"--with": "spam,foo"}, {"foo"}, True), + ({"--without": "spam"}, set(), True), + ({"--without": "spam,bar"}, {"bar"}, True), + ({"--with": "eggs,ham", "--without": "spam"}, set(), True), + ({"--with": "eggs,ham", "--without": "spam,baz"}, {"baz"}, True), + ({"--only": "spam"}, set(), True), + ({"--only": "bim"}, {"bim"}, False), + ({"--only": MAIN_GROUP}, {MAIN_GROUP}, False), + ], +) +def test_invalid_groups_with_without_only( + tester: CommandTester, + mocker: MockerFixture, + options: dict[str, str], + valid_groups: set[str], + should_raise: bool, +): + mocker.patch.object(tester.command.installer, "run", return_value=0) + + cmd_args = " ".join(f"{flag} {groups}" for (flag, groups) in options.items()) + + if not should_raise: + tester.execute(cmd_args) + assert tester.status_code == 0 + else: + with pytest.raises(GroupNotFound, match=r"^Group\(s\) not found:") as e: + tester.execute(cmd_args) + assert tester.status_code is None + for opt, groups in options.items(): + group_list = groups.split(",") + invalid_groups = sorted(set(group_list) - valid_groups) + for group in invalid_groups: + assert ( + re.search(rf"{group} \(via .*{opt}.*\)", str(e.value)) is not None + ) + + def test_remove_untracked_outputs_deprecation_warning( tester: CommandTester, mocker: MockerFixture,