diff --git a/docs/changelog/3433.bugfix.rst b/docs/changelog/3433.bugfix.rst new file mode 100644 index 0000000000..47009b71b6 --- /dev/null +++ b/docs/changelog/3433.bugfix.rst @@ -0,0 +1,2 @@ +Normalize extra names when resolving dependencies so that underscores and hyphens are treated equivalently (e.g. +``extras = snake_case`` now matches ``Provides-Extra: snake-case`` in wheel metadata) - by :user:`Fridayai700`. diff --git a/src/tox/tox_env/python/virtual_env/package/util.py b/src/tox/tox_env/python/virtual_env/package/util.py index a35ca43d5f..4ffd9dfe57 100644 --- a/src/tox/tox_env/python/virtual_env/package/util.py +++ b/src/tox/tox_env/python/virtual_env/package/util.py @@ -34,23 +34,24 @@ def dependencies_with_extras_from_markers( *, available_extras: set[str] | None = None, ) -> list[Requirement]: - if available_extras is not None and extras: + normalized_extras = {canonicalize_name(e) for e in extras} + if available_extras is not None and normalized_extras: normalized_available = {canonicalize_name(e) for e in available_extras} - if unknown := extras - normalized_available: + if unknown := normalized_extras - normalized_available: available_str = ", ".join(sorted(normalized_available)) or "none" unknown_str = ", ".join(sorted(unknown)) msg = f"extras not found for package {package_name}: {unknown_str} (available: {available_str})" raise Fail(msg) result: list[Requirement] = [] found: set[str] = set() - todo: set[str | None] = extras | {None} + todo: set[str | None] = normalized_extras | {None} visited: set[str | None] = set() while todo: new_extras: set[str | None] = set() for req, extra_markers in deps_with_markers: if todo & extra_markers: if req.name == package_name: # support for recursive extras - new_extras.update(req.extras or set()) + new_extras.update(canonicalize_name(e) for e in (req.extras or set())) else: req_str = str(req) if req_str not in found: @@ -81,7 +82,7 @@ def _extract_extra_markers(req: Requirement) -> tuple[Requirement, set[str | Non while marker: extra = _get_extra(marker) if extra is not None: - extra_markers.add(extra) + extra_markers.add(canonicalize_name(extra)) if new_markers and new_markers[-1] in {"and", "or"}: del new_markers[-1] marker = markers.pop(0) if markers else None diff --git a/tests/tox_env/python/virtual_env/package/test_python_package_util.py b/tests/tox_env/python/virtual_env/package/test_python_package_util.py index c5cd000eb4..4c834cea5e 100644 --- a/tests/tox_env/python/virtual_env/package/test_python_package_util.py +++ b/tests/tox_env/python/virtual_env/package/test_python_package_util.py @@ -112,3 +112,34 @@ def test_validate_extras_normalization() -> None: def test_validate_extras_no_available() -> None: with pytest.raises(Fail, match=r"extras not found for package pkg: alpha \(available: none\)"): dependencies_with_extras([], {"alpha"}, "pkg", available_extras=set()) + + +def test_extras_underscore_hyphen_matching() -> None: + """Extras with underscores in tox.ini should match hyphens in metadata markers (#3433).""" + requires = [ + Requirement('dep-a; extra == "kebab-case"'), + Requirement('dep-b; extra == "snake-case"'), + Requirement('dep-c; extra == "kebab-case-2"'), + ] + result = dependencies_with_extras(requires, {"kebab-case", "snake_case", "kebab-case-2"}, "pkg") + assert sorted(str(r) for r in result) == ["dep-a", "dep-b", "dep-c"] + + +def test_extras_underscore_in_markers() -> None: + """Extras with underscores in markers should match hyphens in tox.ini (#3433).""" + requires = [ + Requirement('dep-a; extra == "snake_case"'), + ] + result = dependencies_with_extras(requires, {"snake-case"}, "pkg") + assert [str(r) for r in result] == ["dep-a"] + + +def test_extras_normalization_with_recursive() -> None: + """Recursive extras with underscores should be resolved correctly (#3433).""" + requires = [ + Requirement('dep1; extra == "my-extra"'), + Requirement('name[sub_extra]; extra == "my-extra"'), + Requirement('dep2; extra == "sub-extra"'), + ] + result = dependencies_with_extras(requires, {"my_extra"}, "name") + assert sorted(str(r) for r in result) == ["dep1", "dep2"]