diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py index 4ec51f456..1319fd588 100644 --- a/src/scikit_build_core/metadata/__init__.py +++ b/src/scikit_build_core/metadata/__init__.py @@ -32,6 +32,13 @@ _DICT_STR_FIELDS = frozenset( [ "urls", + "scripts", + "gui-scripts", + ] +) + +_LIST_DICT_FIELDS = frozenset( + [ "authors", "maintainers", ] @@ -42,20 +49,25 @@ _STR_FIELDS | _LIST_STR_FIELDS | _DICT_STR_FIELDS + | _LIST_DICT_FIELDS | frozenset( [ "optional-dependencies", "readme", + "entry-points", ] ) ) -T = typing.TypeVar("T", bound="str | list[str] | dict[str, str] | dict[str, list[str]]") +T = typing.TypeVar( + "T", + bound="str | list[str] | list[dict[str, str]] | dict[str, str] | dict[str, list[str]] | dict[str, dict[str, str]]", +) def _process_dynamic_metadata(field: str, action: Callable[[str], str], result: T) -> T: """ - Helper function for processing the an action on the various possible metadata fields. + Helper function for processing an action on the various possible metadata fields. """ if field in _STR_FIELDS: @@ -74,7 +86,28 @@ def _process_dynamic_metadata(field: str, action: Callable[[str], str], result: ): msg = f"Field {field!r} must be a dictionary of strings" raise RuntimeError(msg) - return {k: action(v) for k, v in result.items()} # type: ignore[return-value] + return {action(k): action(v) for k, v in result.items()} # type: ignore[return-value] + if field in _LIST_DICT_FIELDS: + if not isinstance(result, list) or not all( + isinstance(k, str) and isinstance(v, str) + for d in result + for k, v in d.items() + ): + msg = f"Field {field!r} must be a dictionary of strings" + raise RuntimeError(msg) + return [{k: action(v) for k, v in d.items()} for d in result] # type: ignore[return-value] + if field == "entry-points": + if not isinstance(result, dict) or not all( + isinstance(d, dict) + and all(isinstance(k, str) and isinstance(v, str) for k, v in d.items()) + for d in result.values() + ): + msg = "Field 'entry-points' must be a dictionary of dictionary of strings" + raise RuntimeError(msg) + return { + dk: {action(k): action(v) for k, v in dv.items()} + for dk, dv in result.items() + } # type: ignore[return-value] if field == "optional-dependencies": if not isinstance(result, dict) or not all( isinstance(v, list) for v in result.values() diff --git a/tests/test_dynamic_metadata_unit.py b/tests/test_dynamic_metadata_unit.py index 8d6ea364c..2b45bfd0b 100644 --- a/tests/test_dynamic_metadata_unit.py +++ b/tests/test_dynamic_metadata_unit.py @@ -1,4 +1,9 @@ +from typing import Any + +import pytest + from scikit_build_core.builder._load_provider import process_dynamic_metadata +from scikit_build_core.metadata import _process_dynamic_metadata def test_template_basic() -> None: @@ -64,3 +69,37 @@ def test_regex() -> None: ) assert pyproject["requires-python"] == ">=scikit_build_core" + + +@pytest.mark.parametrize( + ("field", "input", "output"), + [ + pytest.param("version", "{sub}", "42", id="str"), + pytest.param("classifiers", ["a", "{sub}"], ["a", "42"], id="list-str"), + pytest.param( + "scripts", + {"a": "{sub}", "{sub}": "b"}, + {"a": "42", "42": "b"}, + id="dict-str", + ), + pytest.param( + "authors", [{"name": "{sub}"}], [{"name": "42"}], id="list-dict-str" + ), + pytest.param( + "optional-dependencies", + {"dev": ["{sub}"]}, + {"dev": ["42"]}, + id="dict-list-str", + ), + pytest.param("readme", {"text": "{sub}"}, {"text": "42"}, id="readme"), + pytest.param( + "entry-points", + {"ep": {"{sub}": "{sub}"}}, + {"ep": {"42": "42"}}, + id="dict-dict-str", + ), + ], +) +def test_actions(field: str, input: Any, output: Any) -> None: + result = _process_dynamic_metadata(field, lambda x: x.format(sub=42), input) + assert output == result