diff --git a/README.rst b/README.rst index 629a6e0..a45f87c 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.2,<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 2a6d108..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", @@ -176,3 +180,8 @@ showcontent = true directory = "misc" name = "Misc" showcontent = true + +[[tool.towncrier.type]] +directory = "deprecation" +name = "Deprecation" +showcontent = true 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" },