Skip to content

Commit c5dccb8

Browse files
robsdedudessbarnea
andauthored
Fix dependency-group name normalization (#3540)
Co-authored-by: Sorin Sbarnea <[email protected]>
1 parent 7ed917b commit c5dccb8

File tree

3 files changed

+62
-16
lines changed

3 files changed

+62
-16
lines changed

docs/changelog/3539.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix dependency-group name normalization.

src/tox/tox_env/python/dependency_groups.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import sys
4+
from collections import defaultdict
45
from typing import TYPE_CHECKING, TypedDict
56

67
from packaging.requirements import InvalidRequirement, Requirement
@@ -26,28 +27,64 @@ def resolve(root: Path, groups: set[str]) -> set[Requirement]:
2627
return set()
2728
with pyproject_file.open("rb") as file_handler:
2829
pyproject = tomllib.load(file_handler)
29-
dependency_groups = pyproject["dependency-groups"]
30-
if not isinstance(dependency_groups, dict):
31-
msg = f"dependency-groups is {type(dependency_groups).__name__} instead of table"
30+
dependency_groups_raw = pyproject["dependency-groups"]
31+
if not isinstance(dependency_groups_raw, dict):
32+
msg = f"dependency-groups is {type(dependency_groups_raw).__name__} instead of table"
3233
raise Fail(msg)
34+
original_names_lookup, dependency_groups = _normalize_group_names(dependency_groups_raw)
3335
result: set[Requirement] = set()
3436
for group in groups:
35-
result = result.union(_resolve_dependency_group(dependency_groups, group))
37+
result = result.union(_resolve_dependency_group(dependency_groups, group, original_names_lookup))
3638
return result
3739

3840

41+
def _normalize_group_names(
42+
dependency_groups: dict[str, list[str] | _IncludeGroup],
43+
) -> tuple[dict[str, str], dict[str, list[str] | _IncludeGroup]]:
44+
original_names = defaultdict(list)
45+
normalized_groups = {}
46+
47+
for group_name, value in dependency_groups.items():
48+
normed_group_name: str = canonicalize_name(group_name)
49+
original_names[normed_group_name].append(group_name)
50+
normalized_groups[normed_group_name] = value
51+
52+
errors = []
53+
for normed_name, names in original_names.items():
54+
if len(names) > 1:
55+
errors.append(f"{normed_name} ({', '.join(names)})")
56+
if errors:
57+
msg = f"Duplicate dependency group names: {', '.join(errors)}"
58+
raise ValueError(msg)
59+
60+
original_names_lookup = {
61+
normed_name: original_names[0]
62+
for normed_name, original_names in original_names.items()
63+
if len(original_names) == 1
64+
}
65+
66+
return original_names_lookup, normalized_groups
67+
68+
3969
def _resolve_dependency_group(
40-
dependency_groups: dict[str, list[str] | _IncludeGroup], group: str, past_groups: tuple[str, ...] = ()
70+
dependency_groups: dict[str, list[str] | _IncludeGroup],
71+
group: str,
72+
original_names_lookup: dict[str, str],
73+
past_groups: tuple[str, ...] = (),
4174
) -> set[Requirement]:
4275
if group in past_groups:
43-
msg = f"Cyclic dependency group include: {group!r} -> {past_groups!r}"
76+
original_group = original_names_lookup.get(group, group)
77+
original_past_groups = tuple(original_names_lookup.get(g, g) for g in past_groups)
78+
msg = f"Cyclic dependency group include: {original_group!r} -> {original_past_groups!r}"
4479
raise Fail(msg)
4580
if group not in dependency_groups:
46-
msg = f"dependency group {group!r} not found"
81+
original_group = original_names_lookup.get(group, group)
82+
msg = f"dependency group {original_group!r} not found"
4783
raise Fail(msg)
4884
raw_group = dependency_groups[group]
4985
if not isinstance(raw_group, list):
50-
msg = f"dependency group {group!r} is not a list"
86+
original_group = original_names_lookup.get(group, group)
87+
msg = f"dependency group {original_group!r} is not a list"
5188
raise Fail(msg)
5289

5390
result = set()
@@ -63,7 +100,11 @@ def _resolve_dependency_group(
63100
raise Fail(msg) from exc
64101
elif isinstance(item, dict) and tuple(item.keys()) == ("include-group",):
65102
include_group = canonicalize_name(next(iter(item.values())))
66-
result = result.union(_resolve_dependency_group(dependency_groups, include_group, (*past_groups, group)))
103+
result = result.union(
104+
_resolve_dependency_group(
105+
dependency_groups, include_group, original_names_lookup, (*past_groups, group)
106+
)
107+
)
67108
else:
68109
msg = f"invalid dependency group item: {item!r}"
69110
raise Fail(msg)

tests/tox_env/python/test_python_runner.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,12 @@ def test_dependency_groups_include(tox_project: ToxProjectCreator) -> None:
261261
"furo>=2024.8.6",
262262
"sphinx>=8.0.2",
263263
]
264+
"friendly.Bard" = [
265+
"bard-song",
266+
]
264267
type = [
265268
{include-group = "test"},
269+
{include-group = "FrIeNdLy-._.-bArD"},
266270
"mypy>=1",
267271
]
268272
""",
@@ -278,7 +282,7 @@ def test_dependency_groups_include(tox_project: ToxProjectCreator) -> None:
278282
(
279283
"py",
280284
"install_dependency-groups",
281-
["python", "-I", "-m", "pip", "install", "furo>=2024.8.6", "mypy>=1", "sphinx>=8.0.2"],
285+
["python", "-I", "-m", "pip", "install", "bard-song", "furo>=2024.8.6", "mypy>=1", "sphinx>=8.0.2"],
282286
)
283287
]
284288

@@ -330,18 +334,18 @@ def test_dependency_groups_not_list(tox_project: ToxProjectCreator) -> None:
330334
"tox.toml": """
331335
[env_run_base]
332336
skip_install = true
333-
dependency_groups = ["test"]
337+
dependency_groups = ["tEst"]
334338
""",
335339
"pyproject.toml": """
336340
[dependency-groups]
337-
test = 1
341+
teSt = 1
338342
""",
339343
},
340344
)
341345
result = project.run("r", "-e", "py")
342346

343347
result.assert_failed()
344-
assert "py: failed with dependency group 'test' is not a list\n" in result.out
348+
assert "py: failed with dependency group 'teSt' is not a list\n" in result.out
345349

346350

347351
def test_dependency_groups_bad_requirement(tox_project: ToxProjectCreator) -> None:
@@ -398,12 +402,12 @@ def test_dependency_groups_cyclic(tox_project: ToxProjectCreator) -> None:
398402
""",
399403
"pyproject.toml": """
400404
[dependency-groups]
401-
test = [ { include-group = "type" } ]
402-
type = [ { include-group = "test" } ]
405+
teSt = [ { include-group = "type" } ]
406+
tyPe = [ { include-group = "test" } ]
403407
""",
404408
},
405409
)
406410
result = project.run("r", "-e", "py")
407411

408412
result.assert_failed()
409-
assert "py: failed with Cyclic dependency group include: 'test' -> ('test', 'type')\n" in result.out
413+
assert "py: failed with Cyclic dependency group include: 'teSt' -> ('teSt', 'tyPe')\n" in result.out

0 commit comments

Comments
 (0)