setuptools-git-versioning 3.0.1__tar.gz → 3.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {setuptools_git_versioning-3.0.1/setuptools_git_versioning.egg-info → setuptools_git_versioning-3.2.0}/PKG-INFO +31 -2
  2. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/README.rst +29 -1
  3. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/pyproject.toml +19 -5
  4. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/__init__.py +3 -0
  5. setuptools_git_versioning-3.2.0/setuptools_git_versioning/archival.py +225 -0
  6. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/defaults.py +1 -0
  7. setuptools_git_versioning-3.2.0/setuptools_git_versioning/dynamic_metadata.py +51 -0
  8. setuptools_git_versioning-3.2.0/setuptools_git_versioning/scikit_metadata.py +66 -0
  9. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/version.py +56 -23
  10. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0/setuptools_git_versioning.egg-info}/PKG-INFO +31 -2
  11. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning.egg-info/SOURCES.txt +3 -0
  12. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning.egg-info/entry_points.txt +3 -0
  13. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/LICENSE +0 -0
  14. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setup.cfg +0 -0
  15. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/__main__.py +0 -0
  16. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/cli.py +0 -0
  17. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/factories.py +0 -0
  18. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/git.py +0 -0
  19. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/log.py +0 -0
  20. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/setup.py +0 -0
  21. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning/subst.py +0 -0
  22. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning.egg-info/dependency_links.txt +0 -0
  23. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning.egg-info/requires.txt +0 -0
  24. {setuptools_git_versioning-3.0.1 → setuptools_git_versioning-3.2.0}/setuptools_git_versioning.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setuptools-git-versioning
3
- Version: 3.0.1
3
+ Version: 3.2.0
4
4
  Summary: Use git repo data for building a version number according to PEP-440
5
5
  Author-email: dolfinus <martinov.m.s.8@gmail.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
23
24
  Classifier: Operating System :: OS Independent
24
25
  Requires-Python: >=3.7
25
26
  Description-Content-Type: text/x-rst
@@ -81,7 +82,8 @@ between ``setuptools-git-versioning`` and other tools.
81
82
 
82
83
  - Only Git v2 is supported
83
84
 
84
- - Only Setuptools build backend is supported (no Poetry & others)
85
+ - Setuptools and build backends supporting `dynamic-metadata <https://dynamic-metadata.readthedocs.io>`_
86
+ (e.g. `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ 1.0+) are supported.
85
87
 
86
88
  - Currently does not support automatic exporting of package version to a file for runtime use
87
89
  (but you can use ``setuptools-git-versioning > file`` redirect instead)
@@ -173,3 +175,30 @@ and then add new argument ``setuptools_git_versioning`` with config options:
173
175
  )
174
176
 
175
177
  Commands are the same as above, plus ``python -m setup.py`` returns the same version.
178
+
179
+ ``dynamic-metadata``
180
+ ~~~~~~~~~~~~~~~~~~~~
181
+
182
+ For build backends supporting `dynamic-metadata <https://dynamic-metadata.readthedocs.io>`_ 0.4+,
183
+ select the registered provider in ``[[tool.dynamic-metadata]]``. For example, with
184
+ `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ 1.0+:
185
+
186
+ .. code:: toml
187
+
188
+ [build-system]
189
+ requires = [ "scikit-build-core>=1.0", "dynamic-metadata>=0.4", "setuptools-git-versioning>=3.2,<4", ]
190
+ build-backend = "scikit_build_core.build"
191
+
192
+ [project]
193
+ name = "mypackage"
194
+ dynamic = ["version"]
195
+
196
+ [[tool.dynamic-metadata]]
197
+ provider = "setuptools_git_versioning.dynamic_metadata"
198
+
199
+ [tool.setuptools-git-versioning]
200
+ enabled = true
201
+
202
+ The provider sets only ``version`` and reads options from ``[tool.setuptools-git-versioning]``.
203
+ Inline settings in ``[[tool.dynamic-metadata]]`` are not supported.
204
+ The provider itself has no runtime dependency on ``dynamic-metadata``.
@@ -50,7 +50,8 @@ between ``setuptools-git-versioning`` and other tools.
50
50
 
51
51
  - Only Git v2 is supported
52
52
 
53
- - Only Setuptools build backend is supported (no Poetry & others)
53
+ - Setuptools and build backends supporting `dynamic-metadata <https://dynamic-metadata.readthedocs.io>`_
54
+ (e.g. `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ 1.0+) are supported.
54
55
 
55
56
  - Currently does not support automatic exporting of package version to a file for runtime use
56
57
  (but you can use ``setuptools-git-versioning > file`` redirect instead)
@@ -142,3 +143,30 @@ and then add new argument ``setuptools_git_versioning`` with config options:
142
143
  )
143
144
 
144
145
  Commands are the same as above, plus ``python -m setup.py`` returns the same version.
146
+
147
+ ``dynamic-metadata``
148
+ ~~~~~~~~~~~~~~~~~~~~
149
+
150
+ For build backends supporting `dynamic-metadata <https://dynamic-metadata.readthedocs.io>`_ 0.4+,
151
+ select the registered provider in ``[[tool.dynamic-metadata]]``. For example, with
152
+ `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ 1.0+:
153
+
154
+ .. code:: toml
155
+
156
+ [build-system]
157
+ requires = [ "scikit-build-core>=1.0", "dynamic-metadata>=0.4", "setuptools-git-versioning>=3.2,<4", ]
158
+ build-backend = "scikit_build_core.build"
159
+
160
+ [project]
161
+ name = "mypackage"
162
+ dynamic = ["version"]
163
+
164
+ [[tool.dynamic-metadata]]
165
+ provider = "setuptools_git_versioning.dynamic_metadata"
166
+
167
+ [tool.setuptools-git-versioning]
168
+ enabled = true
169
+
170
+ The provider sets only ``version`` and reads options from ``[tool.setuptools-git-versioning]``.
171
+ Inline settings in ``[[tool.dynamic-metadata]]`` are not supported.
172
+ The provider itself has no runtime dependency on ``dynamic-metadata``.
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta:__legacy__"
9
9
 
10
10
  [project]
11
11
  name = "setuptools-git-versioning"
12
- version = "3.0.1"
12
+ version = "3.2.0"
13
13
  authors = [{name = "dolfinus", email = "martinov.m.s.8@gmail.com"}]
14
14
  license = {text = "MIT"}
15
15
  description = "Use git repo data for building a version number according to PEP-440"
@@ -25,6 +25,7 @@ classifiers = [
25
25
  "Programming Language :: Python :: 3.11",
26
26
  "Programming Language :: Python :: 3.12",
27
27
  "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
28
29
  "Operating System :: OS Independent",
29
30
  ]
30
31
  requires-python = ">=3.7"
@@ -54,6 +55,9 @@ setuptools_git_versioning = "setuptools_git_versioning:infer_version"
54
55
  [project.entry-points."console_scripts"]
55
56
  setuptools-git-versioning = "setuptools_git_versioning.cli:main"
56
57
 
58
+ [project.entry-points."dynamic_metadata.provider"]
59
+ "setuptools_git_versioning.dynamic_metadata" = "setuptools_git_versioning.dynamic_metadata"
60
+
57
61
  [tool.setuptools.packages.find]
58
62
  include = ["setuptools_git_versioning*"]
59
63
  exclude = ["docs*", "tests*"]
@@ -65,8 +69,8 @@ dev_template = "{tag}.post{ccount}"
65
69
 
66
70
  [dependency-groups]
67
71
  dev = [
68
- "ruff~=0.14.11",
69
- "prek~=0.2.28",
72
+ "ruff~=0.15.22",
73
+ "prek~=0.5.0",
70
74
  ]
71
75
  test = [
72
76
  "build>=1.1.1",
@@ -77,19 +81,24 @@ test = [
77
81
  # Some tests need to write TOML, but tomli and tomllib are read-only, and toml package is unmaintained
78
82
  "tomli-w>=1.0.0",
79
83
  "wheel>=0.42.0",
84
+ "scikit-build-core>=0.5",
85
+ "dynamic-metadata>=0.4; python_version >= '3.8'",
80
86
  ]
81
87
  docs = [
82
88
  "furo~=2025.12.19",
83
89
  "numpydoc~=1.10.0",
90
+ # https://github.com/sphinx-toolbox/sphinx-toolbox/issues/201
84
91
  "sphinx~=8.2.3",
85
92
  "sphinx-argparse~=0.5.2",
93
+ # https://github.com/sphinx-toolbox/sphinx-toolbox/pull/209
86
94
  "sphinx-autodoc-typehints~=3.5.2",
87
95
  "sphinx-copybutton~=0.5.2",
88
96
  "sphinx-last-updated-by-git~=0.3.8",
97
+ # https://github.com/sphinx-toolbox/sphinx-toolbox/issues/189
89
98
  "sphinx-tabs~=3.4.5",
90
99
  "sphinxcontrib-towncrier~=0.5.0a0",
91
100
  "towncrier~=25.8.0",
92
- "sphinx-toolbox~=4.1.0",
101
+ "sphinx-toolbox~=4.1.2",
93
102
  ]
94
103
 
95
104
  [tool.uv]
@@ -105,7 +114,7 @@ line-length = 120
105
114
 
106
115
  [tool.ruff.lint]
107
116
  select = ["ALL"]
108
- ignore = ["ARG", "ANN", "A002", "COM812", "D", "TD", "FIX002", "PLC0415"]
117
+ ignore = ["ARG", "ANN", "A002", "COM812", "D", "TD", "FIX002", "PLC0415", "CPY001"]
109
118
 
110
119
  [tool.ruff.lint.per-file-ignores]
111
120
  "docs/conf.py" = ["INP001"]
@@ -171,3 +180,8 @@ showcontent = true
171
180
  directory = "misc"
172
181
  name = "Misc"
173
182
  showcontent = true
183
+
184
+ [[tool.towncrier.type]]
185
+ directory = "deprecation"
186
+ name = "Deprecation"
187
+ showcontent = true
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any
4
4
 
5
+ from setuptools_git_versioning.archival import parse_archival_file, version_from_archival
5
6
  from setuptools_git_versioning.git import (
6
7
  count_since,
7
8
  get_all_tags,
@@ -37,5 +38,7 @@ __all__ = [
37
38
  "get_version",
38
39
  "infer_version",
39
40
  "is_dirty",
41
+ "parse_archival_file",
42
+ "version_from_archival",
40
43
  "version_from_git",
41
44
  ]
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os # noqa: TC003
5
+ import re
6
+ from dataclasses import dataclass
7
+ from email.parser import HeaderParser
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ from setuptools_git_versioning.defaults import (
12
+ DEFAULT_DEV_TEMPLATE,
13
+ DEFAULT_DIRTY_TEMPLATE,
14
+ DEFAULT_TEMPLATE,
15
+ )
16
+ from setuptools_git_versioning.factories import create_branch_formatter, create_tag_formatter
17
+ from setuptools_git_versioning.log import DEBUG, INFO
18
+ from setuptools_git_versioning.subst import resolve_substitutions
19
+
20
+ if TYPE_CHECKING:
21
+ from typing import Callable
22
+
23
+ from packaging.version import Version
24
+
25
+ ARCHIVAL_FILENAME = ".git_archival.txt"
26
+ DESCRIBE_UNSUPPORTED = "%(describe"
27
+ FORMAT_UNSUBSTITUTED = "$Format"
28
+ DESCRIBE_PARTS = 3 # tag-N-gSHA
29
+
30
+ REF_TAG_RE = re.compile(r"(?<=\btag: )([^,]+)\b")
31
+ REF_HEAD_RE = re.compile(r"HEAD\s*->\s*([^,]+)")
32
+ FULL_SHA_RE = re.compile(r"^([0-9a-f]{40}|[0-9a-f]{64})$") # SHA-1 or SHA-256
33
+
34
+ log = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class ArchivalData:
39
+ tag: str
40
+ ccount: int
41
+ sha: str
42
+ full_sha: str
43
+ dirty: bool
44
+ branch: str | None
45
+
46
+
47
+ def parse_archival_file(path: str | os.PathLike) -> dict[str, str]:
48
+ """Read a .git_archival.txt file and return its key/value pairs.
49
+
50
+ Keys are normalized to lowercase so lookups behave consistently
51
+ regardless of whether the file uses `node:` or `Node:` etc.
52
+ """
53
+ content = Path(path).read_text(encoding="utf-8")
54
+ log.log(DEBUG, "'%s' content:\n%s", ARCHIVAL_FILENAME, content)
55
+ message = HeaderParser().parsestr(content)
56
+
57
+ # HeaderParser treats the first blank line as the end of headers.
58
+ # Anything after it ends up in the message body and is silently
59
+ # dropped from .items(). Warn the user instead of losing fields.
60
+ payload = message.get_payload()
61
+ if isinstance(payload, str) and payload.strip():
62
+ log.warning(
63
+ "'%s' contains content after a blank line; those fields will be ignored",
64
+ ARCHIVAL_FILENAME,
65
+ )
66
+
67
+ return {key.lower(): value for key, value in message.items()}
68
+
69
+
70
+ def _parse_describe(describe: str) -> tuple[str, int, str | None, bool]:
71
+ """Parse a `git describe`-style string into (tag, ccount, short_sha, dirty)."""
72
+ dirty = False
73
+ if describe.endswith("-dirty"):
74
+ dirty = True
75
+ describe = describe[: -len("-dirty")]
76
+
77
+ parts = describe.rsplit("-", 2)
78
+ if len(parts) < DESCRIBE_PARTS:
79
+ return describe, 0, None, dirty
80
+
81
+ tag, ccount_str, gnode = parts
82
+ try:
83
+ ccount = int(ccount_str)
84
+ except ValueError:
85
+ return describe, 0, None, dirty
86
+
87
+ short_sha = gnode[1:] if gnode.startswith("g") else gnode
88
+ return tag, ccount, short_sha, dirty
89
+
90
+
91
+ def _branch_from_ref_names(ref_names: str) -> str | None:
92
+ match = REF_HEAD_RE.search(ref_names)
93
+ if match:
94
+ return match.group(1).strip()
95
+ return None
96
+
97
+
98
+ def get_data_from_archival_file(data: dict[str, str]) -> ArchivalData | None:
99
+ """Convert parsed archival data into structured version info, or None.
100
+
101
+ Returns None when the file looks unsubstituted or otherwise unusable so
102
+ the caller can fall through to live git.
103
+ """
104
+ if any(FORMAT_UNSUBSTITUTED in value for value in data.values()):
105
+ log.warning(
106
+ "'%s' contains unprocessed '$Format:...$' placeholders, skipping",
107
+ ARCHIVAL_FILENAME,
108
+ )
109
+ return None
110
+
111
+ node = data.get("node", "").strip()
112
+ full_sha = node if FULL_SHA_RE.match(node) else ""
113
+ ref_names = data.get("ref-names", "")
114
+ branch = _branch_from_ref_names(ref_names)
115
+ describe = data.get("describe-name", "").strip()
116
+
117
+ describe_tag: str | None = None
118
+ ccount = 0
119
+ short_sha = ""
120
+ dirty = False
121
+
122
+ if describe and DESCRIBE_UNSUPPORTED not in describe:
123
+ describe_tag, ccount, parsed_sha, dirty = _parse_describe(describe)
124
+ if parsed_sha:
125
+ short_sha = parsed_sha
126
+ elif describe:
127
+ log.warning(
128
+ "git archive did not expand %(describe...) (git <2.32), falling back to ref-names",
129
+ )
130
+
131
+ if describe_tag is not None:
132
+ tag = describe_tag
133
+ else:
134
+ tags = REF_TAG_RE.findall(ref_names)
135
+ if not tags:
136
+ log.log(
137
+ INFO,
138
+ "'%s' has no usable describe-name or tag in ref-names",
139
+ ARCHIVAL_FILENAME,
140
+ )
141
+ return None
142
+ tag = tags[0].strip()
143
+
144
+ # Prefer the full SHA when available so {sha} matches the live-git
145
+ # path's `full_sha[:8]` rendering. Fall back to the short SHA from
146
+ # describe-name only when no valid `node` field is present.
147
+ if full_sha:
148
+ short_sha = full_sha[:8]
149
+ elif short_sha:
150
+ full_sha = short_sha
151
+
152
+ return ArchivalData(
153
+ tag=tag,
154
+ ccount=ccount,
155
+ sha=short_sha[:8],
156
+ full_sha=full_sha,
157
+ dirty=dirty,
158
+ branch=branch,
159
+ )
160
+
161
+
162
+ def version_from_archival( # noqa: PLR0913
163
+ project_root: str | os.PathLike,
164
+ *,
165
+ package_name: str | None = None,
166
+ template: str = DEFAULT_TEMPLATE,
167
+ dev_template: str = DEFAULT_DEV_TEMPLATE,
168
+ dirty_template: str = DEFAULT_DIRTY_TEMPLATE,
169
+ tag_formatter: Callable[[str], str] | str | None = None,
170
+ branch_formatter: Callable[[str], str] | str | None = None,
171
+ ) -> Version | None:
172
+ """Return a Version derived from .git_archival.txt, or None if unavailable."""
173
+ archival_path = Path(project_root).joinpath(ARCHIVAL_FILENAME)
174
+ if not archival_path.exists():
175
+ log.log(DEBUG, "No '%s' present at '%s'", ARCHIVAL_FILENAME, project_root)
176
+ return None
177
+
178
+ log.log(INFO, "File '%s' is found, reading its content", archival_path)
179
+ data = parse_archival_file(archival_path)
180
+ info = get_data_from_archival_file(data)
181
+ if info is None:
182
+ return None
183
+
184
+ log.log(DEBUG, "Parsed archival data: %r", info)
185
+
186
+ tag = info.tag
187
+ if tag_formatter is not None:
188
+ tag_format_callback = create_tag_formatter(tag_formatter, package_name=package_name, root=project_root)
189
+ tag = tag_format_callback(tag)
190
+ log.log(DEBUG, "Tag after formatting: %r", tag)
191
+
192
+ if info.dirty:
193
+ log.log(INFO, "Using template from 'dirty_template' option")
194
+ chosen = dirty_template
195
+ elif info.ccount > 0:
196
+ log.log(INFO, "Using template from 'dev_template' option")
197
+ chosen = dev_template
198
+ else:
199
+ log.log(INFO, "Using template from 'template' option")
200
+ chosen = template
201
+
202
+ # When ref-names is absent or doesn't reveal a current branch, default
203
+ # to the literal "HEAD" so `{branch}` substitution mirrors what
204
+ # `git rev-parse --abbrev-ref HEAD` produces in detached-HEAD state.
205
+ branch = info.branch if info.branch is not None else "HEAD"
206
+ if branch_formatter is not None:
207
+ branch_format_callback = create_branch_formatter(branch_formatter, package_name=package_name, root=project_root)
208
+ branch = branch_format_callback(branch)
209
+ log.log(INFO, "Branch after formatting: %r", branch)
210
+
211
+ rendered = resolve_substitutions(
212
+ chosen,
213
+ sha=info.sha,
214
+ tag=tag,
215
+ ccount=info.ccount,
216
+ branch=branch,
217
+ full_sha=info.full_sha,
218
+ )
219
+ log.log(INFO, "Version number after resolving substitutions: %r", rendered)
220
+
221
+ # Deferred to avoid a top-level circular import:
222
+ # `version.py` imports `version_from_archival` from this module.
223
+ from setuptools_git_versioning.version import sanitize_version
224
+
225
+ return sanitize_version(rendered)
@@ -13,6 +13,7 @@ DEFAULT_CONFIG = {
13
13
  "version_callback": None,
14
14
  "version_file": None,
15
15
  "count_commits_from_version_file": False,
16
+ "prefer_tag": False,
16
17
  "tag_formatter": None,
17
18
  "branch_formatter": None,
18
19
  "tag_filter": None,
@@ -0,0 +1,51 @@
1
+ """Version provider for the dynamic-metadata protocol."""
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"]
16
+
17
+
18
+ def dynamic_metadata(
19
+ settings: Mapping[str, Any],
20
+ project: Mapping[str, Any],
21
+ ) -> dict[str, str]:
22
+ if settings:
23
+ msg = (
24
+ "Inline configuration under [[tool.dynamic-metadata]] is not supported. "
25
+ "Configure setuptools-git-versioning under [tool.setuptools-git-versioning] instead."
26
+ )
27
+ raise ValueError(msg)
28
+
29
+ root = Path.cwd()
30
+
31
+ config = read_toml(root=root)
32
+ if not config:
33
+ msg = (
34
+ "Missing [tool.setuptools-git-versioning] section in pyproject.toml. "
35
+ "Add it (with at minimum 'enabled = true') to use this provider."
36
+ )
37
+ raise ValueError(msg)
38
+
39
+ if not config.pop("enabled", True):
40
+ msg = (
41
+ "[tool.setuptools-git-versioning] has 'enabled = false' but the "
42
+ "metadata provider for setuptools-git-versioning was selected. "
43
+ "Either remove the provider or set 'enabled = true'."
44
+ )
45
+ raise ValueError(msg)
46
+
47
+ set_default_options(config)
48
+
49
+ name = project.get("name")
50
+ package_name = name if isinstance(name, str) else None
51
+ return {"version": str(version_from_git(package_name, **config, root=root))}
@@ -0,0 +1,66 @@
1
+ """Deprecated version provider for the classic scikit-build-core protocol."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from setuptools_git_versioning.dynamic_metadata import dynamic_metadata as _dynamic_metadata
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Mapping
13
+
14
+ __all__ = ["dynamic_metadata", "get_requires_for_dynamic_metadata"]
15
+
16
+ warnings.warn(
17
+ "setuptools_git_versioning.scikit_metadata is deprecated; "
18
+ "migrate to [[tool.dynamic-metadata]] with provider = 'setuptools_git_versioning.dynamic_metadata' "
19
+ "on a backend supporting dynamic-metadata such as scikit-build-core>=1.0.",
20
+ DeprecationWarning,
21
+ stacklevel=2,
22
+ )
23
+
24
+
25
+ def dynamic_metadata(
26
+ field: str,
27
+ settings: Mapping[str, Any] | None = None,
28
+ ) -> str:
29
+ if field != "version":
30
+ msg = f"Only the 'version' field is supported, got {field!r}"
31
+ raise ValueError(msg)
32
+
33
+ if settings:
34
+ msg = (
35
+ "Inline configuration under [tool.scikit-build.metadata.version] is not supported. "
36
+ "Configure setuptools-git-versioning under [tool.setuptools-git-versioning] instead."
37
+ )
38
+ raise ValueError(msg)
39
+
40
+ return _dynamic_metadata({}, {"name": _read_project_name(Path.cwd())})["version"]
41
+
42
+
43
+ def _read_project_name(root: Path) -> str | None:
44
+ pyproject = root / "pyproject.toml"
45
+ if not pyproject.is_file():
46
+ return None
47
+
48
+ try:
49
+ import tomllib
50
+
51
+ with pyproject.open("rb") as file:
52
+ data = tomllib.load(file)
53
+ except ImportError:
54
+ import tomli
55
+
56
+ with pyproject.open("rb") as file:
57
+ data = tomli.load(file)
58
+
59
+ name = data.get("project", {}).get("name")
60
+ return name if isinstance(name, str) else None
61
+
62
+
63
+ def get_requires_for_dynamic_metadata(
64
+ _settings: Mapping[str, Any] | None = None,
65
+ ) -> list[str]:
66
+ return ["setuptools-git-versioning"]
@@ -5,12 +5,13 @@ import logging
5
5
  import os # noqa: TC003
6
6
  import re
7
7
  from pathlib import Path
8
- from typing import Callable
8
+ from typing import Callable, cast
9
9
 
10
10
  # avoid importing 'packaging' because setuptools-git-versioning can be installed using sdist
11
11
  # where 'packaging' is not installed yet
12
12
  from packaging.version import Version
13
13
 
14
+ from setuptools_git_versioning.archival import version_from_archival
14
15
  from setuptools_git_versioning.defaults import (
15
16
  DEFAULT_DEV_TEMPLATE,
16
17
  DEFAULT_DIRTY_TEMPLATE,
@@ -95,6 +96,7 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901
95
96
  version_callback: str | Callable[[], str] | None = None,
96
97
  version_file: str | os.PathLike | None = None,
97
98
  count_commits_from_version_file: bool = False,
99
+ prefer_tag: bool = False,
98
100
  tag_formatter: Callable[[str], str] | str | None = None,
99
101
  branch_formatter: Callable[[str], str] | str | None = None,
100
102
  tag_filter: Callable[[str], str | None] | str | None = None,
@@ -114,12 +116,28 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901
114
116
  # running on sdist package, do not sanitize
115
117
  return Version(version_str)
116
118
 
119
+ archival_version = version_from_archival(
120
+ project_root,
121
+ package_name=package_name,
122
+ template=template,
123
+ dev_template=dev_template,
124
+ dirty_template=dirty_template,
125
+ tag_formatter=tag_formatter,
126
+ branch_formatter=branch_formatter,
127
+ )
128
+ if archival_version is not None:
129
+ log.log(INFO, "Resolved version from '.git_archival.txt': %s", archival_version)
130
+ return archival_version
131
+
117
132
  if version_callback is not None:
118
133
  if version_file is not None:
119
134
  msg = "Either 'version_file' or 'version_callback' can be passed, but not both at the same time"
120
135
  raise ValueError(msg)
121
136
  return get_version_from_callback(version_callback, package_name, root=root)
122
137
 
138
+ ccount: int | None = 0
139
+ on_tag = False
140
+ tag_sha = None
123
141
  head_sha = get_sha(root=root)
124
142
  log.log(INFO, "HEAD SHA-256: %r", head_sha)
125
143
 
@@ -132,15 +150,21 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901
132
150
  tag = get_tag(sort_by=sort_by, root=root, filter_callback=filter_callback)
133
151
  if not tag:
134
152
  log.log(INFO, "No tags found")
135
- tag_sha = None
136
- on_tag = False
137
153
  else:
138
- tag_sha = get_sha(tag, root=root)
154
+ tag_sha = cast("str", get_sha(tag, root=root))
139
155
  log.log(INFO, "Tag SHA-256: %r", tag_sha)
140
156
 
141
157
  on_tag = head_sha is not None and head_sha == tag_sha
142
158
  log.log(INFO, "HEAD is tagged: %r", on_tag)
143
159
 
160
+ ccount = count_since(tag_sha, root=root)
161
+ log.log(INFO, "Commits count between HEAD and last tag: %r", ccount)
162
+
163
+ if tag_formatter is not None:
164
+ tag_format_callback = create_tag_formatter(tag_formatter, package_name=package_name, root=root)
165
+ tag = tag_format_callback(tag)
166
+ log.log(DEBUG, "Tag after formatting: %r", tag)
167
+
144
168
  if version_file:
145
169
  log.log(INFO, "Checking for 'version_file'")
146
170
 
@@ -152,35 +176,44 @@ def version_from_git( # noqa: PLR0915, PLR0912, PLR0913, C901
152
176
  version_file_path,
153
177
  starting_version,
154
178
  )
155
- tag = None
179
+ file_tag = None
156
180
  else:
157
181
  log.log(INFO, "Reading version_file '%s' content", version_file)
158
- tag = version_file_path.read_text().strip() or None
182
+ file_tag = version_file_path.read_text().strip() or None
159
183
 
160
- if not tag:
184
+ if not file_tag:
161
185
  log.log(INFO, "File %r is empty", version_file)
162
186
  else:
163
- log.log(DEBUG, "File content: %r", tag)
164
- if not count_commits_from_version_file:
165
- return sanitize_version(tag)
187
+ log.log(DEBUG, "File content: %r", file_tag)
166
188
 
167
- file_sha = get_latest_file_commit(version_file, root=root)
168
- log.log(DEBUG, "File SHA-256: %r", file_sha)
189
+ if prefer_tag and file_tag and tag:
190
+ log.log(
191
+ INFO,
192
+ "Both 'version_file' and a Git tag (%r) are present, and 'prefer_tag' is enabled: using the tag",
193
+ tag,
194
+ )
195
+ if file_tag and sanitize_version(file_tag) != sanitize_version(tag):
196
+ log.warning(
197
+ "Git tag %r and 'version_file' content %r are different: using the tag. "
198
+ "Update the version file to silence this warning",
199
+ tag,
200
+ file_tag,
201
+ )
169
202
 
170
- ccount = count_since(file_sha, root=root) if file_sha else None
171
- log.log(INFO, "Commits count between HEAD and last version file change: %r", ccount)
203
+ elif file_tag:
204
+ tag = file_tag
172
205
 
173
- elif not head_sha:
174
- log.log(INFO, "Not a git repo, or repo without any branch")
206
+ if not count_commits_from_version_file:
207
+ return sanitize_version(tag)
175
208
 
176
- elif tag_sha:
177
- ccount = count_since(tag_sha, root=root)
178
- log.log(INFO, "Commits count between HEAD and last tag: %r", ccount)
209
+ file_sha = get_latest_file_commit(version_file, root=root)
210
+ log.log(DEBUG, "File SHA-256: %r", file_sha)
179
211
 
180
- if tag_formatter is not None:
181
- tag_format_callback = create_tag_formatter(tag_formatter, package_name=package_name, root=root)
182
- tag = tag_format_callback(tag)
183
- log.log(DEBUG, "Tag after formatting: %r", tag)
212
+ ccount = count_since(file_sha, root=root) if file_sha else None
213
+ log.log(INFO, "Commits count between HEAD and last version file change: %r", ccount)
214
+
215
+ elif not head_sha:
216
+ log.log(INFO, "Not a git repo, or repo without any branch")
184
217
 
185
218
  if not tag:
186
219
  log.log(INFO, "No source for version, return starting_version %r", starting_version)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: setuptools-git-versioning
3
- Version: 3.0.1
3
+ Version: 3.2.0
4
4
  Summary: Use git repo data for building a version number according to PEP-440
5
5
  Author-email: dolfinus <martinov.m.s.8@gmail.com>
6
6
  License: MIT
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.10
20
20
  Classifier: Programming Language :: Python :: 3.11
21
21
  Classifier: Programming Language :: Python :: 3.12
22
22
  Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
23
24
  Classifier: Operating System :: OS Independent
24
25
  Requires-Python: >=3.7
25
26
  Description-Content-Type: text/x-rst
@@ -81,7 +82,8 @@ between ``setuptools-git-versioning`` and other tools.
81
82
 
82
83
  - Only Git v2 is supported
83
84
 
84
- - Only Setuptools build backend is supported (no Poetry & others)
85
+ - Setuptools and build backends supporting `dynamic-metadata <https://dynamic-metadata.readthedocs.io>`_
86
+ (e.g. `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ 1.0+) are supported.
85
87
 
86
88
  - Currently does not support automatic exporting of package version to a file for runtime use
87
89
  (but you can use ``setuptools-git-versioning > file`` redirect instead)
@@ -173,3 +175,30 @@ and then add new argument ``setuptools_git_versioning`` with config options:
173
175
  )
174
176
 
175
177
  Commands are the same as above, plus ``python -m setup.py`` returns the same version.
178
+
179
+ ``dynamic-metadata``
180
+ ~~~~~~~~~~~~~~~~~~~~
181
+
182
+ For build backends supporting `dynamic-metadata <https://dynamic-metadata.readthedocs.io>`_ 0.4+,
183
+ select the registered provider in ``[[tool.dynamic-metadata]]``. For example, with
184
+ `scikit-build-core <https://scikit-build-core.readthedocs.io>`_ 1.0+:
185
+
186
+ .. code:: toml
187
+
188
+ [build-system]
189
+ requires = [ "scikit-build-core>=1.0", "dynamic-metadata>=0.4", "setuptools-git-versioning>=3.2,<4", ]
190
+ build-backend = "scikit_build_core.build"
191
+
192
+ [project]
193
+ name = "mypackage"
194
+ dynamic = ["version"]
195
+
196
+ [[tool.dynamic-metadata]]
197
+ provider = "setuptools_git_versioning.dynamic_metadata"
198
+
199
+ [tool.setuptools-git-versioning]
200
+ enabled = true
201
+
202
+ The provider sets only ``version`` and reads options from ``[tool.setuptools-git-versioning]``.
203
+ Inline settings in ``[[tool.dynamic-metadata]]`` are not supported.
204
+ The provider itself has no runtime dependency on ``dynamic-metadata``.
@@ -3,11 +3,14 @@ README.rst
3
3
  pyproject.toml
4
4
  setuptools_git_versioning/__init__.py
5
5
  setuptools_git_versioning/__main__.py
6
+ setuptools_git_versioning/archival.py
6
7
  setuptools_git_versioning/cli.py
7
8
  setuptools_git_versioning/defaults.py
9
+ setuptools_git_versioning/dynamic_metadata.py
8
10
  setuptools_git_versioning/factories.py
9
11
  setuptools_git_versioning/git.py
10
12
  setuptools_git_versioning/log.py
13
+ setuptools_git_versioning/scikit_metadata.py
11
14
  setuptools_git_versioning/setup.py
12
15
  setuptools_git_versioning/subst.py
13
16
  setuptools_git_versioning/version.py
@@ -4,5 +4,8 @@ setuptools-git-versioning = setuptools_git_versioning.cli:main
4
4
  [distutils.setup_keywords]
5
5
  setuptools_git_versioning = setuptools_git_versioning:parse_config
6
6
 
7
+ [dynamic_metadata.provider]
8
+ setuptools_git_versioning.dynamic_metadata = setuptools_git_versioning.dynamic_metadata
9
+
7
10
  [setuptools.finalize_distribution_options]
8
11
  setuptools_git_versioning = setuptools_git_versioning:infer_version