Repository: scikit-build/scikit-build-core (python)

Issue:
Autogenerate configuration reference
Although all settings are mapped to the python object, just to avoid possible confusion it would be nice to generate the original configuration options [^1] as well, something like
````markdown
```{confval} {{ field-name }}
:type: {{ type }}
:default: {{ default }}
{{ docstring }}

Python equivalent: {{ python-name }}
```
````

It would help a bit with cross-referencing the options and it would work nice with `tippy`.

[^1]: https://www.sphinx-doc.org/en/master/usage/domains/standard.html#directive-confval

Pull request description:
doc: Generate config value reference
This would make it easier to cross-reference. I was debating going for an auto-generated one and integrating with sphinx directly or going with the `nox -t gen` files, and I think I prefer the latter because it makes it more obvious to review the generated text in all the places, wdyt?

I also want to unlock usage of sphinx roles in it, but the readme generation gets in the way. My opinion is that if we have this, we could either move the readme part to the generated page, or truncate it to only display the first summary sentence similar to how `click` does it.

Depends-on: #1051
Closes #999

Patch:
diff --git a/README.md b/README.md
index f28300e..30c48fc 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ as environment variables. `tool.scikit-build` is used in toml, `skbuild.` for
 below:
 
 <!-- [[[cog
-from scikit_build_core.settings.skbuild_docs import mk_skbuild_docs
+from scikit_build_core.settings.skbuild_docs_readme import mk_skbuild_docs
 
 print("\n```toml\n[tool.scikit-build]")
 print(mk_skbuild_docs())
diff --git a/docs/api/scikit_build_core.settings.rst b/docs/api/scikit_build_core.settings.rst
index 08dad83..999a998 100644
--- a/docs/api/scikit_build_core.settings.rst
+++ b/docs/api/scikit_build_core.settings.rst
@@ -41,10 +41,18 @@ scikit\_build\_core.settings.json\_schema module
    :show-inheritance:
    :undoc-members:
 
-scikit\_build\_core.settings.skbuild\_docs module
--------------------------------------------------
+scikit\_build\_core.settings.skbuild\_docs\_readme module
+---------------------------------------------------------
+
+.. automodule:: scikit_build_core.settings.skbuild_docs_readme
+   :members:
+   :show-inheritance:
+   :undoc-members:
+
+scikit\_build\_core.settings.skbuild\_docs\_sphinx module
+---------------------------------------------------------
 
-.. automodule:: scikit_build_core.settings.skbuild_docs
+.. automodule:: scikit_build_core.settings.skbuild_docs_sphinx
    :members:
    :show-inheritance:
    :undoc-members:
diff --git a/docs/index.md b/docs/index.md
index 4792b7c..48ddea6 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -73,6 +73,7 @@ about/changelog
 
 api/scikit_build_core
 schema
+reference/configs
 ```
 
 ## Indices and tables
diff --git a/docs/reference/configs.md b/docs/reference/configs.md
new file mode 100644
index 0000000..96ea5bd
--- /dev/null
+++ b/docs/reference/configs.md
@@ -0,0 +1,443 @@
+# Config Reference
+
+The following are the available configurations in `pyproject.toml` for the
+`[tool.scikit-build]` table. These can be passed in one of the following ways
+
+````{tab} pyproject.toml
+
+```toml
+[tool.scikit-build]
+build.verbose = true
+```
+
+````
+
+`````{tab} config-settings
+
+````{tab} pip
+
+```console
+$ pip install . --config-settings=build.verbose=true
+```
+
+````
+
+````{tab} build
+
+```console
+$ pipx run build --wheel -Cbuild.verbose=true
+```
+
+````
+
+````{tab} cibuildwheel
+
+```toml
+[tool.cibuildwheel.config-settings]
+"build.verbose" = true
+```
+
+````
+
+`````
+
+````{tab} Environment
+
+```console
+$ export SKBUILD_BUILD_VERBOSE="true"
+```
+
+````
+
+<!-- [[[cog
+from scikit_build_core.settings.skbuild_docs_sphinx import mk_skbuild_docs
+
+print()
+print(mk_skbuild_docs())
+]]] -->
+
+## (top-level)
+
+```{eval-rst}
+.. confval:: build-dir
+  :type: ``str``
+
+  The build directory. Defaults to a temporary directory, but can be set.
+```
+
+```{eval-rst}
+.. confval:: experimental
+  :type: ``bool``
+  :default: false
+
+  Enable early previews of features not finalized yet.
+```
+
+```{eval-rst}
+.. confval:: fail
+  :type: ``bool``
+  :default: false
+
+  Immediately fail the build. This is only useful in overrides.
+```
+
+```{eval-rst}
+.. confval:: metadata
+  :type: ``dict[str,dict[str,Any]]``
+
+  List dynamic metadata fields and hook locations in this table.
+```
+
+```{eval-rst}
+.. confval:: minimum-version
+  :type: ``Version``
+  :default: "0.11"  # current version
+
+  If set, this will provide a method for backward compatibility.
+```
+
+```{eval-rst}
+.. confval:: strict-config
+  :type: ``bool``
+  :default: true
+
+  Strictly check all config options. If False, warnings will be printed for unknown options. If True, an error will be raised.
+```
+
+## backport
+
+```{eval-rst}
+.. confval:: backport.find-python
+  :type: ``Version``
+  :default: "3.26.1"
+
+  If CMake is less than this value, backport a copy of FindPython. Set to 0 disable this, or the empty string.
+```
+
+## build
+
+```{eval-rst}
+.. confval:: build.requires
+  :type: ``list[str]``
+
+  Additional ``build-system.requires``. Intended to be used in combination with ``overrides``.
+```
+
+```{eval-rst}
+.. confval:: build.targets
+  :type: ``list[str]``
+
+  The build targets to use when building the project. Empty builds the default target.
+```
+
+```{eval-rst}
+.. confval:: build.tool-args
+  :type: ``list[str]``
+
+  Extra args to pass directly to the builder in the build step.
+```
+
+```{eval-rst}
+.. confval:: build.verbose
+  :type: ``bool``
+  :default: false
+
+  Verbose printout when building.
+```
+
+## cmake
+
+```{eval-rst}
+.. confval:: cmake.args
+  :type: ``list[str]``
+
+  A list of args to pass to CMake when configuring the project. Setting this in config or envvar will override toml. See also ``cmake.define``.
+```
+
+```{eval-rst}
+.. confval:: cmake.build-type
+  :type: ``str``
+  :default: "Release"
+
+  The build type to use when building the project. Valid options are: "Debug", "Release", "RelWithDebInfo", "MinSizeRel", "", etc.
+```
+
+```{eval-rst}
+.. confval:: cmake.define
+  :type: ``EnvVar``
+
+  A table of defines to pass to CMake when configuring the project. Additive.
+```
+
+```{eval-rst}
+.. confval:: cmake.minimum-version
+  :type: ``Version``
+
+  DEPRECATED in 0.8; use version instead.
+```
+
+```{eval-rst}
+.. confval:: cmake.source-dir
+  :type: ``Path``
+  :default: "."
+
+  The source directory to use when building the project. Currently only affects the native builder (not the setuptools plugin).
+```
+
+```{eval-rst}
+.. confval:: cmake.targets
+  :type: ``list[str]``
+
+  DEPRECATED in 0.10; use build.targets instead.
+```
+
+```{eval-rst}
+.. confval:: cmake.verbose
+  :type: ``bool``
+
+  DEPRECATED in 0.10, use build.verbose instead.
+```
+
+```{eval-rst}
+.. confval:: cmake.version
+  :type: ``SpecifierSet``
+
+  The versions of CMake to allow. If CMake is not present on the system or does not pass this specifier, it will be downloaded via PyPI if possible. An empty string will disable this check. The default on 0.10+ is "CMakeLists.txt", which will read it from the project's CMakeLists.txt file, or ">=3.15" if unreadable or <0.10.
+```
+
+## editable
+
+```{eval-rst}
+.. confval:: editable.mode
+  :type: ``"redirect" | "inplace"``
+  :default: "redirect"
+
+  Select the editable mode to use. Can be "redirect" (default) or "inplace".
+```
+
+```{eval-rst}
+.. confval:: editable.rebuild
+  :type: ``bool``
+  :default: false
+
+  Rebuild the project when the package is imported. The build-directory must be set.
+```
+
+```{eval-rst}
+.. confval:: editable.verbose
+  :type: ``bool``
+  :default: true
+
+  Turn on verbose output for the editable mode rebuilds.
+```
+
+## generate[]
+
+```{eval-rst}
+.. confval:: generate[].location
+  :type: ``"install" | "build" | "source"``
+  :default: "install"
+
+  The place to put the generated file. The "build" directory is useful for CMake files, and the "install" directory is useful for Python files, usually. You can also write directly to the "source" directory, will overwrite existing files & remember to gitignore the file.
+```
+
+```{eval-rst}
+.. confval:: generate[].path
+  :type: ``Path``
+
+  The path (relative to platlib) for the file to generate.
+```
+
+```{eval-rst}
+.. confval:: generate[].template
+  :type: ``str``
+
+  The template to use for the file. This includes string.Template style placeholders for all the metadata. If empty, a template-path must be set.
+```
+
+```{eval-rst}
+.. confval:: generate[].template-path
+  :type: ``Path``
+
+  The path to the template file. If empty, a template must be set.
+```
+
+## install
+
+```{eval-rst}
+.. confval:: install.components
+  :type: ``list[str]``
+
+  The components to install. If empty, all default components are installed.
+```
+
+```{eval-rst}
+.. confval:: install.strip
+  :type: ``bool``
+  :default: true
+
+  Whether to strip the binaries. True for release builds on scikit-build-core 0.5+ (0.5-0.10.5 also incorrectly set this for debug builds).
+```
+
+## logging
+
+```{eval-rst}
+.. confval:: logging.level
+  :type: ``"NOTSET" | "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL"``
+  :default: "WARNING"
+
+  The logging level to display, "DEBUG", "INFO", "WARNING", and "ERROR" are possible options.
+```
+
+## messages
+
+```{eval-rst}
+.. confval:: messages.after-failure
+  :type: ``str``
+
+  A message to print after a build failure.
+```
+
+```{eval-rst}
+.. confval:: messages.after-success
+  :type: ``str``
+
+  A message to print after a successful build.
+```
+
+## ninja
+
+```{eval-rst}
+.. confval:: ninja.make-fallback
+  :type: ``bool``
+  :default: true
+
+  If Ninja is not present on the system or is older than required, it will be downloaded via PyPI if this is false.
+```
+
+```{eval-rst}
+.. confval:: ninja.minimum-version
+  :type: ``Version``
+
+  DEPRECATED in 0.8; use version instead.
+```
+
+```{eval-rst}
+.. confval:: ninja.version
+  :type: ``SpecifierSet``
+  :default: ">=1.5"
+
+  The versions of Ninja to allow. If Ninja is not present on the system or does not pass this specifier, it will be downloaded via PyPI if possible. An empty string will disable this check.
+```
+
+## sdist
+
+```{eval-rst}
+.. confval:: sdist.cmake
+  :type: ``bool``
+  :default: false
+
+  If set to True, CMake will be run before building the SDist.
+```
+
+```{eval-rst}
+.. confval:: sdist.exclude
+  :type: ``list[str]``
+
+  Files to exclude from the SDist even if they are included by default. Supports gitignore syntax.
+```
+
+```{eval-rst}
+.. confval:: sdist.include
+  :type: ``list[str]``
+
+  Files to include in the SDist even if they are skipped by default. Supports gitignore syntax.
+```
+
+```{eval-rst}
+.. confval:: sdist.reproducible
+  :type: ``bool``
+  :default: true
+
+  If set to True, try to build a reproducible distribution (Unix and Python 3.9+ recommended).  ``SOURCE_DATE_EPOCH`` will be used for timestamps, or a fixed value if not set.
+```
+
+## search
+
+```{eval-rst}
+.. confval:: search.site-packages
+  :type: ``bool``
+  :default: true
+
+  Add the python build environment site_packages folder to the CMake prefix paths.
+```
+
+## wheel
+
+```{eval-rst}
+.. confval:: wheel.build-tag
+  :type: ``str``
+
+  The build tag to use for the wheel. If empty, no build tag is used.
+```
+
+```{eval-rst}
+.. confval:: wheel.cmake
+  :type: ``bool``
+  :default: true
+
+  If set to True (the default), CMake will be run before building the wheel.
+```
+
+```{eval-rst}
+.. confval:: wheel.exclude
+  :type: ``list[str]``
+
+  A set of patterns to exclude from the wheel. This is additive to the SDist exclude patterns. This applies to the final paths in the wheel, and can exclude files from CMake output as well.  Editable installs may not respect this exclusion.
+```
+
+```{eval-rst}
+.. confval:: wheel.expand-macos-universal-tags
+  :type: ``bool``
+  :default: false
+
+  Fill out extra tags that are not required. This adds "x86_64" and "arm64" to the list of platforms when "universal2" is used, which helps older Pip's (before 21.0.1) find the correct wheel.
+```
+
+```{eval-rst}
+.. confval:: wheel.install-dir
+  :type: ``str``
+
+  The install directory for the wheel. This is relative to the platlib root. You might set this to the package name. The original dir is still at SKBUILD_PLATLIB_DIR (also SKBUILD_DATA_DIR, etc. are available). EXPERIMENTAL: An absolute path will be one level higher than the platlib root, giving access to "/platlib", "/data", "/headers", and "/scripts".
+```
+
+```{eval-rst}
+.. confval:: wheel.license-files
+  :type: ``list[str]``
+
+  A list of license files to include in the wheel. Supports glob patterns. The default is ``["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]``. Must not be set if ``project.license-files`` is set.
+```
+
+```{eval-rst}
+.. confval:: wheel.packages
+  :type: ``list[str]``
+  :default: ["src/<package>", "python/<package>", "<package>"]
+
+  A list of packages to auto-copy into the wheel. If this is not set, it will default to the first of ``src/<package>``, ``python/<package>``, or ``<package>`` if they exist.  The prefix(s) will be stripped from the package name inside the wheel. If a dict, provides a mapping of package name to source directory.
+```
+
+```{eval-rst}
+.. confval:: wheel.platlib
+  :type: ``bool``
+
+  Target the platlib or the purelib. If not set, the default is to target the platlib if wheel.cmake is true, and the purelib otherwise.
+```
+
+```{eval-rst}
+.. confval:: wheel.py-api
+  :type: ``str``
+
+  The Python tags. The default (empty string) will use the default Python version. You can also set this to "cp38" to enable the CPython 3.8+ Stable ABI / Limited API (only on CPython and if the version is sufficient, otherwise this has no effect). Or you can set it to "py3" or "py2.py3" to ignore Python ABI compatibility. The ABI tag is inferred from this tag.
+```
+
+<!-- [[[end]]] -->
diff --git a/noxfile.py b/noxfile.py
index 49bfb5d..8d48d72 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -112,6 +112,18 @@ def readme(session: nox.Session) -> None:
     session.run("cog", "-P", *args, "README.md")
 
 
+@nox.session(reuse_venv=True, tags=["gen"])
+def generate_config_reference(session: nox.Session) -> None:
+    """
+    Update the configure reference with cog. Pass --check to check instead.
+    """
+
+    args = session.posargs or ["-r"]
+
+    session.install("-e.", "cogapp")
+    session.run("cog", "-P", *args, f"{DIR / 'docs/reference/configs.md'}")
+
+
 @nox.session(venv_backend="uv", default=False)
 def minimums(session: nox.Session) -> None:
     """
diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py
index 4ec51f4..1319fd5 100644
--- a/src/scikit_build_core/metadata/__init__.py
+++ b/src/scikit_build_core/metadata/__init__.py
@@ -32,6 +32,13 @@ _LIST_STR_FIELDS = frozenset(
 _DICT_STR_FIELDS = frozenset(
     [
         "urls",
+        "scripts",
+        "gui-scripts",
+    ]
+)
+
+_LIST_DICT_FIELDS = frozenset(
+    [
         "authors",
         "maintainers",
     ]
@@ -42,20 +49,25 @@ _ALL_FIELDS = (
     _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/src/scikit_build_core/settings/documentation.py b/src/scikit_build_core/settings/documentation.py
index 06dadb3..b3320d4 100644
--- a/src/scikit_build_core/settings/documentation.py
+++ b/src/scikit_build_core/settings/documentation.py
@@ -11,7 +11,7 @@ from packaging.specifiers import SpecifierSet
 from packaging.version import Version
 
 from .. import __version__
-from .._compat.typing import get_args, get_origin
+from .._compat.typing import Annotated, get_args, get_origin
 
 if typing.TYPE_CHECKING:
     from collections.abc import Generator
@@ -53,6 +53,7 @@ def pull_docs(dc: type[object]) -> dict[str, str]:
 @dataclasses.dataclass(frozen=True)
 class DCDoc:
     name: str
+    type: str
     default: str
     docs: str
     deprecated: bool = False
@@ -66,6 +67,36 @@ def sanitize_default_field(text: str) -> str:
     return text.replace("'", '"').replace("True", "true").replace("False", "false")
 
 
+def is_optional(field: type) -> bool:
+    return get_origin(field) is typing.Union and type(None) in get_args(field)
+
+
+def get_display_type(field_type: type | str) -> str:
+    if isinstance(field_type, str):
+        return field_type
+    if is_optional(field_type):
+        # Special case for optional, we just take the first part
+        return get_display_type(get_args(field_type)[0])
+    # Handle built-ins
+    if get_origin(field_type) is dict:
+        key_display = get_display_type(get_args(field_type)[0])
+        val_display = get_display_type(get_args(field_type)[1])
+        return f"dict[{key_display},{val_display}]"
+    if get_origin(field_type) is list:
+        return f"list[{get_display_type(get_args(field_type)[0])}]"
+    # Handle other typing specials
+    if get_origin(field_type) is typing.Literal:
+        return " | ".join(f'"{x}"' for x in get_args(field_type))
+    if get_origin(field_type) is Annotated:
+        # For annotated assume we always want the second item
+        return get_display_type(get_args(field_type)[1])
+    if field_type is typing.Any:  # type: ignore[comparison-overlap]
+        # Workaround for python<3.10 where typing.Any.__name__ does not evaluate
+        return "Any"
+    # Otherwise just get the formatted form of the `type` object
+    return field_type.__name__
+
+
 def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]:
     """
     Makes documentation for a dataclass.
@@ -103,6 +134,7 @@ def mk_docs(dc: type[object], prefix: str = "") -> Generator[DCDoc, None, None]:
 
         yield DCDoc(
             name=f"{prefix}{field.name}".replace("_", "-"),
+            type=field.metadata.get("display_type", get_display_type(field.type)),
             default=sanitize_default_field(default),
             docs=docs[field.name],
             deprecated=field.metadata.get("deprecated", False),
diff --git a/src/scikit_build_core/settings/skbuild_docs.py b/src/scikit_build_core/settings/skbuild_docs_readme.py
similarity index 100%
rename from src/scikit_build_core/settings/skbuild_docs.py
rename to src/scikit_build_core/settings/skbuild_docs_readme.py
diff --git a/src/scikit_build_core/settings/skbuild_docs_sphinx.py b/src/scikit_build_core/settings/skbuild_docs_sphinx.py
new file mode 100644
index 0000000..2bd52b8
--- /dev/null
+++ b/src/scikit_build_core/settings/skbuild_docs_sphinx.py
@@ -0,0 +1,97 @@
+"""
+Make documentation for the skbuild model in sphinx format.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import textwrap
+import typing
+from collections import OrderedDict
+
+from .documentation import mk_docs
+from .skbuild_model import ScikitBuildSettings
+
+if typing.TYPE_CHECKING:
+    from .documentation import DCDoc
+
+__all__ = ["mk_skbuild_docs"]
+
+
+def __dir__() -> list[str]:
+    return __all__
+
+
+@dataclasses.dataclass
+class Document:
+    sections: dict[str, Section] = dataclasses.field(
+        default_factory=OrderedDict, init=False
+    )
+
+    def format(self) -> str:
+        return "\n".join(self.sections[it].format() for it in sorted(self.sections))
+
+
+@dataclasses.dataclass
+class Section:
+    TEMPLATE: typing.ClassVar[str] = textwrap.dedent("""\
+    {section_level} {section}
+
+    {content}""")
+    name: str
+    level: int = 2
+    items: dict[str, Item] = dataclasses.field(default_factory=OrderedDict, init=False)
+
+    def format(self) -> str:
+        return self.TEMPLATE.format(
+            section=self.name,
+            section_level="#" * self.level,
+            content="\n".join(self.items[it].format() for it in sorted(self.items)),
+        )
+
+
+@dataclasses.dataclass
+class Item:
+    TEMPLATE: typing.ClassVar[str] = textwrap.dedent("""\
+    ```{{eval-rst}}
+    .. confval:: {item.name}
+      :type: ``{item.type}``{sphinx_default}
+
+      {item.docs}
+    ```
+    """)
+    item: DCDoc
+
+    def ignore_default(self) -> bool:
+        """
+        Check if we want to ignore displaying the default
+        """
+        return self.item.default in ('""', "[]", "{}")
+
+    def default(self) -> str:
+        """
+        Formatted text that includes the `:default:` key or not
+        """
+        if self.ignore_default():
+            return ""
+        return f"\n  :default: {self.item.default}"
+
+    def format(self) -> str:
+        return self.TEMPLATE.format(
+            item=self.item,
+            sphinx_default=self.default(),
+        )
+
+
+def mk_skbuild_docs() -> str:
+    doc = Document()
+    for doc_item in mk_docs(ScikitBuildSettings):
+        name_split = doc_item.name.rsplit(".", maxsplit=1)
+        section_name = f"{name_split[0]}" if len(name_split) > 1 else "(top-level)"
+        section = doc.sections.setdefault(section_name, Section(section_name))
+        section.items[doc_item.name] = Item(doc_item)
+    return doc.format()
+
+
+if __name__ == "__main__":
+    print(mk_skbuild_docs())  # noqa: T201


Test patch:
diff --git a/tests/test_dynamic_metadata_unit.py b/tests/test_dynamic_metadata_unit.py
index 8d6ea36..2b45bfd 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
diff --git a/tests/test_settings_docs.py b/tests/test_settings_docs.py
index bc2a743..7d144f4 100644
--- a/tests/test_settings_docs.py
+++ b/tests/test_settings_docs.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from scikit_build_core.settings.documentation import DCDoc, mk_docs
-from scikit_build_core.settings.skbuild_docs import mk_skbuild_docs
+from scikit_build_core.settings.skbuild_docs_readme import mk_skbuild_docs
 from scikit_build_core.settings.skbuild_model import ScikitBuildSettings
 
 
@@ -21,6 +21,7 @@ def test_mk_docs() -> None:
     assert (
         DCDoc(
             name="cmake.minimum-version",
+            type="Version",
             default='""',
             docs="DEPRECATED in 0.8; use version instead.",
             deprecated=True,
@@ -30,6 +31,7 @@ def test_mk_docs() -> None:
     assert (
         DCDoc(
             name="install.strip",
+            type="bool",
             default="true",
             docs="Whether to strip the binaries. True for release builds on scikit-build-core 0.5+ (0.5-0.10.5 also incorrectly set this for debug builds).",
             deprecated=False,