From 3acaa543fb16bb2c19707ef37219713d73a880cd Mon Sep 17 00:00:00 2001 From: Kaleb Barrett Date: Sun, 3 May 2026 14:02:52 -0400 Subject: [PATCH] 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. --- README.rst | 31 +++- docs/changelog/next_release/130.feature.rst | 1 + pyproject.toml | 1 + setuptools_git_versioning/scikit_metadata.py | 81 ++++++++ .../test_scikit_build_metadata.py | 175 ++++++++++++++++++ uv.lock | 119 ++++++++++++ 6 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/next_release/130.feature.rst create mode 100644 setuptools_git_versioning/scikit_metadata.py create mode 100644 tests/test_integration/test_scikit_build_metadata.py diff --git a/README.rst b/README.rst index 05384f7..89fdec1 100644 --- a/README.rst +++ b/README.rst @@ -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 `_ 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) @@ -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 `_ 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. diff --git a/docs/changelog/next_release/130.feature.rst b/docs/changelog/next_release/130.feature.rst new file mode 100644 index 0000000..44c4c83 --- /dev/null +++ b/docs/changelog/next_release/130.feature.rst @@ -0,0 +1 @@ +Added a dynamic-metadata provider compatible with `scikit-build-core `_. diff --git a/pyproject.toml b/pyproject.toml index dbaf1a1..6a72fd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] docs = [ "furo~=2025.12.19", diff --git a/setuptools_git_versioning/scikit_metadata.py b/setuptools_git_versioning/scikit_metadata.py new file mode 100644 index 0000000..4b1a44a --- /dev/null +++ b/setuptools_git_versioning/scikit_metadata.py @@ -0,0 +1,81 @@ +"""Dynamic metadata provider for the scikit-build-core build backend.""" + +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"] diff --git a/tests/test_integration/test_scikit_build_metadata.py b/tests/test_integration/test_scikit_build_metadata.py new file mode 100644 index 0000000..19e89ef --- /dev/null +++ b/tests/test_integration/test_scikit_build_metadata.py @@ -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" diff --git a/uv.lock b/uv.lock index 1d25848..88fb8ab 100644 --- a/uv.lock +++ b/uv.lock @@ -928,6 +928,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, ] +[[package]] +name = "importlib-resources" +version = "5.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "zipp", version = "3.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/a2/3cab1de83f95dd15297c15bdc04d50902391d707247cada1f021bbfe2149/importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6", size = 39894, upload-time = "2023-02-17T22:32:21.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/71/c13ea695a4393639830bf96baea956538ba7a9d06fcce7cef10bfff20f72/importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a", size = 36211, upload-time = "2023-02-17T22:32:19.266Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.4.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/be/f3e8c6081b684f176b761e6a2fef02a0be939740ed6f54109a2951d806f3/importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", size = 43372, upload-time = "2024-09-09T17:03:14.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/6a/4604f9ae2fa62ef47b9de2fa5ad599589d28c9fd1d335f32759813dfa91e/importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717", size = 36115, upload-time = "2024-09-09T17:03:13.39Z" }, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -1202,6 +1232,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pathspec" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/2a/bd167cdf116d4f3539caaa4c332752aac0b3a0cc0174cdb302ee68933e81/pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3", size = 47032, upload-time = "2023-07-29T01:05:04.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/2a/9b1be29146139ef459188f5e420a66e835dda921208db600b7037093891f/pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", size = 29603, upload-time = "2023-07-29T01:05:02.656Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.10' and python_full_version < '3.12'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -1649,6 +1718,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] +[[package]] +name = "scikit-build-core" +version = "0.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "importlib-resources", version = "5.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pathspec", version = "0.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/75/ad5664c8050bbbea46a5f2b6a3dfbc6e6cf284826c0eee0a12f861364b3f/scikit_build_core-0.10.7.tar.gz", hash = "sha256:04cbb59fe795202a7eeede1849112ee9dcbf3469feebd9b8b36aa541336ac4f8", size = 255019, upload-time = "2024-09-20T20:54:15.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/fe/90476c4f6a1b2f922efa00d26e876dd40c7279e28ec18f08f0851ad21ba6/scikit_build_core-0.10.7-py3-none-any.whl", hash = "sha256:5e13ab7ca7c3c6dd019607c3a6f53cba67dade8757c4c4f75b459e2f90e4dbc3", size = 165511, upload-time = "2024-09-20T20:54:14.181Z" }, +] + +[[package]] +name = "scikit-build-core" +version = "0.12.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version >= '3.10' and python_full_version < '3.12'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version >= '3.8' and python_full_version < '3.11'" }, + { name = "importlib-resources", version = "6.4.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, + { name = "pathspec", version = "0.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pathspec", version = "1.1.1", 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.8' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/cd/9ebb50029b6d8a3ee9e38cdce514ebd70190ec1edf28ab0a1f66d0b84670/scikit_build_core-0.12.2.tar.gz", hash = "sha256:562e0bbc9de1a354c87825ccf732080268d6582a0200f648e8c4a2dcb1e3736d", size = 303553, upload-time = "2026-03-05T18:25:57.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/49/b2f0fbe3165d55c02e7f9eec6a10685d518af0ef6e919ff2f589c2d15c85/scikit_build_core-0.12.2-py3-none-any.whl", hash = "sha256:6ea4730da400f9a998ec3287bd3ebc1d751fe45ad0a93451bead8618adbc02b1", size = 192625, upload-time = "2026-03-05T18:25:56.207Z" }, +] + [[package]] name = "setuptools" version = "68.0.0" @@ -1738,6 +1854,8 @@ test = [ { name = "pytest-xdist", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, { name = "pytest-xdist", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, { name = "pytest-xdist", version = "3.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "scikit-build-core", version = "0.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "scikit-build-core", version = "0.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, { name = "tomli-w", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, { name = "tomli-w", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "wheel", version = "0.42.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, @@ -1776,6 +1894,7 @@ test = [ { name = "pytest", specifier = ">=7.4.4" }, { name = "pytest-rerunfailures", specifier = ">=13.0" }, { name = "pytest-xdist", specifier = ">=3.5.0" }, + { name = "scikit-build-core", specifier = ">=0.5" }, { name = "tomli-w", specifier = ">=1.0.0" }, { name = "wheel", specifier = ">=0.42.0" }, ]