From 8aca79c47f3fc672a03da5102d94d3d7fbdc21fb Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 26 Sep 2026 05:24:22 -0400 Subject: [PATCH 1/2] Add deprecation category for towncrier --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2a6d108..eb89d67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,3 +176,8 @@ showcontent = true directory = "misc" name = "Misc" showcontent = true + +[[tool.towncrier.type]] +directory = "deprecation" +name = "Deprecation" +showcontent = true From 17e0309fb6edb951513f01acf2f4075c75c15b12 Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sat, 26 Sep 2026 05:10:44 -0400 Subject: [PATCH 2/2] Add new dynamic-metadata based dynamic version support The new API is based on the semi-standardized dynamic-metadata package which got some Python core dev blessings and now supports enough functionality for this project to supply version information dynamically via this new API to downstream packages. The old scikit-build-core-specific dynamic version provider has been deprecated. --- README.rst | 27 ++- .../next_release/161.deprecation.rst | 1 + docs/changelog/next_release/161.feature.rst | 1 + pyproject.toml | 4 + setuptools_git_versioning/dynamic_metadata.py | 51 ++++ setuptools_git_versioning/scikit_metadata.py | 39 +--- .../test_integration/test_dynamic_metadata.py | 221 ++++++++++++++++++ .../test_scikit_build_metadata.py | 16 +- uv.lock | 15 ++ 9 files changed, 331 insertions(+), 44 deletions(-) create mode 100644 docs/changelog/next_release/161.deprecation.rst create mode 100644 docs/changelog/next_release/161.feature.rst create mode 100644 setuptools_git_versioning/dynamic_metadata.py create mode 100644 tests/test_integration/test_dynamic_metadata.py diff --git a/README.rst b/README.rst index 629a6e0..a6aca0d 100644 --- a/README.rst +++ b/README.rst @@ -50,8 +50,8 @@ between ``setuptools-git-versioning`` and other tools. - Only Git v2 is supported -- Setuptools and `scikit-build-core `_ build backends are - supported (no Poetry & others) +- Setuptools and build backends supporting `dynamic-metadata `_ + (e.g. `scikit-build-core `_ 1.0+) are supported. - 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) @@ -144,30 +144,29 @@ 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`` -~~~~~~~~~~~~~~~~~~~~~ +``dynamic-metadata`` +~~~~~~~~~~~~~~~~~~~~ -If your project uses the `scikit-build-core `_ 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: +For build backends supporting `dynamic-metadata `_ 0.4+, +select the registered provider in ``[[tool.dynamic-metadata]]``. For example, with +`scikit-build-core `_ 1.0+: .. code:: toml [build-system] - requires = [ "scikit-build-core", "setuptools-git-versioning>=3.0,<4", ] + requires = [ "scikit-build-core>=1.0", "dynamic-metadata>=0.4", "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.dynamic-metadata]] + provider = "setuptools_git_versioning.dynamic_metadata" [tool.setuptools-git-versioning] enabled = true -All options under ``[tool.setuptools-git-versioning]`` work exactly as the for ``setuptools`` backend. -Inline configuration under ``[tool.scikit-build.metadata.version]`` is not supported. +The provider sets only ``version`` and reads options from ``[tool.setuptools-git-versioning]``. +Inline settings in ``[[tool.dynamic-metadata]]`` are not supported. +The provider itself has no runtime dependency on ``dynamic-metadata``. diff --git a/docs/changelog/next_release/161.deprecation.rst b/docs/changelog/next_release/161.deprecation.rst new file mode 100644 index 0000000..11bc8bb --- /dev/null +++ b/docs/changelog/next_release/161.deprecation.rst @@ -0,0 +1 @@ +Deprecated the ``setuptools_git_versioning.scikit_metadata`` provider. Migrate to ``[[tool.dynamic-metadata]]`` with ``provider = "setuptools_git_versioning.dynamic_metadata"`` on scikit-build-core 1.0+ or another compatible backend. diff --git a/docs/changelog/next_release/161.feature.rst b/docs/changelog/next_release/161.feature.rst new file mode 100644 index 0000000..f47defb --- /dev/null +++ b/docs/changelog/next_release/161.feature.rst @@ -0,0 +1 @@ +Added support for ``dynamic-metadata`` 0.4+ through the ``setuptools_git_versioning.dynamic_metadata`` provider. This enables automatic versioning with scikit-build-core 1.0+ and other compatible backends. diff --git a/pyproject.toml b/pyproject.toml index eb89d67..5a5ec6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,9 @@ setuptools_git_versioning = "setuptools_git_versioning:infer_version" [project.entry-points."console_scripts"] setuptools-git-versioning = "setuptools_git_versioning.cli:main" +[project.entry-points."dynamic_metadata.provider"] +"setuptools_git_versioning.dynamic_metadata" = "setuptools_git_versioning.dynamic_metadata" + [tool.setuptools.packages.find] include = ["setuptools_git_versioning*"] exclude = ["docs*", "tests*"] @@ -79,6 +82,7 @@ test = [ "tomli-w>=1.0.0", "wheel>=0.42.0", "scikit-build-core>=0.5", + "dynamic-metadata>=0.4; python_version >= '3.8'", ] docs = [ "furo~=2025.12.19", diff --git a/setuptools_git_versioning/dynamic_metadata.py b/setuptools_git_versioning/dynamic_metadata.py new file mode 100644 index 0000000..6b90678 --- /dev/null +++ b/setuptools_git_versioning/dynamic_metadata.py @@ -0,0 +1,51 @@ +"""Version provider for the dynamic-metadata protocol.""" + +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"] + + +def dynamic_metadata( + settings: Mapping[str, Any], + project: Mapping[str, Any], +) -> dict[str, str]: + if settings: + msg = ( + "Inline configuration under [[tool.dynamic-metadata]] 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 " + "metadata provider for setuptools-git-versioning was selected. " + "Either remove the provider or set 'enabled = true'." + ) + raise ValueError(msg) + + set_default_options(config) + + name = project.get("name") + package_name = name if isinstance(name, str) else None + return {"version": str(version_from_git(package_name, **config, root=root))} diff --git a/setuptools_git_versioning/scikit_metadata.py b/setuptools_git_versioning/scikit_metadata.py index 4b1a44a..2485e90 100644 --- a/setuptools_git_versioning/scikit_metadata.py +++ b/setuptools_git_versioning/scikit_metadata.py @@ -1,19 +1,26 @@ -"""Dynamic metadata provider for the scikit-build-core build backend.""" +"""Deprecated version provider for the classic scikit-build-core protocol.""" from __future__ import annotations +import warnings 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 +from setuptools_git_versioning.dynamic_metadata import dynamic_metadata as _dynamic_metadata if TYPE_CHECKING: from collections.abc import Mapping __all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"] +warnings.warn( + "setuptools_git_versioning.scikit_metadata is deprecated; " + "migrate to [[tool.dynamic-metadata]] with provider = 'setuptools_git_versioning.dynamic_metadata' " + "on a backend supporting dynamic-metadata such as scikit-build-core>=1.0.", + DeprecationWarning, + stacklevel=2, +) + def dynamic_metadata( field: str, @@ -30,29 +37,7 @@ def dynamic_metadata( ) 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)) + return _dynamic_metadata({}, {"name": _read_project_name(Path.cwd())})["version"] def _read_project_name(root: Path) -> str | None: diff --git a/tests/test_integration/test_dynamic_metadata.py b/tests/test_integration/test_dynamic_metadata.py new file mode 100644 index 0000000..6d31535 --- /dev/null +++ b/tests/test_integration/test_dynamic_metadata.py @@ -0,0 +1,221 @@ +from __future__ import annotations + +import re +import sys +import textwrap +from types import MappingProxyType +from typing import TYPE_CHECKING, Any + +import pytest +import tomli_w + +from setuptools_git_versioning import dynamic_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({}, {"name": "mypkg"})["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({}, {"name": "mypkg"})["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({}, {"name": "mypkg"})["version"] + assert re.fullmatch(r"1\.2\.3\.post1\+git\.[0-9a-f]{8}", result), result + + +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({}, {"name": "mypkg"})["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({}, {"name": "mypkg"})["version"] + + +def test_registered_provider(repo, monkeypatch): + importlib_metadata = pytest.importorskip("importlib.metadata") + write_config(repo, {"enabled": True}) + create_tag(repo, "1.2.3") + monkeypatch.chdir(repo) + + entry_points = [ + entry + for entry in importlib_metadata.distribution("setuptools-git-versioning").entry_points + if entry.group == "dynamic_metadata.provider" and entry.name == "setuptools_git_versioning.dynamic_metadata" + ] + (entry_point,) = entry_points + provider = entry_point.load() + settings = MappingProxyType({}) + project = MappingProxyType({"name": "mypkg", "dynamic": ["version"]}) + + assert provider.dynamic_metadata(settings=settings, project=project) == {"version": "1.2.3"} + assert project == {"name": "mypkg", "dynamic": ["version"]} + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="dynamic-metadata requires Python 3.8+") +@pytest.mark.parametrize("build_state", ["sdist", "wheel", "editable", "metadata_wheel", "metadata_editable"]) +def test_dynamic_metadata_loader(repo, monkeypatch, build_state): + from dynamic_metadata.loader import ( + dynamic_wheel_fields, + entries_from_pyproject, + get_requires_for_dynamic_metadata, + process_dynamic_metadata, + ) + + config = { + "project": {"name": "mypkg", "dynamic": ["version", "description"]}, + "tool": { + "setuptools-git-versioning": {"enabled": True}, + "dynamic-metadata": [ + {"provider": "setuptools_git_versioning.dynamic_metadata"}, + { + "provider": "dynamic_metadata.template", + "field": "description", + "result": "{project[name]} {project[version]}", + }, + ], + }, + } + create_file(repo, "pyproject.toml", tomli_w.dumps(config)) + create_tag(repo, "1.2.3") + monkeypatch.chdir(repo) + + entries = entries_from_pyproject(config) + assert process_dynamic_metadata(config["project"], entries, build_state) == { + "name": "mypkg", + "version": "1.2.3", + "description": "mypkg 1.2.3", + "dynamic": [], + } + assert dynamic_wheel_fields(entries) == set() + assert get_requires_for_dynamic_metadata(entries) == [] + + +def test_dynamic_metadata_uses_resolved_project_name(repo, monkeypatch): + write_config(repo, {"enabled": True, "version_callback": ".version:get_version"}) + (repo / "resolved_pkg").mkdir() + create_file(repo, "resolved_pkg/__init__.py", "") + create_file(repo, "resolved_pkg/version.py", 'def get_version():\n return "2.3.4"\n') + monkeypatch.chdir(repo) + + assert metadata.dynamic_metadata({}, {"name": "resolved_pkg"}) == {"version": "2.3.4"} + + +def test_dynamic_metadata_without_project_name(repo, monkeypatch): + write_config(repo, {"enabled": True, "starting_version": "2.0"}) + monkeypatch.chdir(repo) + + assert metadata.dynamic_metadata({}, {}) == {"version": "2.0"} + + +def test_dynamic_metadata_rejects_inline_settings(repo, monkeypatch): + write_config(repo, {"enabled": True}) + monkeypatch.chdir(repo) + settings = MappingProxyType({"template": "{tag}"}) + + with pytest.raises(ValueError, match=r"Inline configuration under \[\[tool\.dynamic-metadata\]\]"): + metadata.dynamic_metadata(settings, {"name": "mypkg"}) + + assert settings == {"template": "{tag}"} + + +def test_version_file(repo, monkeypatch): + write_config(repo, {"enabled": True, "version_file": "VERSION"}) + create_file(repo, "VERSION", "3.4.5") + monkeypatch.chdir(repo) + + assert metadata.dynamic_metadata({}, {"name": "mypkg"})["version"] == "3.4.5" + + +def test_sdist_version_is_preserved(repo_dir, monkeypatch): + (repo_dir / "pyproject.toml").write_text("[tool.setuptools-git-versioning]\nenabled = true\n") + (repo_dir / "PKG-INFO").write_text("Metadata-Version: 2.2\nName: mypkg\nVersion: 4.5.6\n") + monkeypatch.chdir(repo_dir) + + assert metadata.dynamic_metadata({}, {"name": "mypkg"})["version"] == "4.5.6" + + +def test_end_to_end_build_via_scikit_build_core(repo): + """Build using the new dynamic-metadata provider.""" + import scikit_build_core + from packaging.version import Version + + if Version(scikit_build_core.__version__) < Version("1.0"): + pytest.skip("The tool.dynamic-metadata configuration requires scikit-build-core 1.0+") + + config = { + "setuptools-git-versioning": {"enabled": True}, + "dynamic-metadata": [{"provider": "setuptools_git_versioning.dynamic_metadata"}], + } + requirements = ["scikit-build-core", "setuptools-git-versioning", "dynamic-metadata>=0.4"] + + create_file( + repo, + "pyproject.toml", + tomli_w.dumps( + { + "build-system": { + "requires": requirements, + "build-backend": "scikit_build_core.build", + }, + "project": {"name": "mypkg", "dynamic": ["version"]}, + "tool": config, + }, + ), + ) + 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" diff --git a/tests/test_integration/test_scikit_build_metadata.py b/tests/test_integration/test_scikit_build_metadata.py index 19e89ef..53386d8 100644 --- a/tests/test_integration/test_scikit_build_metadata.py +++ b/tests/test_integration/test_scikit_build_metadata.py @@ -1,19 +1,24 @@ from __future__ import annotations +import importlib import re import sys import textwrap +import warnings 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 +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from setuptools_git_versioning import scikit_metadata as metadata + pytestmark = [pytest.mark.all, pytest.mark.important] @@ -62,7 +67,7 @@ def fake_version_from_git(package_name=None, **_kwargs): captured["package_name"] = package_name return Version("9.9.9") - monkeypatch.setattr(metadata, "version_from_git", fake_version_from_git) + monkeypatch.setattr("setuptools_git_versioning.dynamic_metadata.version_from_git", fake_version_from_git) assert metadata.dynamic_metadata("version") == "9.9.9" assert captured["package_name"] == "mypkg" @@ -84,7 +89,7 @@ def fake_version_from_git(package_name=None, **_kwargs): captured["package_name"] = package_name return Version("0.0.1") - monkeypatch.setattr(metadata, "version_from_git", fake_version_from_git) + monkeypatch.setattr("setuptools_git_versioning.dynamic_metadata.version_from_git", fake_version_from_git) metadata.dynamic_metadata("version") assert captured["package_name"] is None @@ -128,6 +133,11 @@ def test_get_requires_for_dynamic_metadata(): assert metadata.get_requires_for_dynamic_metadata({"anything": True}) == ["setuptools-git-versioning"] +def test_legacy_module_warns(): + with pytest.warns(DeprecationWarning, match=r"migrate to \[\[tool\.dynamic-metadata\]\]"): + importlib.reload(metadata) + + def test_end_to_end_build_via_scikit_build_core(repo): """Drive the actual scikit-build-core backend to verify the provider protocol matches.""" diff --git a/uv.lock b/uv.lock index 690bde1..de3ae94 100644 --- a/uv.lock +++ b/uv.lock @@ -829,6 +829,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/11/208f72084084d3f6a2ed5ebfdfc846692c3f7ad6dce65e400194924f7eed/domdf_python_tools-3.10.0-py3-none-any.whl", hash = "sha256:5e71c1be71bbcc1f881d690c8984b60e64298ec256903b3147f068bc33090c36", size = 126860, upload-time = "2025-02-12T17:34:04.093Z" }, ] +[[package]] +name = "dynamic-metadata" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-resources", version = "6.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/b0/48deaa4c81bdcb92e99a6aecb9e242c79606842ac52952e1ffb55c59b603/dynamic_metadata-0.6.0.tar.gz", hash = "sha256:a01ea4bbd5eda19b051889f9bc61d8d059d8113ff979fb3ce706343f4f6b9a73", size = 55505, upload-time = "2026-09-02T00:11:54.827Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9b/8127a041502cabca313511567d975c52eabb6b0c626422287f4e46d6f9f9/dynamic_metadata-0.6.0-py3-none-any.whl", hash = "sha256:24feb00d794d409987a900f70272b0e47fca44867f66436de1c6c9e5f808e628", size = 33810, upload-time = "2026-09-02T00:11:53.567Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -1938,6 +1951,7 @@ test = [ { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "coverage", version = "7.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "dynamic-metadata", marker = "python_full_version >= '3.8'" }, { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, @@ -1986,6 +2000,7 @@ docs = [ test = [ { name = "build", specifier = ">=1.1.1" }, { name = "coverage", specifier = ">=7.2.7" }, + { name = "dynamic-metadata", marker = "python_full_version >= '3.8'", specifier = ">=0.4" }, { name = "pytest", specifier = ">=7.4.4" }, { name = "pytest-rerunfailures", specifier = ">=13.0" }, { name = "pytest-xdist", specifier = ">=3.5.0" },