From 56176fd326b06af8b72f74ef26fa3998f03fb105 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 17 Apr 2025 14:49:24 -0400 Subject: [PATCH 1/7] feat: add template dynamic-metadata plugin and dynamic_metadata_needs Signed-off-by: Henry Schreiner --- docs/configuration/dynamic.md | 27 ++++++++++- pyproject.toml | 4 +- src/scikit_build_core/build/metadata.py | 16 +++++-- .../{settings => builder}/_load_provider.py | 46 +++++++++++++++---- src/scikit_build_core/builder/get_requires.py | 2 +- src/scikit_build_core/metadata/__init__.py | 30 +++++++++++- .../metadata/fancy_pypi_readme.py | 21 +++++++-- src/scikit_build_core/metadata/regex.py | 4 +- .../dynamic_metadata/plugin_project.toml | 9 +++- tests/test_dynamic_metadata.py | 5 +- 10 files changed, 140 insertions(+), 24 deletions(-) rename src/scikit_build_core/{settings => builder}/_load_provider.py (54%) diff --git a/docs/configuration/dynamic.md b/docs/configuration/dynamic.md index f46d44bd9..5bf2830aa 100644 --- a/docs/configuration/dynamic.md +++ b/docs/configuration/dynamic.md @@ -52,7 +52,7 @@ dynamic_version() project(MyPackage VERSION ${PROJECT_VERSION}) ``` -## `version`: Regex +## Regex If you want to pull a string-valued expression (usually version) from an existing file, you can the integrated `regex` plugin to pull the information. @@ -112,6 +112,31 @@ metadata.readme.provider = "scikit_build_core.metadata.fancy_pypi_readme" # tool.hatch.metadata.hooks.fancy-pypi-readme options here ``` +```{versionchanged} 0.11.2 + +The version number feature now works. +``` + +## Template + +You can access other metadata fields and produce templated outputs. + +```toml +[tool.scikit-build.metadata.optional-dependencies] +provider = "scikit_build_core.metadata.template" +needs = ["version"] +result = {"dev" = ["subpackage=={project.version}"]} +``` + +You can specify `needs` to ensure other metadata is computed before this one. +You can use `result` to specify the output. The result must match the type of +the metadata field you are writing to. You can use `project` to access the current +metadata values. + +```{versionadded} 0.11.2 + +``` + ## `build-system.requires`: Scikit-build-core's `build.requires` If you need to inject and manipulate additional `build-system.requires`, you can diff --git a/pyproject.toml b/pyproject.toml index f6b496848..129949779 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ dependencies = [ "pathspec >=0.10.1", "tomli >=1.2.2; python_version<'3.11'", "typing-extensions >=3.10.0; python_version<'3.9'", + "graphlib_backport >=1; python_version<'3.9'", ] # Note: cmake and possibly ninja are also required if those are not already # present (user controllable) - but a system version is fine. @@ -182,7 +183,7 @@ disallow_untyped_defs = true disallow_incomplete_defs = true [[tool.mypy.overrides]] -module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv"] +module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv", "graphlib_backport"] ignore_missing_imports = true @@ -301,6 +302,7 @@ known-local-folder = ["pathutils"] "importlib.resources".msg = "Use scikit_build_core._compat.importlib.resources instead." "importlib_resources".msg = "Use scikit_build_core._compat.importlib.resources instead." "pyproject_metadata".msg = "Use scikit_build_core._vendor.pyproject_metadata instead." +"graphlib".msg = "Use scikit_build_core._compat.graphlib instead." [tool.ruff.lint.per-file-ignores] diff --git a/src/scikit_build_core/build/metadata.py b/src/scikit_build_core/build/metadata.py index a35dcb6f2..aad89b15d 100644 --- a/src/scikit_build_core/build/metadata.py +++ b/src/scikit_build_core/build/metadata.py @@ -1,6 +1,7 @@ from __future__ import annotations import copy +import inspect import sys from typing import TYPE_CHECKING, Any @@ -13,7 +14,7 @@ extras_build_system, extras_top_level, ) -from ..settings._load_provider import load_dynamic_metadata +from ..builder._load_provider import load_dynamic_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -34,7 +35,6 @@ def __dir__() -> list[str]: errors.ExceptionGroup = ExceptionGroup # type: ignore[misc, assignment] -# If pyproject-metadata eventually supports updates, this can be simplified def get_standard_metadata( pyproject_dict: Mapping[str, Any], settings: ScikitBuildSettings, @@ -49,7 +49,17 @@ def get_standard_metadata( if field not in pyproject_dict.get("project", {}).get("dynamic", []): msg = f"{field} is not in project.dynamic" raise KeyError(msg) - new_pyproject_dict["project"][field] = provider.dynamic_metadata(field, config) + + sig = inspect.signature(provider.dynamic_metadata) + if len(sig.parameters) < 3: + # Backcompat for dynamic_metadata without metadata dict + new_pyproject_dict["project"][field] = provider.dynamic_metadata( # type: ignore[call-arg] + field, config + ) + else: + new_pyproject_dict["project"][field] = provider.dynamic_metadata( + field, config, new_pyproject_dict["project"].copy() + ) new_pyproject_dict["project"]["dynamic"].remove(field) if settings.strict_config: diff --git a/src/scikit_build_core/settings/_load_provider.py b/src/scikit_build_core/builder/_load_provider.py similarity index 54% rename from src/scikit_build_core/settings/_load_provider.py rename to src/scikit_build_core/builder/_load_provider.py index d992e4218..100149584 100644 --- a/src/scikit_build_core/settings/_load_provider.py +++ b/src/scikit_build_core/builder/_load_provider.py @@ -3,7 +3,9 @@ import importlib import sys from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol, Union +from typing import TYPE_CHECKING, Any, Protocol, Union, runtime_checkable + +from .._compat.graphlib import TopologicalSorter if TYPE_CHECKING: from collections.abc import Generator, Iterable, Mapping @@ -15,34 +17,41 @@ def __dir__() -> list[str]: return __all__ +@runtime_checkable class DynamicMetadataProtocol(Protocol): def dynamic_metadata( - self, fields: Iterable[str], settings: dict[str, Any] + self, fields: Iterable[str], settings: dict[str, Any], metadata: dict[str, Any] ) -> dict[str, Any]: ... +@runtime_checkable class DynamicMetadataRequirementsProtocol(DynamicMetadataProtocol, Protocol): def get_requires_for_dynamic_metadata( self, settings: dict[str, Any] ) -> list[str]: ... +@runtime_checkable class DynamicMetadataWheelProtocol(DynamicMetadataProtocol, Protocol): def dynamic_wheel( self, field: str, settings: Mapping[str, Any] | None = None ) -> bool: ... -class DynamicMetadataRequirementsWheelProtocol( - DynamicMetadataRequirementsProtocol, DynamicMetadataWheelProtocol, Protocol -): ... +@runtime_checkable +class DynamicMetadataNeeds(DynamicMetadataProtocol, Protocol): + def dynamic_metadata_needs( + self, + field: str, + settings: Mapping[str, object] | None = None, + ) -> list[str]: ... DMProtocols = Union[ DynamicMetadataProtocol, DynamicMetadataRequirementsProtocol, DynamicMetadataWheelProtocol, - DynamicMetadataRequirementsWheelProtocol, + DynamicMetadataNeeds, ] @@ -64,14 +73,31 @@ def load_provider( sys.path.pop(0) -def load_dynamic_metadata( +def _load_dynamic_metadata( metadata: Mapping[str, Mapping[str, str]], -) -> Generator[tuple[str, DMProtocols | None, dict[str, str]], None, None]: +) -> Generator[ + tuple[str, DMProtocols | None, dict[str, str], frozenset[str]], None, None +]: for field, orig_config in metadata.items(): if "provider" in orig_config: config = dict(orig_config) provider = config.pop("provider") provider_path = config.pop("provider-path", None) - yield field, load_provider(provider, provider_path), config + loaded_provider = load_provider(provider, provider_path) + needs = frozenset( + loaded_provider.dynamic_metadata_needs(field, config) + if isinstance(loaded_provider, DynamicMetadataNeeds) + else [] + ) + yield field, loaded_provider, config, needs else: - yield field, None, dict(orig_config) + yield field, None, dict(orig_config), frozenset() + + +def load_dynamic_metadata( + metadata: Mapping[str, Mapping[str, str]], +) -> list[tuple[str, DMProtocols | None, dict[str, str]]]: + initial = {f: (p, c, n) for (f, p, c, n) in _load_dynamic_metadata(metadata)} + sorter = TopologicalSorter({f: n for f, (_, _, n) in initial.items()}) + order = sorter.static_order() + return [(f, *initial[f][:2]) for f in order] diff --git a/src/scikit_build_core/builder/get_requires.py b/src/scikit_build_core/builder/get_requires.py index 492aa1ec7..aff5c2c66 100644 --- a/src/scikit_build_core/builder/get_requires.py +++ b/src/scikit_build_core/builder/get_requires.py @@ -19,8 +19,8 @@ get_ninja_programs, ) from ..resources import resources -from ..settings._load_provider import load_provider from ..settings.skbuild_read_settings import SettingsReader +from ._load_provider import load_provider if TYPE_CHECKING: from collections.abc import Generator, Mapping diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py index bdec2fc8c..f6fa25dbf 100644 --- a/src/scikit_build_core/metadata/__init__.py +++ b/src/scikit_build_core/metadata/__init__.py @@ -1,3 +1,31 @@ from __future__ import annotations -__all__: list[str] = [] +__all__: list[str] = ["_DICT_LIST_FIELDS", "_LIST_STR_FIELDS", "_STR_FIELDS"] + + +# Name is not dynamically settable, so not in this list +_STR_FIELDS = frozenset( + [ + "version", + "description", + "requires-python", + "license", + ] +) + +# Dynamic is not dynamically settable, so not in this list +_LIST_STR_FIELDS = frozenset( + [ + "classifiers", + "keywords", + "dependencies", + "license_files", + ] +) + +_DICT_LIST_FIELDS = frozenset( + [ + "urls", + "optional-dependencies", + ] +) diff --git a/src/scikit_build_core/metadata/fancy_pypi_readme.py b/src/scikit_build_core/metadata/fancy_pypi_readme.py index 1187f3a2b..4e0b60dc8 100644 --- a/src/scikit_build_core/metadata/fancy_pypi_readme.py +++ b/src/scikit_build_core/metadata/fancy_pypi_readme.py @@ -1,10 +1,15 @@ from __future__ import annotations from pathlib import Path +from typing import Any from .._compat import tomllib -__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"] +__all__ = [ + "dynamic_metadata", + "dynamic_requires_needs", + "get_requires_for_dynamic_metadata", +] def __dir__() -> list[str]: @@ -13,7 +18,8 @@ def __dir__() -> list[str]: def dynamic_metadata( field: str, - settings: dict[str, list[str] | str] | None = None, + settings: dict[str, list[str] | str], + metadata: dict[str, Any], ) -> str | dict[str, str]: from hatch_fancy_pypi_readme._builder import build_text from hatch_fancy_pypi_readme._config import load_and_validate_config @@ -36,7 +42,9 @@ def dynamic_metadata( if hasattr(config, "substitutions"): try: # We don't have access to the version at this point - text = build_text(config.fragments, config.substitutions, "") + text = build_text( + config.fragments, config.substitutions, version=metadata["version"] + ) except TypeError: # Version 23.2.0 and before don't have a version field # pylint: disable-next=no-value-for-parameter @@ -56,3 +64,10 @@ def get_requires_for_dynamic_metadata( _settings: dict[str, object] | None = None, ) -> list[str]: return ["hatch-fancy-pypi-readme>=22.3"] + + +def dynamic_requires_needs( + _field: str, + _settings: dict[str, object], +) -> list[str]: + return ["version"] diff --git a/src/scikit_build_core/metadata/regex.py b/src/scikit_build_core/metadata/regex.py index 5f1213a65..86fa40041 100644 --- a/src/scikit_build_core/metadata/regex.py +++ b/src/scikit_build_core/metadata/regex.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from . import _STR_FIELDS + if TYPE_CHECKING: from collections.abc import Mapping @@ -22,7 +24,7 @@ def dynamic_metadata( settings: Mapping[str, Any], ) -> str: # Input validation - if field not in {"version", "description", "requires-python"}: + if field not in _STR_FIELDS: msg = "Only string fields supported by this plugin" raise RuntimeError(msg) if settings.keys() > KEYS: diff --git a/tests/packages/dynamic_metadata/plugin_project.toml b/tests/packages/dynamic_metadata/plugin_project.toml index d460feba2..ec203b7b8 100644 --- a/tests/packages/dynamic_metadata/plugin_project.toml +++ b/tests/packages/dynamic_metadata/plugin_project.toml @@ -4,12 +4,17 @@ build-backend = "scikit_build_core.build" [project] name = "fancy" -dynamic = ["readme", "version"] +dynamic = ["readme", "version", "optional-dependencies"] [tool.scikit-build.metadata] version.provider = "scikit_build_core.metadata.setuptools_scm" readme.provider = "scikit_build_core.metadata.fancy_pypi_readme" +[tool.scikit-build.metadata.optional-dependencies] +provider = "scikit_build_core.metadata.template" +needs = ["version", "name"] +result = {"dev" = ["{name}=={version}"]} + [tool.setuptools_scm] [tool.hatch.metadata.hooks.fancy-pypi-readme] @@ -19,4 +24,4 @@ content-type = "text/x-rst" text = "Fragment #1" [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] -text = "Fragment #2" +text = "Fragment #2 -- $HFPR_VERSION" diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py index 359970793..16f75c3e7 100644 --- a/tests/test_dynamic_metadata.py +++ b/tests/test_dynamic_metadata.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any import pytest +from packaging.requirements import Requirement from packaging.version import Version from scikit_build_core._compat import tomllib @@ -145,7 +146,7 @@ def test_plugin_metadata(): assert str(metadata.version) == "0.1.0" assert metadata.readme == pyproject_metadata.Readme( - "Fragment #1Fragment #2", None, "text/x-rst" + "Fragment #1Fragment #2 -- 0.1.0", None, "text/x-rst" ) assert set(GetRequires().dynamic_metadata()) == { @@ -153,6 +154,8 @@ def test_plugin_metadata(): "setuptools-scm", } + assert metadata.optional_dependencies == {"dev": [Requirement("fancy==0.1.0")]} + @pytest.mark.usefixtures("package_dynamic_metadata") def test_faulty_metadata(): From 2952e9f919cb3dda54f4985ae0cb633ebd91b5c0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:51:11 +0000 Subject: [PATCH 2/7] style: pre-commit fixes --- docs/configuration/dynamic.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/dynamic.md b/docs/configuration/dynamic.md index 5bf2830aa..36ddec138 100644 --- a/docs/configuration/dynamic.md +++ b/docs/configuration/dynamic.md @@ -130,8 +130,8 @@ result = {"dev" = ["subpackage=={project.version}"]} You can specify `needs` to ensure other metadata is computed before this one. You can use `result` to specify the output. The result must match the type of -the metadata field you are writing to. You can use `project` to access the current -metadata values. +the metadata field you are writing to. You can use `project` to access the +current metadata values. ```{versionadded} 0.11.2 From c2c2dd29a4e3478ef780ca9f0191c6b047b44ee0 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 17 Apr 2025 15:42:57 -0400 Subject: [PATCH 3/7] fix: allow more fields in regex Signed-off-by: Henry Schreiner --- docs/api/scikit_build_core.metadata.rst | 8 +++ pyproject.toml | 3 +- .../builder/_load_provider.py | 2 +- src/scikit_build_core/metadata/__init__.py | 49 ++++++++++++++++--- src/scikit_build_core/metadata/regex.py | 20 +++++--- src/scikit_build_core/metadata/template.py | 46 +++++++++++++++++ .../dynamic_metadata/plugin_project.toml | 2 +- tests/test_dynamic_metadata.py | 2 - 8 files changed, 111 insertions(+), 21 deletions(-) create mode 100644 src/scikit_build_core/metadata/template.py diff --git a/docs/api/scikit_build_core.metadata.rst b/docs/api/scikit_build_core.metadata.rst index 7073d5ee8..da1713af8 100644 --- a/docs/api/scikit_build_core.metadata.rst +++ b/docs/api/scikit_build_core.metadata.rst @@ -32,3 +32,11 @@ scikit\_build\_core.metadata.setuptools\_scm module :members: :show-inheritance: :undoc-members: + +scikit\_build\_core.metadata.template module +-------------------------------------------- + +.. automodule:: scikit_build_core.metadata.template + :members: + :show-inheritance: + :undoc-members: diff --git a/pyproject.toml b/pyproject.toml index 129949779..1c847a78c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,7 +183,7 @@ disallow_untyped_defs = true disallow_incomplete_defs = true [[tool.mypy.overrides]] -module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv", "graphlib_backport"] +module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv", "graphlib"] ignore_missing_imports = true @@ -302,7 +302,6 @@ known-local-folder = ["pathutils"] "importlib.resources".msg = "Use scikit_build_core._compat.importlib.resources instead." "importlib_resources".msg = "Use scikit_build_core._compat.importlib.resources instead." "pyproject_metadata".msg = "Use scikit_build_core._vendor.pyproject_metadata instead." -"graphlib".msg = "Use scikit_build_core._compat.graphlib instead." [tool.ruff.lint.per-file-ignores] diff --git a/src/scikit_build_core/builder/_load_provider.py b/src/scikit_build_core/builder/_load_provider.py index 100149584..2ed52b538 100644 --- a/src/scikit_build_core/builder/_load_provider.py +++ b/src/scikit_build_core/builder/_load_provider.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol, Union, runtime_checkable -from .._compat.graphlib import TopologicalSorter +from graphlib import TopologicalSorter if TYPE_CHECKING: from collections.abc import Generator, Iterable, Mapping diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py index f6fa25dbf..a01c1200c 100644 --- a/src/scikit_build_core/metadata/__init__.py +++ b/src/scikit_build_core/metadata/__init__.py @@ -1,6 +1,12 @@ from __future__ import annotations -__all__: list[str] = ["_DICT_LIST_FIELDS", "_LIST_STR_FIELDS", "_STR_FIELDS"] +import typing + +if typing.TYPE_CHECKING: + from collections.abc import Callable + + +__all__: list[str] = ["_process_dynamic_metadata"] # Name is not dynamically settable, so not in this list @@ -23,9 +29,38 @@ ] ) -_DICT_LIST_FIELDS = frozenset( - [ - "urls", - "optional-dependencies", - ] -) +T = typing.TypeVar("T", bound="str | list[str] | dict[str, str] | dict[str, list[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. + """ + + if field in _STR_FIELDS: + if not isinstance(result, str): + msg = f"Field {field!r} must be a string" + raise RuntimeError(msg) + return action(result) # type: ignore[return-value] + if field in _LIST_STR_FIELDS: + if not (isinstance(result, list) and all(isinstance(r, str) for r in result)): + msg = f"Field {field!r} must be a list of strings" + raise RuntimeError(msg) + return [action(r) for r in result] # type: ignore[return-value] + if field == "urls": + if not isinstance(result, dict) or not all( + isinstance(v, str) for v in result.values() + ): + msg = "Field 'urls' must be a dictionary of strings" + raise RuntimeError(msg) + return {k: action(v) for k, v 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() + ): + msg = "Field 'optional-dependencies' must be a dictionary of lists" + raise RuntimeError(msg) + return {k: [action(r) for r in v] for k, v in result.items()} # type: ignore[return-value] + + msg = f"Unsupported field {field!r} for action" + raise RuntimeError(msg) diff --git a/src/scikit_build_core/metadata/regex.py b/src/scikit_build_core/metadata/regex.py index 86fa40041..787406ab6 100644 --- a/src/scikit_build_core/metadata/regex.py +++ b/src/scikit_build_core/metadata/regex.py @@ -1,10 +1,11 @@ from __future__ import annotations +import functools import re from pathlib import Path from typing import TYPE_CHECKING, Any -from . import _STR_FIELDS +from . import _process_dynamic_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -19,14 +20,18 @@ def __dir__() -> list[str]: KEYS = {"input", "regex", "result", "remove"} +def _process(match: re.Match[str], remove: str, result: str) -> str: + retval = result.format(*match.groups(), **match.groupdict()) + if remove: + retval = re.sub(remove, "", retval) + return retval + + def dynamic_metadata( field: str, settings: Mapping[str, Any], ) -> str: # Input validation - if field not in _STR_FIELDS: - msg = "Only string fields supported by this plugin" - raise RuntimeError(msg) if settings.keys() > KEYS: msg = f"Only {KEYS} settings allowed by this plugin" raise RuntimeError(msg) @@ -57,7 +62,6 @@ def dynamic_metadata( msg = f"Couldn't find {regex!r} in {input_filename}" raise RuntimeError(msg) - retval = result.format(*match.groups(), **match.groupdict()) - if remove: - retval = re.sub(remove, "", retval) - return retval + return _process_dynamic_metadata( + field, functools.partial(_process, match, remove), result + ) diff --git a/src/scikit_build_core/metadata/template.py b/src/scikit_build_core/metadata/template.py new file mode 100644 index 000000000..8ac5b45aa --- /dev/null +++ b/src/scikit_build_core/metadata/template.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Mapping + +from . import _process_dynamic_metadata + +__all__ = ["dynamic_metadata", "dynamic_metadata_needs"] + + +def __dir__() -> list[str]: + return __all__ + + +KEYS = {"needs", "result"} + + +def dynamic_metadata( + field: str, + settings: Mapping[str, str | list[str] | dict[str, str] | dict[str, list[str]]], + metadata: Mapping[str, Any], +) -> str | list[str] | dict[str, str] | dict[str, list[str]]: + if settings.keys() > KEYS: + msg = f"Only {KEYS} settings allowed by this plugin" + raise RuntimeError(msg) + + if "result" not in settings: + msg = "Must contain the 'result' setting with a template substitution" + raise RuntimeError(msg) + + result = settings["result"] + + return _process_dynamic_metadata( + field, + lambda r: r.format(**metadata), + result, + ) + + +def dynamic_metadata_needs( + field: str, # noqa: ARG001 + settings: Mapping[str, Any], +) -> list[str]: + return settings.get("needs", []) diff --git a/tests/packages/dynamic_metadata/plugin_project.toml b/tests/packages/dynamic_metadata/plugin_project.toml index ec203b7b8..f3d7ae283 100644 --- a/tests/packages/dynamic_metadata/plugin_project.toml +++ b/tests/packages/dynamic_metadata/plugin_project.toml @@ -12,7 +12,7 @@ readme.provider = "scikit_build_core.metadata.fancy_pypi_readme" [tool.scikit-build.metadata.optional-dependencies] provider = "scikit_build_core.metadata.template" -needs = ["version", "name"] +needs = ["version"] result = {"dev" = ["{name}=={version}"]} [tool.setuptools_scm] diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py index 16f75c3e7..e11308282 100644 --- a/tests/test_dynamic_metadata.py +++ b/tests/test_dynamic_metadata.py @@ -277,8 +277,6 @@ def test_regex(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: def test_regex_errors() -> None: with pytest.raises(RuntimeError): regex.dynamic_metadata("version", {}) - with pytest.raises(RuntimeError, match="Only string fields supported"): - regex.dynamic_metadata("author", {"input": "x", "regex": "x"}) def test_multipart_regex(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: From cbddac077422e0e2874bcaa4e6226eac07a5bcec Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Fri, 18 Apr 2025 15:32:32 -0400 Subject: [PATCH 4/7] fix: ensure needs always works Signed-off-by: Henry Schreiner --- .../builder/_load_provider.py | 14 +++++++++- src/scikit_build_core/metadata/__init__.py | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/scikit_build_core/builder/_load_provider.py b/src/scikit_build_core/builder/_load_provider.py index 2ed52b538..8cd1df67e 100644 --- a/src/scikit_build_core/builder/_load_provider.py +++ b/src/scikit_build_core/builder/_load_provider.py @@ -10,6 +10,8 @@ if TYPE_CHECKING: from collections.abc import Generator, Iterable, Mapping +from ..metadata import _ALL_FIELDS + __all__ = ["load_dynamic_metadata", "load_provider"] @@ -80,6 +82,9 @@ def _load_dynamic_metadata( ]: for field, orig_config in metadata.items(): if "provider" in orig_config: + if field not in _ALL_FIELDS: + msg = f"{field} is not a valid field" + raise KeyError(msg) config = dict(orig_config) provider = config.pop("provider") provider_path = config.pop("provider-path", None) @@ -89,6 +94,9 @@ def _load_dynamic_metadata( if isinstance(loaded_provider, DynamicMetadataNeeds) else [] ) + if needs > _ALL_FIELDS: + msg = f"Invalid dyanmic_metada_needs: {needs - _ALL_FIELDS}" + raise KeyError(msg) yield field, loaded_provider, config, needs else: yield field, None, dict(orig_config), frozenset() @@ -98,6 +106,10 @@ def load_dynamic_metadata( metadata: Mapping[str, Mapping[str, str]], ) -> list[tuple[str, DMProtocols | None, dict[str, str]]]: initial = {f: (p, c, n) for (f, p, c, n) in _load_dynamic_metadata(metadata)} - sorter = TopologicalSorter({f: n for f, (_, _, n) in initial.items()}) + + dynamic_fields = initial.keys() + sorter = TopologicalSorter( + {f: n & dynamic_fields for f, (_, _, n) in initial.items()} + ) order = sorter.static_order() return [(f, *initial[f][:2]) for f in order] diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py index a01c1200c..8ce5546ee 100644 --- a/src/scikit_build_core/metadata/__init__.py +++ b/src/scikit_build_core/metadata/__init__.py @@ -6,7 +6,7 @@ from collections.abc import Callable -__all__: list[str] = ["_process_dynamic_metadata"] +__all__: list[str] = ["_ALL_FIELDS", "_process_dynamic_metadata"] # Name is not dynamically settable, so not in this list @@ -29,6 +29,27 @@ ] ) +_DICT_STR_FIELDS = frozenset( + [ + "urls", + "authors", + "maintainers", + ] +) + +# "dynamic" and "name" can't be set or requested +_ALL_FIELDS = ( + _STR_FIELDS + | _LIST_STR_FIELDS + | _DICT_STR_FIELDS + | frozenset( + [ + "optional-dependencies", + "readme", + ] + ) +) + T = typing.TypeVar("T", bound="str | list[str] | dict[str, str] | dict[str, list[str]]") @@ -47,11 +68,11 @@ def _process_dynamic_metadata(field: str, action: Callable[[str], str], result: msg = f"Field {field!r} must be a list of strings" raise RuntimeError(msg) return [action(r) for r in result] # type: ignore[return-value] - if field == "urls": + if field in _DICT_STR_FIELDS: if not isinstance(result, dict) or not all( isinstance(v, str) for v in result.values() ): - msg = "Field 'urls' must be a dictionary of strings" + 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] if field == "optional-dependencies": From c15955cde1fdf80e7af43e017fd4505823778cf9 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 23 Apr 2025 17:18:43 -0400 Subject: [PATCH 5/7] refactor: implicit needs Signed-off-by: Henry Schreiner --- docs/configuration/dynamic.md | 11 +- pyproject.toml | 3 +- src/scikit_build_core/build/metadata.py | 25 +--- .../builder/_load_provider.py | 117 ++++++++++++------ .../metadata/fancy_pypi_readme.py | 15 +-- src/scikit_build_core/metadata/template.py | 13 +- .../dynamic_metadata/plugin_project.toml | 3 +- tests/test_dynamic_metadata.py | 3 + 8 files changed, 98 insertions(+), 92 deletions(-) diff --git a/docs/configuration/dynamic.md b/docs/configuration/dynamic.md index 36ddec138..1be8bc1d5 100644 --- a/docs/configuration/dynamic.md +++ b/docs/configuration/dynamic.md @@ -124,14 +124,13 @@ You can access other metadata fields and produce templated outputs. ```toml [tool.scikit-build.metadata.optional-dependencies] provider = "scikit_build_core.metadata.template" -needs = ["version"] -result = {"dev" = ["subpackage=={project.version}"]} +result = {"dev" = ["{project[name]}=={project[version]}"]} ``` -You can specify `needs` to ensure other metadata is computed before this one. -You can use `result` to specify the output. The result must match the type of -the metadata field you are writing to. You can use `project` to access the -current metadata values. +You can use `project` to access the current metadata values. You can reference +other dynamic metadata fields, and they will be computed before this one. You +can use `result` to specify the output. The result must match the type of the +metadata field you are writing to. ```{versionadded} 0.11.2 diff --git a/pyproject.toml b/pyproject.toml index 1c847a78c..f6b496848 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ dependencies = [ "pathspec >=0.10.1", "tomli >=1.2.2; python_version<'3.11'", "typing-extensions >=3.10.0; python_version<'3.9'", - "graphlib_backport >=1; python_version<'3.9'", ] # Note: cmake and possibly ninja are also required if those are not already # present (user controllable) - but a system version is fine. @@ -183,7 +182,7 @@ disallow_untyped_defs = true disallow_incomplete_defs = true [[tool.mypy.overrides]] -module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv", "graphlib"] +module = ["numpy", "pathspec", "setuptools_scm", "hatch_fancy_pypi_readme", "virtualenv"] ignore_missing_imports = true diff --git a/src/scikit_build_core/build/metadata.py b/src/scikit_build_core/build/metadata.py index aad89b15d..91af185d9 100644 --- a/src/scikit_build_core/build/metadata.py +++ b/src/scikit_build_core/build/metadata.py @@ -1,7 +1,6 @@ from __future__ import annotations import copy -import inspect import sys from typing import TYPE_CHECKING, Any @@ -39,28 +38,12 @@ def get_standard_metadata( pyproject_dict: Mapping[str, Any], settings: ScikitBuildSettings, ) -> StandardMetadata: - new_pyproject_dict = copy.deepcopy(pyproject_dict) + new_pyproject_dict = copy.deepcopy(dict(pyproject_dict)) # Handle any dynamic metadata - for field, provider, config in load_dynamic_metadata(settings.metadata): - if provider is None: - msg = f"{field} is missing provider" - raise KeyError(msg) - if field not in pyproject_dict.get("project", {}).get("dynamic", []): - msg = f"{field} is not in project.dynamic" - raise KeyError(msg) - - sig = inspect.signature(provider.dynamic_metadata) - if len(sig.parameters) < 3: - # Backcompat for dynamic_metadata without metadata dict - new_pyproject_dict["project"][field] = provider.dynamic_metadata( # type: ignore[call-arg] - field, config - ) - else: - new_pyproject_dict["project"][field] = provider.dynamic_metadata( - field, config, new_pyproject_dict["project"].copy() - ) - new_pyproject_dict["project"]["dynamic"].remove(field) + new_pyproject_dict["project"] = load_dynamic_metadata( + new_pyproject_dict["project"], settings.metadata + ) if settings.strict_config: extra_keys_top = extras_top_level(new_pyproject_dict) diff --git a/src/scikit_build_core/builder/_load_provider.py b/src/scikit_build_core/builder/_load_provider.py index 8cd1df67e..d760e5e27 100644 --- a/src/scikit_build_core/builder/_load_provider.py +++ b/src/scikit_build_core/builder/_load_provider.py @@ -1,14 +1,19 @@ from __future__ import annotations +import dataclasses import importlib +import inspect import sys +from collections.abc import Iterator, Mapping from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol, Union, runtime_checkable -from graphlib import TopologicalSorter - if TYPE_CHECKING: - from collections.abc import Generator, Iterable, Mapping + from collections.abc import Generator, Iterable + + StrMapping = Mapping[str, Any] +else: + StrMapping = Mapping from ..metadata import _ALL_FIELDS @@ -22,7 +27,10 @@ def __dir__() -> list[str]: @runtime_checkable class DynamicMetadataProtocol(Protocol): def dynamic_metadata( - self, fields: Iterable[str], settings: dict[str, Any], metadata: dict[str, Any] + self, + fields: Iterable[str], + settings: dict[str, Any], + metadata: Mapping[str, Any], ) -> dict[str, Any]: ... @@ -40,20 +48,10 @@ def dynamic_wheel( ) -> bool: ... -@runtime_checkable -class DynamicMetadataNeeds(DynamicMetadataProtocol, Protocol): - def dynamic_metadata_needs( - self, - field: str, - settings: Mapping[str, object] | None = None, - ) -> list[str]: ... - - DMProtocols = Union[ DynamicMetadataProtocol, DynamicMetadataRequirementsProtocol, DynamicMetadataWheelProtocol, - DynamicMetadataNeeds, ] @@ -77,39 +75,76 @@ def load_provider( def _load_dynamic_metadata( metadata: Mapping[str, Mapping[str, str]], -) -> Generator[ - tuple[str, DMProtocols | None, dict[str, str], frozenset[str]], None, None -]: +) -> Generator[tuple[str, DMProtocols, dict[str, str]], None, None]: for field, orig_config in metadata.items(): - if "provider" in orig_config: - if field not in _ALL_FIELDS: - msg = f"{field} is not a valid field" - raise KeyError(msg) - config = dict(orig_config) - provider = config.pop("provider") - provider_path = config.pop("provider-path", None) - loaded_provider = load_provider(provider, provider_path) - needs = frozenset( - loaded_provider.dynamic_metadata_needs(field, config) - if isinstance(loaded_provider, DynamicMetadataNeeds) - else [] + if "provider" not in orig_config: + msg = "Missing provider in dynamic metadata" + raise KeyError(msg) + + if field not in _ALL_FIELDS: + msg = f"{field} is not a valid field" + raise KeyError(msg) + config = dict(orig_config) + provider = config.pop("provider") + provider_path = config.pop("provider-path", None) + loaded_provider = load_provider(provider, provider_path) + yield field, loaded_provider, config + + +@dataclasses.dataclass +class DynamicSettings(StrMapping): + settings: dict[str, dict[str, Any]] + project: dict[str, Any] + providers: dict[str, DMProtocols] + + def __getitem__(self, key: str) -> Any: + # Try to get the settings from either the static file or dynamic metadata provider + if key in self.project: + return self.project[key] + + # Check if we are in a loop, i.e. something else is already requesting + # this key while trying to get another key + if key not in self.providers: + dep_type = "missing" if key in self.settings else "circular" + msg = f"Encountered a {dep_type} dependency at {key}" + raise ValueError(msg) + + provider = self.providers.pop(key) + sig = inspect.signature(provider.dynamic_metadata) + if len(sig.parameters) < 3: + # Backcompat for dynamic_metadata without metadata dict + self.project[key] = provider.dynamic_metadata( # type: ignore[call-arg] + key, self.settings[key] ) - if needs > _ALL_FIELDS: - msg = f"Invalid dyanmic_metada_needs: {needs - _ALL_FIELDS}" - raise KeyError(msg) - yield field, loaded_provider, config, needs else: - yield field, None, dict(orig_config), frozenset() + self.project[key] = provider.dynamic_metadata( + key, self.settings[key], self.project + ) + self.project["dynamic"].remove(key) + + return self.project[key] + + def __iter__(self) -> Iterator[str]: + # Iterate over the keys of the static settings + yield from self.project + + # Iterate over the keys of the dynamic metadata providers + yield from self.providers + + def __len__(self) -> int: + return len(self.project) + len(self.providers) def load_dynamic_metadata( + project: Mapping[str, Any], metadata: Mapping[str, Mapping[str, str]], -) -> list[tuple[str, DMProtocols | None, dict[str, str]]]: - initial = {f: (p, c, n) for (f, p, c, n) in _load_dynamic_metadata(metadata)} +) -> dict[str, Any]: + initial = {f: (p, c) for (f, p, c) in _load_dynamic_metadata(metadata)} - dynamic_fields = initial.keys() - sorter = TopologicalSorter( - {f: n & dynamic_fields for f, (_, _, n) in initial.items()} + settings = DynamicSettings( + settings={f: c for f, (v, c) in initial.items()}, + project=dict(project), + providers={k: v for k, (v, _) in initial.items()}, ) - order = sorter.static_order() - return [(f, *initial[f][:2]) for f in order] + + return dict(settings) diff --git a/src/scikit_build_core/metadata/fancy_pypi_readme.py b/src/scikit_build_core/metadata/fancy_pypi_readme.py index 4e0b60dc8..682d614b5 100644 --- a/src/scikit_build_core/metadata/fancy_pypi_readme.py +++ b/src/scikit_build_core/metadata/fancy_pypi_readme.py @@ -1,13 +1,15 @@ from __future__ import annotations from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any from .._compat import tomllib +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = [ "dynamic_metadata", - "dynamic_requires_needs", "get_requires_for_dynamic_metadata", ] @@ -19,7 +21,7 @@ def __dir__() -> list[str]: def dynamic_metadata( field: str, settings: dict[str, list[str] | str], - metadata: dict[str, Any], + metadata: Mapping[str, Any], ) -> str | dict[str, str]: from hatch_fancy_pypi_readme._builder import build_text from hatch_fancy_pypi_readme._config import load_and_validate_config @@ -64,10 +66,3 @@ def get_requires_for_dynamic_metadata( _settings: dict[str, object] | None = None, ) -> list[str]: return ["hatch-fancy-pypi-readme>=22.3"] - - -def dynamic_requires_needs( - _field: str, - _settings: dict[str, object], -) -> list[str]: - return ["version"] diff --git a/src/scikit_build_core/metadata/template.py b/src/scikit_build_core/metadata/template.py index 8ac5b45aa..c1398da20 100644 --- a/src/scikit_build_core/metadata/template.py +++ b/src/scikit_build_core/metadata/template.py @@ -7,14 +7,14 @@ from . import _process_dynamic_metadata -__all__ = ["dynamic_metadata", "dynamic_metadata_needs"] +__all__ = ["dynamic_metadata"] def __dir__() -> list[str]: return __all__ -KEYS = {"needs", "result"} +KEYS = {"result"} def dynamic_metadata( @@ -34,13 +34,6 @@ def dynamic_metadata( return _process_dynamic_metadata( field, - lambda r: r.format(**metadata), + lambda r: r.format(project=metadata), result, ) - - -def dynamic_metadata_needs( - field: str, # noqa: ARG001 - settings: Mapping[str, Any], -) -> list[str]: - return settings.get("needs", []) diff --git a/tests/packages/dynamic_metadata/plugin_project.toml b/tests/packages/dynamic_metadata/plugin_project.toml index f3d7ae283..974acc4d6 100644 --- a/tests/packages/dynamic_metadata/plugin_project.toml +++ b/tests/packages/dynamic_metadata/plugin_project.toml @@ -12,8 +12,7 @@ readme.provider = "scikit_build_core.metadata.fancy_pypi_readme" [tool.scikit-build.metadata.optional-dependencies] provider = "scikit_build_core.metadata.template" -needs = ["version"] -result = {"dev" = ["{name}=={version}"]} +result = {"dev" = ["{project[name]}=={project[version]}"]} [tool.setuptools_scm] diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py index e11308282..9038edc55 100644 --- a/tests/test_dynamic_metadata.py +++ b/tests/test_dynamic_metadata.py @@ -159,6 +159,9 @@ def test_plugin_metadata(): @pytest.mark.usefixtures("package_dynamic_metadata") def test_faulty_metadata(): + reason_msg = "install hatch-fancy-pypi-readme to test the dynamic metadata plugins" + pytest.importorskip("hatch_fancy_pypi_readme", reason=reason_msg) + with Path("faulty_project.toml").open("rb") as ft: pyproject = tomllib.load(ft) settings_reader = SettingsReader(pyproject, {}, state="metadata_wheel") From 69e8424a0d1b270f614f54db26b8f43c99f01888 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 23 Apr 2025 18:20:15 -0400 Subject: [PATCH 6/7] fix: avoid triggering with containership checks Signed-off-by: Henry Schreiner --- src/scikit_build_core/builder/_load_provider.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/scikit_build_core/builder/_load_provider.py b/src/scikit_build_core/builder/_load_provider.py index d760e5e27..f0e2f4288 100644 --- a/src/scikit_build_core/builder/_load_provider.py +++ b/src/scikit_build_core/builder/_load_provider.py @@ -134,6 +134,9 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self.project) + len(self.providers) + def __contains__(self, key: object) -> bool: + return key in self.project or key in self.providers + def load_dynamic_metadata( project: Mapping[str, Any], From d5e70dc8d5ddcfbd1515e2b1272c9dd3a110720e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 24 Apr 2025 12:50:27 -0400 Subject: [PATCH 7/7] tests: add tests for regex and template Signed-off-by: Henry Schreiner --- src/scikit_build_core/build/metadata.py | 4 ++-- .../builder/_load_provider.py | 24 ++++++++----------- src/scikit_build_core/metadata/__init__.py | 2 +- .../metadata/fancy_pypi_readme.py | 4 ++-- src/scikit_build_core/metadata/template.py | 4 ++-- 5 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/scikit_build_core/build/metadata.py b/src/scikit_build_core/build/metadata.py index 91af185d9..ec82601ee 100644 --- a/src/scikit_build_core/build/metadata.py +++ b/src/scikit_build_core/build/metadata.py @@ -13,7 +13,7 @@ extras_build_system, extras_top_level, ) -from ..builder._load_provider import load_dynamic_metadata +from ..builder._load_provider import process_dynamic_metadata if TYPE_CHECKING: from collections.abc import Mapping @@ -41,7 +41,7 @@ def get_standard_metadata( new_pyproject_dict = copy.deepcopy(dict(pyproject_dict)) # Handle any dynamic metadata - new_pyproject_dict["project"] = load_dynamic_metadata( + new_pyproject_dict["project"] = process_dynamic_metadata( new_pyproject_dict["project"], settings.metadata ) diff --git a/src/scikit_build_core/builder/_load_provider.py b/src/scikit_build_core/builder/_load_provider.py index f0e2f4288..cc6862f82 100644 --- a/src/scikit_build_core/builder/_load_provider.py +++ b/src/scikit_build_core/builder/_load_provider.py @@ -17,7 +17,7 @@ from ..metadata import _ALL_FIELDS -__all__ = ["load_dynamic_metadata", "load_provider"] +__all__ = ["load_provider", "process_dynamic_metadata"] def __dir__() -> list[str]: @@ -30,7 +30,7 @@ def dynamic_metadata( self, fields: Iterable[str], settings: dict[str, Any], - metadata: Mapping[str, Any], + project: Mapping[str, Any], ) -> dict[str, Any]: ... @@ -43,9 +43,7 @@ def get_requires_for_dynamic_metadata( @runtime_checkable class DynamicMetadataWheelProtocol(DynamicMetadataProtocol, Protocol): - def dynamic_wheel( - self, field: str, settings: Mapping[str, Any] | None = None - ) -> bool: ... + def dynamic_wheel(self, field: str, settings: Mapping[str, Any]) -> bool: ... DMProtocols = Union[ @@ -75,7 +73,7 @@ def load_provider( def _load_dynamic_metadata( metadata: Mapping[str, Mapping[str, str]], -) -> Generator[tuple[str, DMProtocols, dict[str, str]], None, None]: +) -> Generator[tuple[str, DMProtocols, dict[str, Any]], None, None]: for field, orig_config in metadata.items(): if "provider" not in orig_config: msg = "Missing provider in dynamic metadata" @@ -92,7 +90,7 @@ def _load_dynamic_metadata( @dataclasses.dataclass -class DynamicSettings(StrMapping): +class DynamicPyProject(StrMapping): settings: dict[str, dict[str, Any]] project: dict[str, Any] providers: dict[str, DMProtocols] @@ -117,9 +115,7 @@ def __getitem__(self, key: str) -> Any: key, self.settings[key] ) else: - self.project[key] = provider.dynamic_metadata( - key, self.settings[key], self.project - ) + self.project[key] = provider.dynamic_metadata(key, self.settings[key], self) self.project["dynamic"].remove(key) return self.project[key] @@ -138,14 +134,14 @@ def __contains__(self, key: object) -> bool: return key in self.project or key in self.providers -def load_dynamic_metadata( +def process_dynamic_metadata( project: Mapping[str, Any], - metadata: Mapping[str, Mapping[str, str]], + metadata: Mapping[str, Mapping[str, Any]], ) -> dict[str, Any]: initial = {f: (p, c) for (f, p, c) in _load_dynamic_metadata(metadata)} - settings = DynamicSettings( - settings={f: c for f, (v, c) in initial.items()}, + settings = DynamicPyProject( + settings={f: c for f, (_, c) in initial.items()}, project=dict(project), providers={k: v for k, (v, _) in initial.items()}, ) diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py index 8ce5546ee..4ec51f456 100644 --- a/src/scikit_build_core/metadata/__init__.py +++ b/src/scikit_build_core/metadata/__init__.py @@ -68,7 +68,7 @@ def _process_dynamic_metadata(field: str, action: Callable[[str], str], result: msg = f"Field {field!r} must be a list of strings" raise RuntimeError(msg) return [action(r) for r in result] # type: ignore[return-value] - if field in _DICT_STR_FIELDS: + if field in _DICT_STR_FIELDS | {"readme"}: if not isinstance(result, dict) or not all( isinstance(v, str) for v in result.values() ): diff --git a/src/scikit_build_core/metadata/fancy_pypi_readme.py b/src/scikit_build_core/metadata/fancy_pypi_readme.py index 682d614b5..f78f5ab08 100644 --- a/src/scikit_build_core/metadata/fancy_pypi_readme.py +++ b/src/scikit_build_core/metadata/fancy_pypi_readme.py @@ -21,7 +21,7 @@ def __dir__() -> list[str]: def dynamic_metadata( field: str, settings: dict[str, list[str] | str], - metadata: Mapping[str, Any], + project: Mapping[str, Any], ) -> str | dict[str, str]: from hatch_fancy_pypi_readme._builder import build_text from hatch_fancy_pypi_readme._config import load_and_validate_config @@ -45,7 +45,7 @@ def dynamic_metadata( try: # We don't have access to the version at this point text = build_text( - config.fragments, config.substitutions, version=metadata["version"] + config.fragments, config.substitutions, version=project["version"] ) except TypeError: # Version 23.2.0 and before don't have a version field diff --git a/src/scikit_build_core/metadata/template.py b/src/scikit_build_core/metadata/template.py index c1398da20..749d1b569 100644 --- a/src/scikit_build_core/metadata/template.py +++ b/src/scikit_build_core/metadata/template.py @@ -20,7 +20,7 @@ def __dir__() -> list[str]: def dynamic_metadata( field: str, settings: Mapping[str, str | list[str] | dict[str, str] | dict[str, list[str]]], - metadata: Mapping[str, Any], + project: Mapping[str, Any], ) -> str | list[str] | dict[str, str] | dict[str, list[str]]: if settings.keys() > KEYS: msg = f"Only {KEYS} settings allowed by this plugin" @@ -34,6 +34,6 @@ def dynamic_metadata( return _process_dynamic_metadata( field, - lambda r: r.format(project=metadata), + lambda r: r.format(project=project), result, )