Skip to content

Commit 3acaa54

Browse files
committed
Add dynamic version metadata provider for compatibility with scikit-build-core
This creates a dynamic version metadata provider, by reusing interfaces already present in this tool, to provide a version for the scikit-build-core build backend. This allows this library to be used with scikit-build-core.
1 parent 929e0d6 commit 3acaa54

6 files changed

Lines changed: 407 additions & 1 deletion

File tree

README.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ between ``setuptools-git-versioning`` and other tools.
5050

5151
- Only Git v2 is supported
5252

53-
- Only Setuptools build backend is supported (no Poetry & others)
53+
- Setuptools and `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ build backends are
54+
supported (no Poetry & others)
5455

5556
- Currently does not support automatic exporting of package version to a file for runtime use
5657
(but you can use ``setuptools-git-versioning > file`` redirect instead)
@@ -142,3 +143,31 @@ and then add new argument ``setuptools_git_versioning`` with config options:
142143
)
143144
144145
Commands are the same as above, plus ``python -m setup.py`` returns the same version.
146+
147+
``scikit-build-core``
148+
~~~~~~~~~~~~~~~~~~~~~
149+
150+
If your project uses the `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ build backend,
151+
add ``setuptools-git-versioning`` to ``build-system.requires``,
152+
mark the ``version`` field as dynamic,
153+
register the dynamic-metadata provider,
154+
and configure options under the usual ``[tool.setuptools-git-versioning]`` section:
155+
156+
.. code:: toml
157+
158+
[build-system]
159+
requires = [ "scikit-build-core", "setuptools-git-versioning>=3.0,<4", ]
160+
build-backend = "scikit_build_core.build"
161+
162+
[project]
163+
name = "mypackage"
164+
dynamic = ["version"]
165+
166+
[tool.scikit-build.metadata.version]
167+
provider = "setuptools_git_versioning.scikit_metadata"
168+
169+
[tool.setuptools-git-versioning]
170+
enabled = true
171+
172+
All options under ``[tool.setuptools-git-versioning]`` work exactly as the setuptools backend.
173+
Inline configuration under ``[tool.scikit-build.metadata.version]`` is not supported.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added a dynamic-metadata provider compatible with `scikit-build-core <https://scikit-build-core.readthedocs.io>`_.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ test = [
7878
# Some tests need to write TOML, but tomli and tomllib are read-only, and toml package is unmaintained
7979
"tomli-w>=1.0.0",
8080
"wheel>=0.42.0",
81+
"scikit-build-core>=0.5",
8182
]
8283
docs = [
8384
"furo~=2025.12.19",
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Dynamic metadata provider for the scikit-build-core build backend."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import TYPE_CHECKING, Any
7+
8+
from setuptools_git_versioning.defaults import set_default_options
9+
from setuptools_git_versioning.setup import read_toml
10+
from setuptools_git_versioning.version import version_from_git
11+
12+
if TYPE_CHECKING:
13+
from collections.abc import Mapping
14+
15+
__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]
16+
17+
18+
def dynamic_metadata(
19+
field: str,
20+
settings: Mapping[str, Any] | None = None,
21+
) -> str:
22+
if field != "version":
23+
msg = f"Only the 'version' field is supported, got {field!r}"
24+
raise ValueError(msg)
25+
26+
if settings:
27+
msg = (
28+
"Inline configuration under [tool.scikit-build.metadata.version] is not supported. "
29+
"Configure setuptools-git-versioning under [tool.setuptools-git-versioning] instead."
30+
)
31+
raise ValueError(msg)
32+
33+
root = Path.cwd()
34+
35+
config = read_toml(root=root)
36+
if not config:
37+
msg = (
38+
"Missing [tool.setuptools-git-versioning] section in pyproject.toml. "
39+
"Add it (with at minimum 'enabled = true') to use this provider."
40+
)
41+
raise ValueError(msg)
42+
43+
if not config.pop("enabled", True):
44+
msg = (
45+
"[tool.setuptools-git-versioning] has 'enabled = false' but the scikit-build-core "
46+
"metadata provider for setuptools-git-versioning was selected. "
47+
"Either remove the provider or set 'enabled = true'."
48+
)
49+
raise ValueError(msg)
50+
51+
set_default_options(config)
52+
53+
package_name = _read_project_name(root)
54+
55+
return str(version_from_git(package_name, **config, root=root))
56+
57+
58+
def _read_project_name(root: Path) -> str | None:
59+
pyproject = root / "pyproject.toml"
60+
if not pyproject.is_file():
61+
return None
62+
63+
try:
64+
import tomllib
65+
66+
with pyproject.open("rb") as file:
67+
data = tomllib.load(file)
68+
except ImportError:
69+
import tomli
70+
71+
with pyproject.open("rb") as file:
72+
data = tomli.load(file)
73+
74+
name = data.get("project", {}).get("name")
75+
return name if isinstance(name, str) else None
76+
77+
78+
def get_requires_for_dynamic_metadata(
79+
_settings: Mapping[str, Any] | None = None,
80+
) -> list[str]:
81+
return ["setuptools-git-versioning"]
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import sys
5+
import textwrap
6+
from typing import TYPE_CHECKING, Any
7+
8+
import pytest
9+
import tomli_w
10+
11+
from setuptools_git_versioning import scikit_metadata as metadata
12+
from tests.lib.util import create_file, create_tag, execute
13+
14+
if TYPE_CHECKING:
15+
from pathlib import Path
16+
17+
pytestmark = [pytest.mark.all, pytest.mark.important]
18+
19+
20+
def write_config(repo: Path, config: dict[str, Any] | None) -> None:
21+
"""Write a pyproject.toml with only the [tool.setuptools-git-versioning] section we need."""
22+
cfg: dict[str, Any] = {"project": {"name": "mypkg", "dynamic": ["version"]}}
23+
if config is not None:
24+
cfg["tool"] = {"setuptools-git-versioning": config}
25+
create_file(repo, "pyproject.toml", tomli_w.dumps(cfg))
26+
27+
28+
def test_untagged_repo_returns_starting_version(repo, monkeypatch):
29+
write_config(repo, {"enabled": True})
30+
monkeypatch.chdir(repo)
31+
32+
assert metadata.dynamic_metadata("version") == "0.0.1"
33+
34+
35+
def test_tagged_repo_returns_tag(repo, monkeypatch):
36+
write_config(repo, {"enabled": True})
37+
create_tag(repo, "1.2.3")
38+
monkeypatch.chdir(repo)
39+
40+
assert metadata.dynamic_metadata("version") == "1.2.3"
41+
42+
43+
def test_dev_template_used_after_tag(repo, monkeypatch):
44+
write_config(repo, {"enabled": True})
45+
create_tag(repo, "1.2.3")
46+
create_file(repo, "extra.txt", "extra")
47+
monkeypatch.chdir(repo)
48+
49+
result = metadata.dynamic_metadata("version")
50+
assert re.fullmatch(r"1\.2\.3\.post1\+git\.[0-9a-f]{8}", result), result
51+
52+
53+
def test_reads_project_name_from_pyproject(repo, monkeypatch):
54+
write_config(repo, {"enabled": True})
55+
monkeypatch.chdir(repo)
56+
57+
captured: dict[str, Any] = {}
58+
59+
def fake_version_from_git(package_name=None, **_kwargs):
60+
from packaging.version import Version
61+
62+
captured["package_name"] = package_name
63+
return Version("9.9.9")
64+
65+
monkeypatch.setattr(metadata, "version_from_git", fake_version_from_git)
66+
67+
assert metadata.dynamic_metadata("version") == "9.9.9"
68+
assert captured["package_name"] == "mypkg"
69+
70+
71+
def test_no_project_section_means_no_package_name(repo, monkeypatch):
72+
create_file(
73+
repo,
74+
"pyproject.toml",
75+
tomli_w.dumps({"tool": {"setuptools-git-versioning": {"enabled": True}}}),
76+
)
77+
monkeypatch.chdir(repo)
78+
79+
captured: dict[str, Any] = {}
80+
81+
def fake_version_from_git(package_name=None, **_kwargs):
82+
from packaging.version import Version
83+
84+
captured["package_name"] = package_name
85+
return Version("0.0.1")
86+
87+
monkeypatch.setattr(metadata, "version_from_git", fake_version_from_git)
88+
89+
metadata.dynamic_metadata("version")
90+
assert captured["package_name"] is None
91+
92+
93+
def test_rejects_non_version_field(repo, monkeypatch):
94+
write_config(repo, {"enabled": True})
95+
monkeypatch.chdir(repo)
96+
97+
with pytest.raises(ValueError, match="Only the 'version' field"):
98+
metadata.dynamic_metadata("description")
99+
100+
101+
def test_rejects_inline_settings(repo, monkeypatch):
102+
write_config(repo, {"enabled": True})
103+
monkeypatch.chdir(repo)
104+
105+
with pytest.raises(ValueError, match="Inline configuration"):
106+
metadata.dynamic_metadata("version", {"template": "{tag}"})
107+
108+
109+
def test_rejects_missing_section(repo, monkeypatch):
110+
# pyproject.toml exists but has no [tool.setuptools-git-versioning] section
111+
create_file(repo, "pyproject.toml", textwrap.dedent('[project]\nname = "mypkg"\n'))
112+
monkeypatch.chdir(repo)
113+
114+
with pytest.raises(ValueError, match=r"Missing \[tool\.setuptools-git-versioning\]"):
115+
metadata.dynamic_metadata("version")
116+
117+
118+
def test_rejects_enabled_false(repo, monkeypatch):
119+
write_config(repo, {"enabled": False})
120+
monkeypatch.chdir(repo)
121+
122+
with pytest.raises(ValueError, match="enabled = false"):
123+
metadata.dynamic_metadata("version")
124+
125+
126+
def test_get_requires_for_dynamic_metadata():
127+
assert metadata.get_requires_for_dynamic_metadata() == ["setuptools-git-versioning"]
128+
assert metadata.get_requires_for_dynamic_metadata({"anything": True}) == ["setuptools-git-versioning"]
129+
130+
131+
def test_end_to_end_build_via_scikit_build_core(repo):
132+
"""Drive the actual scikit-build-core backend to verify the provider protocol matches."""
133+
134+
create_file(
135+
repo,
136+
"pyproject.toml",
137+
tomli_w.dumps(
138+
{
139+
"build-system": {
140+
"requires": ["scikit-build-core", "setuptools-git-versioning"],
141+
"build-backend": "scikit_build_core.build",
142+
},
143+
"project": {"name": "mypkg", "dynamic": ["version"]},
144+
"tool": {
145+
"scikit-build": {
146+
"experimental": True,
147+
"metadata": {
148+
"version": {"provider": "setuptools_git_versioning.scikit_metadata"},
149+
},
150+
},
151+
"setuptools-git-versioning": {"enabled": True},
152+
},
153+
},
154+
),
155+
)
156+
create_file(
157+
repo,
158+
"CMakeLists.txt",
159+
textwrap.dedent(
160+
"""
161+
cmake_minimum_required(VERSION 3.15)
162+
project(mypkg LANGUAGES NONE)
163+
install(FILES mypkg/__init__.py DESTINATION mypkg)
164+
""",
165+
),
166+
)
167+
(repo / "mypkg").mkdir()
168+
create_file(repo, "mypkg/__init__.py", "")
169+
create_tag(repo, "1.2.3")
170+
171+
execute(repo, sys.executable, "-m", "build", "--sdist", "--no-isolation")
172+
173+
sdists = list((repo / "dist").glob("mypkg-*.tar.gz"))
174+
assert len(sdists) == 1, sdists
175+
assert sdists[0].name == "mypkg-1.2.3.tar.gz"

0 commit comments

Comments
 (0)