Repository: pdm-project/pdm

Issue:
dependency group with non-normalized name does not work in pdm 2.20.0
- [X] I have searched the issue tracker and believe that this is not a duplicate.

according to [the PEP for the new dependency groups](https://peps.python.org/pep-0735/#specification), names are allowed to have underscores but should be normalized when comparing them. this does not seem to be the case as of pdm 2.20.0 (but they did work in 2.19)

## Steps to reproduce

```toml
# happens with both the new and the old dependency group format
# [dependency-groups]
[tool.pdm.dev-dependencies]
a = [
    "libcst>=1.5.0",
]
a_b = [
    "libcst==1.1.0",
]
```
```
pdm lock --update-reuse -G a_b -v
```
## Actual behavior

it ignores the specified dependency group and instead attempts to install both groups, which results in an error in this case because the dependencies are conflicting

```
> pdm lock --update-reuse -G a_b -v
Ignoring non-existing groups: a_b
pdm.termui: ======== Start resolving requirements ========
pdm.termui:   libcst>=1.5.0
pdm.termui:   libcst==1.1.0
pdm.termui:   Adding requirement python>=3.9
pdm.termui:   Adding requirement libcst>=1.5.0
pdm.termui:   Adding requirement libcst==1.1.0
ERROR: Unable to find a resolution for libcst
because of the following conflicts:
  libcst==1.1.0 (from project)
  libcst>=1.5.0 (from project)
To fix this, you could loosen the dependency version constraints in pyproject.toml. See https://pdm-project.org/en/latest/usage/lockfile/#solve-the-locking-failure for more details.
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Scripts\pdm.EXE\__main__.py", line 8, in <module>
    sys.exit(main())
             ~~~~^^
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Lib\site-packages\pdm\core.py", line 378, in main
    return core.main(args or sys.argv[1:])
           ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Lib\site-packages\pdm\core.py", line 267, in main
    raise cast(Exception, err).with_traceback(traceback) from None
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Lib\site-packages\pdm\core.py", line 262, in main
    self.handle(project, options)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Lib\site-packages\pdm\core.py", line 194, in handle
    command.handle(project, options)
    ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Lib\site-packages\pdm\cli\commands\lock.py", line 123, in handle
    actions.do_lock(
    ~~~~~~~~~~~~~~~^
        project,
        ^^^^^^^^
    ...<6 lines>...
        append=options.append,
        ^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Users\user\Documents\asdfads\.pyprojectx\venvs\main-c43ab60f3a327ed2433bc961caec408b-py3.13\Lib\site-packages\pdm\cli\actions.py", line 150, in do_lock
    raise ResolutionImpossible("Unable to find a resolution") from None
resolvelib.resolvers.exceptions.ResolutionImpossible: Unable to find a resolution
```

## Expected behavior

<!--A clear and concise description of what you expected to happen.-->

## Environment Information

```
PDM version:
  2.20.0.post1
Python Interpreter:
  C:\Users\user\Documents\asdfads\.venv\Scripts\python.exe (3.13)
Project Root:
  C:/Users/user/Documents/asdfads
Local Packages:

{
  "implementation_name": "cpython",
  "implementation_version": "3.13.0",
  "os_name": "nt",
  "platform_machine": "AMD64",
  "platform_release": "10",
  "platform_system": "Windows",
  "platform_version": "10.0.19045",
  "python_full_version": "3.13.0",
  "platform_python_implementation": "CPython",
  "python_version": "3.13",
  "sys_platform": "win32"
}
```



Discussion:

Patch:
diff --git a/news/3247.bugfix.md b/news/3247.bugfix.md
new file mode 100644
index 0000000000..076672b5f6
--- /dev/null
+++ b/news/3247.bugfix.md
@@ -0,0 +1,1 @@
+Fix group name normalization when comparing groups.
diff --git a/src/pdm/cli/commands/add.py b/src/pdm/cli/commands/add.py
index cef31b9c35..b90d7c168c 100644
--- a/src/pdm/cli/commands/add.py
+++ b/src/pdm/cli/commands/add.py
@@ -122,7 +122,7 @@ def do_add(
             if project.enable_write_lockfile:
                 project.core.ui.info(f"Adding group [success]{group}[/] to lockfile")
             lock_groups.append(group)
-        if group == "default" or not selection.dev and group not in project.pyproject.dev_dependencies:
+        if group == "default" or not selection.dev and normalize_name(group) not in project.pyproject.dev_dependencies:
             if editables:
                 raise PdmUsageError("Cannot add editables to the default or optional dependency group")
         for r in [parse_requirement(line, True) for line in editables] + [parse_requirement(line) for line in packages]:
diff --git a/src/pdm/cli/filters.py b/src/pdm/cli/filters.py
index b8e6f217bf..d90c78ba32 100644
--- a/src/pdm/cli/filters.py
+++ b/src/pdm/cli/filters.py
@@ -5,6 +5,7 @@
 from typing import TYPE_CHECKING
 
 from pdm.exceptions import PdmUsageError
+from pdm.utils import normalize_name
 
 if TYPE_CHECKING:
     from typing import Iterator, Sequence
@@ -78,9 +79,9 @@ def _translated_groups(self) -> list[str]:
         if dev is None:  # --prod is not set, include dev-dependencies
             dev = True
         project = self.project
-        optional_groups = set(project.pyproject.metadata.get("optional-dependencies", {}))
+        optional_groups = {normalize_name(g) for g in project.pyproject.metadata.get("optional-dependencies", {})}
         dev_groups = set(project.pyproject.dev_dependencies)
-        groups_set = set(groups)
+        groups_set = {normalize_name(g) if g != ":all" else g for g in groups}
         if groups_set & dev_groups:
             if not dev:
                 raise PdmUsageError("--prod is not allowed with dev groups and should be left")
@@ -91,9 +92,9 @@ def _translated_groups(self) -> list[str]:
             groups_set.update(optional_groups)
         if default:
             groups_set.add("default")
-        groups_set -= set(self.excluded_groups)
+        groups_set -= {normalize_name(g) for g in self.excluded_groups}
 
-        invalid_groups = groups_set - set(project.iter_groups())
+        invalid_groups = groups_set - {normalize_name(g) for g in project.iter_groups()}
         if invalid_groups:
             project.core.ui.echo(
                 "[d]Ignoring non-existing groups: [success]" f"{', '.join(invalid_groups)}[/]",
diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py
index 870d82e4e3..97596dc08e 100644
--- a/src/pdm/project/core.py
+++ b/src/pdm/project/core.py
@@ -442,7 +442,8 @@ def iter_groups(self) -> Iterable[str]:
         groups = {"default"}
         if self.pyproject.metadata.get("optional-dependencies"):
             groups.update(self.pyproject.metadata["optional-dependencies"].keys())
-        groups.update(self.pyproject.dev_dependencies.keys())
+        groups.update(self.pyproject._data.get("dependency-groups", {}).keys())
+        groups.update(self.pyproject.settings.get("dev-dependencies", {}).keys())
         return groups
 
     @property
@@ -662,6 +663,8 @@ def update_dev_dependencies(deps: list[str]) -> None:
         metadata, settings = self.pyproject.metadata, self.pyproject.settings
         if group == "default":
             return metadata.get("dependencies", tomlkit.array()), lambda x: metadata.__setitem__("dependencies", x)
+        dev_dependencies = self.pyproject._data.get("dependency-groups", {})
+        dev_dependencies.update(self.pyproject.settings.get("dev-dependencies", {}))
         deps_setter = [
             (
                 metadata.get("optional-dependencies", {}),
@@ -669,11 +672,15 @@ def update_dev_dependencies(deps: list[str]) -> None:
                 if x
                 else metadata.setdefault("optional-dependencies", {}).pop(group, None),
             ),
-            (self.pyproject.dev_dependencies, update_dev_dependencies),
+            (dev_dependencies, update_dev_dependencies),
         ]
+        normalized_group = normalize_name(group)
         for deps, setter in deps_setter:
+            normalized_groups = {normalize_name(g) for g in deps}
             if group in deps:
                 return make_array(deps[group], True), setter
+            if normalized_group in normalized_groups:
+                raise PdmUsageError(f"Group {group} already exists in another non-normalized form")
         # If not found, return an empty list and a setter to add the group
         return tomlkit.array(), deps_setter[int(dev)][1]
 


Test patch:
diff --git a/tests/cli/test_add.py b/tests/cli/test_add.py
index 8da5d9610a..9176c418ac 100644
--- a/tests/cli/test_add.py
+++ b/tests/cli/test_add.py
@@ -327,3 +327,15 @@ def test_add_dependency_with_direct_minimal_versions(project, pdm, repository):
     assert "django>=1.11.8" in project.pyproject.metadata["dependencies"]
     assert all_candidates["django"].version == "1.11.8"
     assert all_candidates["pytz"].version == "2019.6"
+
+
+def test_add_group_with_normalized_name(project, pdm, working_set):
+    project.pyproject.dependency_groups.update({"foo_bar": ["requests"]})
+    project.pyproject.write()
+    pdm(["lock"], obj=project, strict=True)
+    assert "foo-bar" in project.lockfile.groups
+    pdm(["sync", "-G", "foo.bar"], obj=project, strict=True)
+    assert "requests" in working_set
+    result = pdm(["add", "-G", "foo-bar", "pytz"], obj=project)
+    assert result.exit_code != 0
+    assert "Group foo-bar already exists in another non-normalized form" in result.stderr