Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ between ``setuptools-git-versioning`` and other tools.

- Only Git v2 is supported

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

- Currently does not support automatic exporting of package version to a file for runtime use
(but you can use ``setuptools-git-versioning > file`` redirect instead)
Expand Down Expand Up @@ -142,3 +143,31 @@ and then add new argument ``setuptools_git_versioning`` with config options:
)

Commands are the same as above, plus ``python -m setup.py`` returns the same version.

``scikit-build-core``
~~~~~~~~~~~~~~~~~~~~~

If your project uses the `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ build backend,
add ``setuptools-git-versioning`` to ``build-system.requires``,
mark the ``version`` field as dynamic,
register the dynamic-metadata provider,
and configure options under the usual ``[tool.setuptools-git-versioning]`` section:

.. code:: toml

[build-system]
requires = [ "scikit-build-core", "setuptools-git-versioning>=3.0,<4", ]
build-backend = "scikit_build_core.build"

[project]
name = "mypackage"
dynamic = ["version"]

[tool.scikit-build.metadata.version]
provider = "setuptools_git_versioning.scikit_metadata"

[tool.setuptools-git-versioning]
enabled = true

All options under ``[tool.setuptools-git-versioning]`` work exactly as the setuptools backend.
Inline configuration under ``[tool.scikit-build.metadata.version]`` is not supported.
1 change: 1 addition & 0 deletions docs/changelog/next_release/130.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a dynamic-metadata provider compatible with `scikit-build-core <https://scikit-build-core.readthedocs.io>`_.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ test = [
# Some tests need to write TOML, but tomli and tomllib are read-only, and toml package is unmaintained
"tomli-w>=1.0.0",
"wheel>=0.42.0",
"scikit-build-core>=0.5",
Comment thread
dolfinus marked this conversation as resolved.
]
docs = [
"furo~=2025.12.19",
Expand Down
81 changes: 81 additions & 0 deletions setuptools_git_versioning/scikit_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Dynamic metadata provider for the scikit-build-core build backend."""
Comment thread
ktbarrett marked this conversation as resolved.

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any

from setuptools_git_versioning.defaults import set_default_options
from setuptools_git_versioning.setup import read_toml
from setuptools_git_versioning.version import version_from_git

if TYPE_CHECKING:
from collections.abc import Mapping

__all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]


def dynamic_metadata(
field: str,
settings: Mapping[str, Any] | None = None,
) -> str:
if field != "version":
msg = f"Only the 'version' field is supported, got {field!r}"
raise ValueError(msg)

if settings:
msg = (
"Inline configuration under [tool.scikit-build.metadata.version] is not supported. "
"Configure setuptools-git-versioning under [tool.setuptools-git-versioning] instead."
)
raise ValueError(msg)

root = Path.cwd()

config = read_toml(root=root)
if not config:
msg = (
"Missing [tool.setuptools-git-versioning] section in pyproject.toml. "
"Add it (with at minimum 'enabled = true') to use this provider."
)
raise ValueError(msg)

if not config.pop("enabled", True):
msg = (
"[tool.setuptools-git-versioning] has 'enabled = false' but the scikit-build-core "
"metadata provider for setuptools-git-versioning was selected. "
"Either remove the provider or set 'enabled = true'."
)
raise ValueError(msg)

set_default_options(config)

package_name = _read_project_name(root)

return str(version_from_git(package_name, **config, root=root))


def _read_project_name(root: Path) -> str | None:
pyproject = root / "pyproject.toml"
if not pyproject.is_file():
return None

try:
import tomllib

with pyproject.open("rb") as file:
data = tomllib.load(file)
except ImportError:
import tomli

with pyproject.open("rb") as file:
data = tomli.load(file)

name = data.get("project", {}).get("name")
return name if isinstance(name, str) else None


def get_requires_for_dynamic_metadata(
_settings: Mapping[str, Any] | None = None,
) -> list[str]:
return ["setuptools-git-versioning"]
175 changes: 175 additions & 0 deletions tests/test_integration/test_scikit_build_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from __future__ import annotations

import re
import sys
import textwrap
from typing import TYPE_CHECKING, Any

import pytest
import tomli_w

from setuptools_git_versioning import scikit_metadata as metadata
from tests.lib.util import create_file, create_tag, execute

if TYPE_CHECKING:
from pathlib import Path

pytestmark = [pytest.mark.all, pytest.mark.important]


def write_config(repo: Path, config: dict[str, Any] | None) -> None:
"""Write a pyproject.toml with only the [tool.setuptools-git-versioning] section we need."""
cfg: dict[str, Any] = {"project": {"name": "mypkg", "dynamic": ["version"]}}
if config is not None:
cfg["tool"] = {"setuptools-git-versioning": config}
create_file(repo, "pyproject.toml", tomli_w.dumps(cfg))


def test_untagged_repo_returns_starting_version(repo, monkeypatch):
write_config(repo, {"enabled": True})
monkeypatch.chdir(repo)

assert metadata.dynamic_metadata("version") == "0.0.1"


def test_tagged_repo_returns_tag(repo, monkeypatch):
write_config(repo, {"enabled": True})
create_tag(repo, "1.2.3")
monkeypatch.chdir(repo)

assert metadata.dynamic_metadata("version") == "1.2.3"


def test_dev_template_used_after_tag(repo, monkeypatch):
write_config(repo, {"enabled": True})
create_tag(repo, "1.2.3")
create_file(repo, "extra.txt", "extra")
monkeypatch.chdir(repo)

result = metadata.dynamic_metadata("version")
assert re.fullmatch(r"1\.2\.3\.post1\+git\.[0-9a-f]{8}", result), result


def test_reads_project_name_from_pyproject(repo, monkeypatch):
write_config(repo, {"enabled": True})
monkeypatch.chdir(repo)

captured: dict[str, Any] = {}

def fake_version_from_git(package_name=None, **_kwargs):
from packaging.version import Version

captured["package_name"] = package_name
return Version("9.9.9")

monkeypatch.setattr(metadata, "version_from_git", fake_version_from_git)

assert metadata.dynamic_metadata("version") == "9.9.9"
assert captured["package_name"] == "mypkg"


def test_no_project_section_means_no_package_name(repo, monkeypatch):
create_file(
repo,
"pyproject.toml",
tomli_w.dumps({"tool": {"setuptools-git-versioning": {"enabled": True}}}),
)
monkeypatch.chdir(repo)

captured: dict[str, Any] = {}

def fake_version_from_git(package_name=None, **_kwargs):
from packaging.version import Version

captured["package_name"] = package_name
return Version("0.0.1")

monkeypatch.setattr(metadata, "version_from_git", fake_version_from_git)

metadata.dynamic_metadata("version")
assert captured["package_name"] is None


def test_rejects_non_version_field(repo, monkeypatch):
write_config(repo, {"enabled": True})
monkeypatch.chdir(repo)

with pytest.raises(ValueError, match="Only the 'version' field"):
metadata.dynamic_metadata("description")


def test_rejects_inline_settings(repo, monkeypatch):
write_config(repo, {"enabled": True})
monkeypatch.chdir(repo)

with pytest.raises(ValueError, match="Inline configuration"):
metadata.dynamic_metadata("version", {"template": "{tag}"})


def test_rejects_missing_section(repo, monkeypatch):
# pyproject.toml exists but has no [tool.setuptools-git-versioning] section
create_file(repo, "pyproject.toml", textwrap.dedent('[project]\nname = "mypkg"\n'))
monkeypatch.chdir(repo)

with pytest.raises(ValueError, match=r"Missing \[tool\.setuptools-git-versioning\]"):
metadata.dynamic_metadata("version")


def test_rejects_enabled_false(repo, monkeypatch):
write_config(repo, {"enabled": False})
monkeypatch.chdir(repo)

with pytest.raises(ValueError, match="enabled = false"):
metadata.dynamic_metadata("version")


def test_get_requires_for_dynamic_metadata():
assert metadata.get_requires_for_dynamic_metadata() == ["setuptools-git-versioning"]
assert metadata.get_requires_for_dynamic_metadata({"anything": True}) == ["setuptools-git-versioning"]


def test_end_to_end_build_via_scikit_build_core(repo):
"""Drive the actual scikit-build-core backend to verify the provider protocol matches."""

create_file(
repo,
"pyproject.toml",
tomli_w.dumps(
{
"build-system": {
"requires": ["scikit-build-core", "setuptools-git-versioning"],
"build-backend": "scikit_build_core.build",
},
"project": {"name": "mypkg", "dynamic": ["version"]},
"tool": {
"scikit-build": {
"experimental": True,
"metadata": {
"version": {"provider": "setuptools_git_versioning.scikit_metadata"},
},
},
"setuptools-git-versioning": {"enabled": True},
},
},
),
)
create_file(
repo,
"CMakeLists.txt",
textwrap.dedent(
"""
cmake_minimum_required(VERSION 3.15)
project(mypkg LANGUAGES NONE)
install(FILES mypkg/__init__.py DESTINATION mypkg)
""",
),
)
(repo / "mypkg").mkdir()
create_file(repo, "mypkg/__init__.py", "")
create_tag(repo, "1.2.3")

execute(repo, sys.executable, "-m", "build", "--sdist", "--no-isolation")

sdists = list((repo / "dist").glob("mypkg-*.tar.gz"))
assert len(sdists) == 1, sdists
assert sdists[0].name == "mypkg-1.2.3.tar.gz"
Loading
Loading