Skip to content

Commit 6540cf6

Browse files
bennyrowlandBen Rowlandhenryiii
authored
feat: support dynamic metadata (#197)
This is a first pass at a scheme for supporting dynamic metadata in a generic way. It keeps everything as simple as possible by using functions in modules with a very simple signature to provide each value. I am happy to discuss the proposed interface and make changes, but thought it would be best to get an implementation out there to have something concrete to discuss. Scikit-build-core's settings have been extended to have an additional metadata field which is just an optional list mapping string keys to string values. For each entry in this list, if the key is also included in the project.dynamic list, scikit-build-core will look up an a module with a function. This should be a function that accepts the parsed pyproject.toml (this seemed like a minimum but the final chosen signature is very much up for debate, for example, maybe the config settings dict should also be passed) and returns the dict that should be merged into the pyproject["project"] entry in the loaded toml config. Also included are a couple of core "plugins" which provide entry-points for setuptools_scm and hatch-fancy-pypi-readme, essentially just adapters as neither tool provides an explicit function matching the proposed entry-point signature. For tests, I have created a fixture which mocks the entrypoints list with fake functions to give more direct control over what values we want to test. I have duplicated most of the "simplest_c" test package to allow the complete build process to be applied, but it is also possible to test with only a pyproject.toml file, and in fact most of the tests I have currently implemented do only use the pyproject.toml file and stop at testing the calculated metadata, although this can of course be changed. There is also a simple test for the setuptools_scm and hatch-fancy-pypi-readme plugins to make sure they are working correctly. Adding setuptools_scm as a test dependency to test the plugin had the unexpected effect of making setuptools backend tests fail because of the file finder hook setuptools_scm provides which includes unexpected files (because they are under source control) in the sdist. To solve this, I have modified those tests to copy the package files to a temporary location before building so that the build source is not under git. --------- Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Ben Rowland <[email protected]> Co-authored-by: Henry Schreiner <[email protected]>
1 parent 4f25e3a commit 6540cf6

25 files changed

+524
-29
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ jobs:
8181
python-version: ${{ matrix.python-version }}
8282

8383
- name: Install package
84-
run: pip install .[test,cov] cmake ninja rich
84+
run:
85+
pip install .[test,cov] cmake ninja rich hatch-fancy-pypi-readme
86+
setuptools-scm
8587

8688
- name: Test package
8789
run: pytest -ra --showlocals --cov=scikit_build_core
@@ -91,9 +93,11 @@ jobs:
9193
with:
9294
name: ${{ runner.os }}-${{ matrix.python-version }}
9395

96+
# the min requirements are not compatible with the metadata plugin
97+
# packages so we remove those first (they then won't be tested)
9498
- name: Min requirements
9599
run: |
96-
pip uninstall -y cmake
100+
pip uninstall -y cmake hatch-fancy-pypi-readme setuptools-scm
97101
pip install --constraint tests/constraints.txt .[test]
98102
99103
- name: Setup CMake ${{ matrix.cmake-version }}

.pre-commit-config.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,29 @@ repos:
6363
rev: v1.1.1
6464
hooks:
6565
- id: mypy
66-
exclude: tests/packages/(simplest_c/src/simplest/__init__.py|.*/setup.py)
66+
exclude: |
67+
(?x)^(
68+
tests/packages/simplest_c/src/simplest/__init__.py|
69+
tests/packages/dynamic_metadata/src/dynamic/__init__.py|
70+
tests/packages/.*/setup.py
71+
)
6772
files: ^(src|tests)
6873
args: []
6974
additional_dependencies:
7075
- build
7176
- cattrs
7277
- cmake
7378
- exceptiongroup
74-
- importlib_metadata
79+
- gitpython
80+
- hatch-fancy-pypi-readme
81+
- importlib-metadata
7582
- importlib_resources
7683
- ninja
7784
- packaging
7885
- pyproject_metadata
7986
- pytest
8087
- rich
88+
- setuptools-scm
8189
- tomli
8290
- types-setuptools
8391
- typing_extensions >=4; python_version<'3.11'

noxfile.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ def pylint(session: nox.Session) -> None:
2727
"""
2828
# This needs to be installed into the package environment, and is slower
2929
# than a pre-commit check
30-
session.install("-e.[dev,test]", "pylint")
30+
session.install(
31+
"-e.[dev,test]", "pylint", "hatch-fancy-pypi-readme", "setuptools-scm"
32+
)
3133
session.run("pylint", "scikit_build_core", *session.posargs)
3234

3335

@@ -37,7 +39,7 @@ def tests(session: nox.Session) -> None:
3739
Run the unit and regular tests.
3840
"""
3941
env = {"PIP_DISABLE_PIP_VERSION_CHECK": "1"}
40-
extra = ["rich"]
42+
extra = ["hatch-fancy-pypi-readme", "rich", "setuptools-scm"]
4143
# This will not work if system CMake is too old (<3.15)
4244
if shutil.which("cmake") is None and shutil.which("cmake3") is None:
4345
extra.append("cmake")

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ rich = [
5353
test = [
5454
"build[virtualenv]",
5555
"cattrs >=22.2.0",
56+
"gitpython",
5657
"importlib_metadata; python_version<'3.8'",
5758
"pathspec >=0.10.1",
5859
"pybind11",
@@ -129,7 +130,7 @@ module = ["scikit_build_core.*"]
129130
disallow_untyped_defs = true
130131

131132
[[tool.mypy.overrides]]
132-
module = ["pathspec"]
133+
module = ["pathspec", "setuptools_scm", "hatch_fancy_pypi_readme"]
133134
ignore_missing_imports = true
134135

135136

@@ -197,6 +198,7 @@ extend-ignore = [
197198
"PLR2004",
198199
"PLE1205", # Format check doesn't work with our custom logger
199200
"E501", # Line too long
201+
"PT004",
200202
]
201203
target-version = "py37"
202204
typing-modules = ["scikit_build_core._compat.typing"]

src/scikit_build_core/build/sdist.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
import tarfile
99
from pathlib import Path
1010

11-
from pyproject_metadata import StandardMetadata
12-
1311
from .._compat import tomllib
12+
from ..settings.metadata import get_standard_metadata
1413
from ..settings.skbuild_read_settings import SettingsReader
1514
from ._file_processor import each_unignored_file
1615
from ._init import setup_logging
@@ -69,23 +68,23 @@ def build_sdist(
6968
sdist_directory: str,
7069
config_settings: dict[str, list[str] | str] | None = None,
7170
) -> str:
72-
settings_reader = SettingsReader.from_file("pyproject.toml", config_settings)
71+
with Path("pyproject.toml").open("rb") as f:
72+
pyproject = tomllib.load(f)
73+
74+
settings_reader = SettingsReader(pyproject, config_settings or {})
7375
settings = settings_reader.settings
7476
setup_logging(settings.logging.level)
7577

7678
settings_reader.validate_may_exit()
7779

7880
sdist_dir = Path(sdist_directory)
7981

80-
with Path("pyproject.toml").open("rb") as f:
81-
pyproject = tomllib.load(f)
82-
8382
reproducible = settings.sdist.reproducible
8483
timestamp = get_reproducible_epoch() if reproducible else None
8584

85+
metadata = get_standard_metadata(pyproject, settings)
8686
# Using deepcopy here because of a bug in pyproject-metadata
8787
# https://github.com/FFY00/python-pyproject-metadata/pull/49
88-
metadata = StandardMetadata.from_pyproject(pyproject)
8988
pkg_info = bytes(copy.deepcopy(metadata).as_rfc822())
9089

9190
srcdirname = f"{metadata.name}-{metadata.version}"

src/scikit_build_core/build/wheel.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
from pathlib import Path
1010

1111
from packaging.version import Version
12-
from pyproject_metadata import StandardMetadata
1312

1413
from .. import __version__
1514
from .._compat import tomllib
1615
from .._logging import logger, rich_print
1716
from ..builder.builder import Builder, archs_to_tags, get_archs
1817
from ..builder.wheel_tag import WheelTag
1918
from ..cmake import CMake, CMaker
19+
from ..settings.metadata import get_standard_metadata
2020
from ..settings.skbuild_read_settings import SettingsReader
2121
from ._init import setup_logging
2222
from ._pathutil import packages_to_file_mapping
@@ -63,16 +63,17 @@ def _build_wheel_impl(
6363
"""
6464
Build a wheel or just prepare metadata (if wheel dir is None).
6565
"""
66+
pyproject_path = Path("pyproject.toml")
67+
with pyproject_path.open("rb") as ft:
68+
pyproject = tomllib.load(ft)
6669

67-
settings_reader = SettingsReader.from_file("pyproject.toml", config_settings)
70+
settings_reader = SettingsReader(pyproject, config_settings or {})
6871
settings = settings_reader.settings
6972
setup_logging(settings.logging.level)
7073

7174
settings_reader.validate_may_exit()
7275

73-
with Path("pyproject.toml").open("rb") as ft:
74-
pyproject = tomllib.load(ft)
75-
metadata = StandardMetadata.from_pyproject(pyproject)
76+
metadata = get_standard_metadata(pyproject, settings, config_settings)
7677

7778
if metadata.version is None:
7879
msg = "project.version is not statically specified, must be present currently"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from __future__ import annotations
2+
3+
__all__: list[str] = []
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
__all__ = ["dynamic_metadata"]
6+
7+
8+
def __dir__() -> list[str]:
9+
return __all__
10+
11+
12+
def dynamic_metadata(
13+
pyproject_dict: dict[str, Any],
14+
_config_settings: dict[str, list[str] | str] | None = None,
15+
) -> dict[str, str | dict[str, str | None]]:
16+
from hatch_fancy_pypi_readme._builder import build_text
17+
from hatch_fancy_pypi_readme._config import load_and_validate_config
18+
19+
config = load_and_validate_config(
20+
pyproject_dict["tool"]["hatch"]["metadata"]["hooks"]["fancy-pypi-readme"]
21+
)
22+
23+
return {
24+
"readme": {
25+
"content-type": config.content_type,
26+
"text": build_text(config.fragments, config.substitutions),
27+
}
28+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from __future__ import annotations
2+
3+
__all__ = ["dynamic_metadata"]
4+
5+
6+
def __dir__() -> list[str]:
7+
return __all__
8+
9+
10+
def dynamic_metadata(
11+
pyproject_dict: dict[str, object], # noqa: ARG001
12+
_config_settings: dict[str, list[str] | str] | None = None,
13+
) -> dict[str, str | dict[str, str | None]]:
14+
# this is a classic implementation, waiting for the release of
15+
# vcs-versioning and an improved public interface
16+
from setuptools_scm import Configuration, _get_version
17+
18+
config = Configuration.from_file("pyproject.toml")
19+
version: str = _get_version(config)
20+
21+
return {"version": version}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
from typing import Any
5+
6+
from pyproject_metadata import StandardMetadata
7+
8+
from ..settings.skbuild_model import ScikitBuildSettings
9+
10+
__all__ = ["get_standard_metadata"]
11+
12+
13+
def __dir__() -> list[str]:
14+
return __all__
15+
16+
17+
def _load(
18+
mod_name: str,
19+
pyproject_dict: dict[str, Any],
20+
config_settings: dict[str, list[str] | str] | None = None,
21+
) -> dict[str, Any]:
22+
return importlib.import_module(mod_name).dynamic_metadata(pyproject_dict, config_settings) # type: ignore[no-any-return]
23+
24+
25+
# If pyproject-metadata eventually supports updates, this can be simplified
26+
def get_standard_metadata(
27+
pyproject_dict: dict[str, Any],
28+
settings: ScikitBuildSettings,
29+
config_settings: dict[str, list[str] | str] | None = None,
30+
) -> StandardMetadata:
31+
# Handle any dynamic metadata
32+
for field in settings.metadata:
33+
if field not in pyproject_dict.get("project", {}).get("dynamic", []):
34+
msg = f"{field} is not in project.dynamic"
35+
raise KeyError(msg)
36+
37+
plugins = set(settings.metadata.values())
38+
cached_plugins = {
39+
key: _load(key, pyproject_dict, config_settings) for key in plugins
40+
}
41+
42+
for field, mod_name in settings.metadata.items():
43+
if field not in cached_plugins[mod_name]:
44+
msg = f"{field} is not provided by plugin {mod_name}"
45+
raise KeyError(msg)
46+
47+
pyproject_dict["project"][field] = cached_plugins[mod_name][field]
48+
pyproject_dict["project"]["dynamic"].remove(field)
49+
50+
return StandardMetadata.from_pyproject(pyproject_dict)

src/scikit_build_core/settings/skbuild_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ class ScikitBuildSettings:
110110
sdist: SDistSettings = dataclasses.field(default_factory=SDistSettings)
111111
wheel: WheelSettings = dataclasses.field(default_factory=WheelSettings)
112112
backport: BackportSettings = dataclasses.field(default_factory=BackportSettings)
113+
metadata: Dict[str, str] = dataclasses.field(default_factory=dict)
113114

114115
#: Strictly check all config options. If False, warnings will be
115116
#: printed for unknown options. If True, an error will be raised.

tests/constraints.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ cattrs==22.2.0
33
pytest==7.0.0
44
tomli==1.1.0
55
packaging==20.9
6+
importlib-metadata==4.13.0
67
importlib-resources==1.3.0
7-
pyproject-metadata==0.5.0
8+
pyproject-metadata==0.6.1
89
pathspec==0.10.1
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
cmake_minimum_required(VERSION 3.15...3.25)
2+
3+
project(
4+
${SKBUILD_PROJECT_NAME}
5+
LANGUAGES C
6+
VERSION ${SKBUILD_PROJECT_VERSION})
7+
8+
find_package(Python COMPONENTS Interpreter Development.Module)
9+
set(Python_SOABI ${SKBUILD_SOABI})
10+
11+
python_add_library(_module MODULE src/module.c WITH_SOABI)
12+
13+
install(TARGETS _module DESTINATION ${SKBUILD_PROJECT_NAME})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "fancy"
7+
dynamic = ["version", "license"]
8+
9+
[tool.scikit-build.metadata]
10+
version = "test_dual"
11+
license = "test_dual"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "fancy"
7+
dynamic = ["version", "readme", "license"]
8+
9+
[tool.scikit-build.metadata]
10+
version = "test_dual"
11+
license = "test_dual"
12+
readme = "test_dual"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "fancy"
7+
version = "0.0.1"
8+
9+
[tool.scikit-build.metadata]
10+
readme = "scikit_build_core.metadata.fancy_pypi_readme"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "fancy"
7+
dynamic = ["readme", "version"]
8+
9+
[tool.scikit-build.metadata]
10+
version = "scikit_build_core.metadata.setuptools_scm"
11+
readme = "scikit_build_core.metadata.fancy_pypi_readme"
12+
13+
[tool.setuptools_scm]
14+
15+
[tool.hatch.metadata.hooks.fancy-pypi-readme]
16+
content-type = "text/x-rst"
17+
18+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
19+
text = "Fragment #1"
20+
21+
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
22+
text = "Fragment #2"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[build-system]
2+
requires = ["scikit-build-core"]
3+
build-backend = "scikit_build_core.build"
4+
5+
[project]
6+
name = "dynamic"
7+
dynamic = ["version", "readme", "license"]
8+
9+
[tool.scikit-build.metadata]
10+
version = "test_version"
11+
readme = "test_readme"
12+
license = "test_license"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from ._module import square
2+
3+
__all__ = ["square"]

0 commit comments

Comments
 (0)