diff --git a/src/poetry/console/commands/group_command.py b/src/poetry/console/commands/group_command.py index d876884fe55..86c031fd1ed 100644 --- a/src/poetry/console/commands/group_command.py +++ b/src/poetry/console/commands/group_command.py @@ -103,6 +103,18 @@ def activated_groups(self) -> set[NormalizedName]: key: {canonicalize_name(group) for group in key_groups} for key, key_groups in groups.items() } + # Warn if any group name was normalized (deduplicate warnings) + warned: set[str] = set() + for key_groups in groups.values(): + for group in key_groups: + normalized = canonicalize_name(group) + if normalized != group and group not in warned: + warned.add(group) + self.line_error( + f"Group '{group}' was normalized to '{normalized}'." + " Consider updating your command to use the normalized name." + ) + norm_default_groups = {canonicalize_name(name) for name in self.default_groups} return norm_groups["only"] or norm_default_groups.union( @@ -121,7 +133,9 @@ def _validate_group_options(self, group_options: dict[str, set[str]]) -> None: invalid_options = defaultdict(set) for opt, groups in group_options.items(): for group in groups: - if not self.poetry.package.has_dependency_group(group): + if not self.poetry.package.has_dependency_group( + canonicalize_name(group) + ): invalid_options[group].add(opt) if invalid_options: message_parts = [] diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index d5590dd77e1..155cd5bdde0 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -396,6 +396,63 @@ def test_invalid_groups_with_without_only( ) +@pytest.mark.parametrize( + ("option", "group_input", "normalized"), + [ + ("--with", "Foo", "foo"), + ("--without", "Foo", "foo"), + ("--only", "Foo", "foo"), + ("--with", "FOO", "foo"), + ("--with", "bim", "bim"), + ], +) +def test_non_normalized_group_name_emits_warning( + tester: CommandTester, + mocker: MockerFixture, + option: str, + group_input: str, + normalized: str, +) -> None: + """ + A warning is emitted when a group name passed via --with/--without/--only + differs from its normalized form (e.g. 'Foo' instead of 'foo'). + """ + assert isinstance(tester.command, InstallerCommand) + mocker.patch.object(tester.command.installer, "run", return_value=0) + + tester.execute(f"{option} {group_input}") + error_output = tester.io.fetch_error() + + if group_input != normalized: + assert "was normalized to" in error_output + assert f"'{group_input}'" in error_output + assert f"'{normalized}'" in error_output + else: + assert "was normalized to" not in error_output + + +def test_normalized_group_name_does_not_raise( + tester: CommandTester, + mocker: MockerFixture, +) -> None: + """ + Passing a non-normalized but valid group name (e.g. 'Foo' for group 'foo') + should not raise a GroupNotFoundError. + """ + assert isinstance(tester.command, InstallerCommand) + mocker.patch.object(tester.command.installer, "run", return_value=0) + + # Should not raise even though 'Foo' != 'foo', because they normalize to the same name + # Use --no-root to avoid editable build failure in the test project fixture + tester.execute("--with Foo --no-root") + error_output = tester.io.fetch_error() + assert tester.status_code == 0 + assert "GroupNotFoundError" not in error_output + assert "not found" not in error_output + assert "was normalized to" in error_output + assert "'Foo'" in error_output + + def test_dry_run_populates_installer( tester: CommandTester, mocker: MockerFixture ) -> None: