vcs-versioning 2.2.4__tar.gz → 2.3.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.
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/CHANGELOG.md +29 -0
- {vcs_versioning-2.2.4/src/vcs_versioning.egg-info → vcs_versioning-2.3.0}/PKG-INFO +1 -1
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/pyproject.toml +3 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_git.py +102 -3
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_hg.py +151 -33
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_scm_workdir.py +64 -1
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_config.py +3 -14
- vcs_versioning-2.3.0/src/vcs_versioning/_dynamic_metadata.py +49 -0
- vcs_versioning-2.3.0/src/vcs_versioning/_file_finders/_git.py +208 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_protocols.py +14 -6
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_run_cmd.py +2 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_test_utils.py +1 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/test_api.py +14 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0/src/vcs_versioning.egg-info}/PKG-INFO +1 -1
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/SOURCES.txt +4 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/entry_points.txt +3 -0
- vcs_versioning-2.3.0/testing_vcs/test_dynamic_metadata.py +49 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_git.py +90 -21
- vcs_versioning-2.3.0/testing_vcs/test_hg_tag_strict.py +206 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_tag_config.py +11 -2
- vcs_versioning-2.3.0/testing_vcs/test_tag_strict_diagnostics.py +293 -0
- vcs_versioning-2.2.4/src/vcs_versioning/_file_finders/_git.py +0 -122
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/LICENSE.txt +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/MANIFEST.in +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/README.md +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/setup.cfg +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/setup.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/__init__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/__main__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/__init__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_hg_git.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_backends/_jj.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/__init__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/_args.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_compat.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_discover.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_dump_version.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_entrypoints.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_environment.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_exceptions.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_fallback_workdir.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_fallbacks.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/__init__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/_hg.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_file_finders/_jj.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_get_version_impl.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_integration.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_integrator_helpers.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_legacy_parse.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_log.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_modify_version.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_node_utils.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_overrides.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_paths.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_project_overrides.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_pyproject_reading.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_requirement_cls.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_scm_metadata.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_scm_version.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_toml.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_types.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_cls.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_fields.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_inference.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_common.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_standard.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/_worktree_discovery.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/overrides.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning/py.typed +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/requires.txt +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/src/vcs_versioning.egg-info/top_level.txt +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/__init__.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/conftest.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_better_root_errors.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_chain_api.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_compat.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_config.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_expect_parse.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_file_finders.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_hg_git.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_integrator_helpers.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_internal_log_level.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_jj.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_legacy_parse.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_mercurial.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_overrides_api.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_overrides_env_reader.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_project_overrides.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_project_path.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_regressions.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_scm_metadata.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_version.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_version_scheme_towncrier.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_version_schemes.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_workdir_api.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/testing_vcs/test_workdir_discovery.py +0 -0
- {vcs_versioning-2.2.4 → vcs_versioning-2.3.0}/uv.lock +0 -0
|
@@ -2,6 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- towncrier release notes start -->
|
|
4
4
|
|
|
5
|
+
## 2.3.0 (2026-08-13)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Add a ``vcs_versioning.dynamic_metadata`` provider for the [dynamic-metadata](https://github.com/scikit-build/dynamic-metadata) system. ([#1465](https://github.com/pypa/setuptools-scm/issues/1465))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Make the `tag.strict` and `scm.git.describe_command` diagnostics actionable and non-conflicting.
|
|
15
|
+
|
|
16
|
+
The `tag.strict` future-default notice is now reported by the git backend rather than at configuration time, and only when the future default would actually select a different tag for the repository -- the message names both the current and the future version string. Projects the change cannot affect are silent, and setting an explicit `describe_command` no longer triggers it at all, so the two warnings can no longer contradict each other.
|
|
17
|
+
|
|
18
|
+
The `describe_command` notice is likewise limited to the case where it and an explicit `tag.strict` really disagree, and no longer claims that `tag.prefix` has no effect -- prefix stripping applies regardless of how the tag was selected.
|
|
19
|
+
|
|
20
|
+
Both are logged at warning level instead of raised as warnings, so `SETUPTOOLS_SCM_DEBUG=ERROR` silences them. ([#1429](https://github.com/pypa/setuptools-scm/issues/1429))
|
|
21
|
+
- Honour `export-ignore` on directories and submodules again in the git file finder.
|
|
22
|
+
|
|
23
|
+
The switch from `git archive` to `git ls-files --recurse-submodules` lost two parts of the archive semantics: `--recurse-submodules` listed every submodule regardless of `export-ignore`, and the `:(exclude,attr:export-ignore)` pathspec only matches files, so an `export-ignore` on a directory no longer excluded the files below it. Projects that kept vendored submodules in an `export-ignore`d directory suddenly shipped them in their sdists.
|
|
24
|
+
|
|
25
|
+
The finder now lists a repository without recursion, checks `export-ignore` for directories via `git check-attr` (which is what `git archive` effectively does when it skips a tree), and only then descends into the submodules that survived. Submodule contents are still listed - with their own `.gitattributes` applied - so `export-ignore` in the parent repository now controls exactly which submodules get packaged. Submodules that are not checked out are skipped instead of failing the listing. ([#1469](https://github.com/pypa/setuptools-scm/issues/1469))
|
|
26
|
+
- Honour `tag.strict` on Mercurial changesets that carry tags of their own, and report the coming strict default for Mercurial repositories.
|
|
27
|
+
|
|
28
|
+
`tag.strict` was only applied when looking for the latest tag, so a checked-out changeset tagged `event-2024` still produced version `2024` even with `tag.strict = true`, while git rejected the same tag. Strict matching now applies to the tags on the changeset too: a changeset carrying only event-style tags is treated as untagged and versioning continues from the last real version tag, matching `git describe --match`. When several tags sit on one changeset, the version-shaped one is now selected instead of whichever Mercurial happened to list first.
|
|
29
|
+
|
|
30
|
+
The `tag.strict` divergence diagnostic added in #1429 now covers Mercurial as well, naming the current and future version whenever the coming default would change them. Both backends share the message, and the git-only helpers moved to `_backends/_scm_workdir.py`.
|
|
31
|
+
|
|
32
|
+
Note that the Mercurial backend required a dot in version tags before setuptools-scm 9, so for Mercurial projects the coming strict default restores the historical behavior. ([#1495](https://github.com/pypa/setuptools-scm/issues/1495))
|
|
33
|
+
|
|
5
34
|
## 2.2.4 (2026-08-07)
|
|
6
35
|
|
|
7
36
|
### Fixed
|
|
@@ -91,6 +91,9 @@ node-and-timestamp = "vcs_versioning._version_schemes:get_local_node_and_timesta
|
|
|
91
91
|
"semver-pep440-release-branch" = "vcs_versioning._version_schemes:release_branch_semver_version"
|
|
92
92
|
"towncrier-fragments" = "vcs_versioning._version_schemes._towncrier:version_from_fragments"
|
|
93
93
|
|
|
94
|
+
[project.entry-points."dynamic_metadata.provider"]
|
|
95
|
+
"vcs_versioning" = "vcs_versioning._dynamic_metadata"
|
|
96
|
+
|
|
94
97
|
[tool.setuptools.packages.find]
|
|
95
98
|
where = ["src"]
|
|
96
99
|
namespaces = false
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import dataclasses
|
|
4
|
+
import fnmatch
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
@@ -23,10 +24,17 @@ from .._run_cmd import CompletedProcess as _CompletedProcess
|
|
|
23
24
|
from .._run_cmd import require_command as _require_command
|
|
24
25
|
from .._run_cmd import run as _run
|
|
25
26
|
from .._scm_version import ScmVersion, meta, tag_to_version
|
|
26
|
-
from ._scm_workdir import
|
|
27
|
+
from ._scm_workdir import (
|
|
28
|
+
STRICT_DIAGNOSTIC,
|
|
29
|
+
Workdir,
|
|
30
|
+
config_location,
|
|
31
|
+
get_latest_file_mtime,
|
|
32
|
+
report_once,
|
|
33
|
+
version_outcome,
|
|
34
|
+
)
|
|
27
35
|
|
|
28
36
|
if TYPE_CHECKING:
|
|
29
|
-
from .._protocols import DescribeCapable
|
|
37
|
+
from .._protocols import DescribeCapable, GitQueryable
|
|
30
38
|
from . import _hg_git as hg_git
|
|
31
39
|
log = logging.getLogger(__name__)
|
|
32
40
|
|
|
@@ -74,12 +82,14 @@ def run_git(
|
|
|
74
82
|
*,
|
|
75
83
|
check: bool = False,
|
|
76
84
|
timeout: int | None = None,
|
|
85
|
+
input: str | None = None,
|
|
77
86
|
) -> _CompletedProcess:
|
|
78
87
|
return _run(
|
|
79
88
|
["git", "--git-dir", repo / ".git", *args],
|
|
80
89
|
cwd=repo,
|
|
81
90
|
check=check,
|
|
82
91
|
timeout=timeout,
|
|
92
|
+
input=input,
|
|
83
93
|
)
|
|
84
94
|
|
|
85
95
|
|
|
@@ -232,7 +242,10 @@ class GitWorkdir(Workdir):
|
|
|
232
242
|
def default_describe(self) -> _CompletedProcess:
|
|
233
243
|
match_glob = self.config.tag.describe_match_glob()
|
|
234
244
|
cmd = make_describe_command(match_glob)
|
|
235
|
-
|
|
245
|
+
res = self.run_git(cmd[1:])
|
|
246
|
+
if self.config.tag.strict is None:
|
|
247
|
+
_warn_if_strict_would_differ(self, self.config, res)
|
|
248
|
+
return res
|
|
236
249
|
|
|
237
250
|
def get_scm_version(self) -> ScmVersion | None:
|
|
238
251
|
"""Obtain version metadata from this git work directory."""
|
|
@@ -264,6 +277,90 @@ class GitWorkdir(Workdir):
|
|
|
264
277
|
return res.returncode == 0
|
|
265
278
|
|
|
266
279
|
|
|
280
|
+
def _describe_tag(output: str) -> str | None:
|
|
281
|
+
"""Extract just the tag name from a ``git describe --long`` output."""
|
|
282
|
+
if not output.strip():
|
|
283
|
+
return None
|
|
284
|
+
return _git_parse_describe(output.strip())[0]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _strict_match_glob(prefix: str) -> str:
|
|
288
|
+
"""The ``--match`` glob ``tag.strict = true`` would produce."""
|
|
289
|
+
return f"{prefix}*[0-9]*.*[0-9]*"
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _describe_outcome(output: str, config: Configuration) -> str:
|
|
293
|
+
"""Render what a ``git describe`` *output* would yield as a version."""
|
|
294
|
+
if not output.strip():
|
|
295
|
+
return version_outcome(config, None)
|
|
296
|
+
|
|
297
|
+
tag, distance, node, dirty = _git_parse_describe(output.strip())
|
|
298
|
+
return version_outcome(config, tag, distance=distance, dirty=dirty, node=node)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _warn_if_strict_would_differ(
|
|
302
|
+
wd: GitQueryable, config: Configuration, permissive: _CompletedProcess
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Report the ``tag.strict`` future default only when it changes the answer.
|
|
305
|
+
|
|
306
|
+
``tag.strict`` is unset, so the permissive glob was used. Stay silent
|
|
307
|
+
unless the strict glob would pick a different tag, so that projects the
|
|
308
|
+
future default cannot affect are never nagged.
|
|
309
|
+
"""
|
|
310
|
+
permissive_tag = _describe_tag(permissive.stdout)
|
|
311
|
+
strict_glob = _strict_match_glob(config.tag.prefix)
|
|
312
|
+
|
|
313
|
+
if permissive_tag is None:
|
|
314
|
+
# nothing matched the wider glob, so nothing matches the narrower one
|
|
315
|
+
return
|
|
316
|
+
if fnmatch.fnmatchcase(permissive_tag, strict_glob):
|
|
317
|
+
# the strict glob matches a subset of the permissive one, so a
|
|
318
|
+
# permissive answer that is itself strict-matching is also the
|
|
319
|
+
# closest strict-matching tag -- no subprocess needed
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
strict = wd.run_git(make_describe_command(strict_glob)[1:])
|
|
323
|
+
report_once(
|
|
324
|
+
f"strict-divergence:{wd.path}:{permissive_tag}:{_describe_tag(strict.stdout)}",
|
|
325
|
+
STRICT_DIAGNOSTIC,
|
|
326
|
+
_describe_outcome(permissive.stdout, config),
|
|
327
|
+
_describe_outcome(strict.stdout, config),
|
|
328
|
+
config_location(config),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _warn_if_describe_command_overrides_strict(
|
|
333
|
+
wd: GitQueryable, config: Configuration, describe_res: _CompletedProcess
|
|
334
|
+
) -> None:
|
|
335
|
+
"""Report that ``describe_command`` beat an explicit ``tag.strict``.
|
|
336
|
+
|
|
337
|
+
Only fires when the two actually disagree about which tag to use --
|
|
338
|
+
setting both is harmless as long as they pick the same tag. ``tag.prefix``
|
|
339
|
+
is deliberately not mentioned: it still strips the prefix before version
|
|
340
|
+
parsing, so combining it with ``describe_command`` is legitimate.
|
|
341
|
+
"""
|
|
342
|
+
strict_glob = _strict_match_glob(config.tag.prefix)
|
|
343
|
+
strict = wd.run_git(make_describe_command(strict_glob)[1:])
|
|
344
|
+
strict_tag = _describe_tag(strict.stdout)
|
|
345
|
+
describe_tag = _describe_tag(describe_res.stdout)
|
|
346
|
+
if strict_tag == describe_tag:
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
report_once(
|
|
350
|
+
f"describe-overrides-strict:{wd.path}:{describe_tag}:{strict_tag}",
|
|
351
|
+
"scm.git.describe_command takes precedence over tag.strict, and they"
|
|
352
|
+
" disagree for this repository:\n"
|
|
353
|
+
" describe_command gives: %s\n"
|
|
354
|
+
" tag.strict = %s would give: %s\n"
|
|
355
|
+
"Drop tag.strict, or drop describe_command and let tag.prefix/tag.strict"
|
|
356
|
+
" build the match pattern, in %s.",
|
|
357
|
+
_describe_outcome(describe_res.stdout, config),
|
|
358
|
+
str(config.tag.strict).lower(),
|
|
359
|
+
_describe_outcome(strict.stdout, config),
|
|
360
|
+
config_location(config),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
267
364
|
def warn_on_shallow(wd: GitWorkdir) -> None:
|
|
268
365
|
"""experimental, may change at any time"""
|
|
269
366
|
if wd.is_shallow() and not wd.head_is_exact_tag():
|
|
@@ -410,6 +507,8 @@ def version_from_describe(
|
|
|
410
507
|
describe_res = wd.run_git(cmd_args[1:])
|
|
411
508
|
else:
|
|
412
509
|
describe_res = _run(cmd_args, wd.path, timeout=wd._subprocess_timeout)
|
|
510
|
+
if config.tag.strict is not None:
|
|
511
|
+
_warn_if_describe_command_overrides_strict(wd, config, describe_res)
|
|
413
512
|
else:
|
|
414
513
|
describe_res = wd.default_describe()
|
|
415
514
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import dataclasses
|
|
3
4
|
import datetime
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
@@ -15,12 +16,92 @@ from .._run_cmd import require_command as _require_command
|
|
|
15
16
|
from .._run_cmd import run as _run
|
|
16
17
|
from .._scm_version import ScmVersion, meta, tag_to_version
|
|
17
18
|
from .._version_cls import Version
|
|
18
|
-
from
|
|
19
|
+
from .._version_schemes import format_version
|
|
20
|
+
from ._scm_workdir import (
|
|
21
|
+
STRICT_DIAGNOSTIC,
|
|
22
|
+
Workdir,
|
|
23
|
+
config_location,
|
|
24
|
+
get_latest_file_mtime,
|
|
25
|
+
report_once,
|
|
26
|
+
)
|
|
19
27
|
|
|
20
28
|
log = logging.getLogger(__name__)
|
|
21
29
|
|
|
22
30
|
_HG_PSEUDO_TAGS = frozenset({"tip", "qbase", "qtip", "qparent"})
|
|
23
31
|
|
|
32
|
+
_KEEP: Any = object()
|
|
33
|
+
"""Sentinel for "use the configured value" -- ``None`` is a real strictness."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def hg_tag_pattern(config: Configuration, strict: bool | None = _KEEP) -> str:
|
|
37
|
+
"""Build the Mercurial regex ``latesttag()`` is given from tag config.
|
|
38
|
+
|
|
39
|
+
*strict* overrides ``config.tag.strict``, which the diagnostics use to ask
|
|
40
|
+
what the other setting would have selected.
|
|
41
|
+
"""
|
|
42
|
+
if strict is _KEEP:
|
|
43
|
+
strict = config.tag.strict
|
|
44
|
+
prefix = re.escape(config.tag.prefix) if config.tag.prefix else ""
|
|
45
|
+
if strict:
|
|
46
|
+
# Require at least one dot in the version part
|
|
47
|
+
return rf"{prefix}\d+\.\d+"
|
|
48
|
+
else:
|
|
49
|
+
return rf"{prefix}\d+"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def matches_tag_pattern(
|
|
53
|
+
tag: str, config: Configuration, strict: bool | None = _KEEP
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""Whether *tag* satisfies the configured strictness.
|
|
56
|
+
|
|
57
|
+
The pattern is the same one ``latesttag()`` is given on the distance path,
|
|
58
|
+
so both paths agree about which tags count as version tags.
|
|
59
|
+
"""
|
|
60
|
+
return re.search(hg_tag_pattern(config, strict), tag) is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def select_tag(
|
|
64
|
+
tags: list[str], config: Configuration, strict: bool | None = _KEEP
|
|
65
|
+
) -> str | None:
|
|
66
|
+
"""Pick the version tag to use from the tags on the current changeset.
|
|
67
|
+
|
|
68
|
+
Pre-filters with tag_regex so non-version tags are silently skipped
|
|
69
|
+
without emitting warnings from tag_to_version().
|
|
70
|
+
Strips tag.prefix before matching when configured.
|
|
71
|
+
|
|
72
|
+
Under ``tag.strict = true`` a tag that is not version-shaped is
|
|
73
|
+
rejected outright rather than falling back to a looser match, so a
|
|
74
|
+
changeset carrying only event-style tags falls through to the distance
|
|
75
|
+
path -- the same thing ``git describe --match`` does (#1495).
|
|
76
|
+
"""
|
|
77
|
+
if strict is _KEEP:
|
|
78
|
+
strict = config.tag.strict
|
|
79
|
+
tag_prefix = config.tag.prefix
|
|
80
|
+
for tag_str in tags:
|
|
81
|
+
check_str = tag_str
|
|
82
|
+
if tag_prefix and tag_str.startswith(tag_prefix):
|
|
83
|
+
check_str = tag_str[len(tag_prefix) :]
|
|
84
|
+
if not config.tag.regex.match(check_str):
|
|
85
|
+
log.debug("skipping non-version tag %r", tag_str)
|
|
86
|
+
continue
|
|
87
|
+
# only narrow when strictness was asked for, so the permissive
|
|
88
|
+
# path keeps selecting exactly what it always has
|
|
89
|
+
if strict and not matches_tag_pattern(tag_str, config, strict):
|
|
90
|
+
log.debug("skipping tag %r: not version-shaped under tag.strict", tag_str)
|
|
91
|
+
continue
|
|
92
|
+
if tag_to_version(tag_str, config) is not None:
|
|
93
|
+
return tag_str
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _rendered(version: ScmVersion | None, tag: str | None) -> str:
|
|
98
|
+
"""Render one side of the ``tag.strict`` comparison for the diagnostic."""
|
|
99
|
+
if version is None:
|
|
100
|
+
return "no matching tag, falling back to the fallback version"
|
|
101
|
+
if tag is None:
|
|
102
|
+
return format_version(version)
|
|
103
|
+
return f"{format_version(version)} (from tag {tag!r})"
|
|
104
|
+
|
|
24
105
|
|
|
25
106
|
def _get_hg_command() -> str:
|
|
26
107
|
"""Read the hg command from resolved runtime settings.
|
|
@@ -95,12 +176,71 @@ class HgWorkdir(Workdir):
|
|
|
95
176
|
tags = self._parse_tags(tags_str)
|
|
96
177
|
|
|
97
178
|
# Try to get version from current tags
|
|
179
|
+
result: ScmVersion | None
|
|
98
180
|
tag_version = self._get_version_from_tags(tags, config)
|
|
99
181
|
if tag_version:
|
|
100
|
-
|
|
182
|
+
result = meta(tag_version, dirty=dirty, branch=branch, config=config)
|
|
183
|
+
else:
|
|
184
|
+
# Fall back to distance-based versioning
|
|
185
|
+
result = self._get_distance_based_version(
|
|
186
|
+
config, dirty, branch, node, node_date
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if config.tag.strict is None:
|
|
190
|
+
self._report_strict_divergence(config, tags, result)
|
|
191
|
+
return result
|
|
101
192
|
|
|
102
|
-
|
|
103
|
-
|
|
193
|
+
def _report_strict_divergence(
|
|
194
|
+
self, config: Configuration, tags: list[str], result: ScmVersion | None
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Report the coming ``tag.strict`` default when it changes the version.
|
|
197
|
+
|
|
198
|
+
Mirrors the git backend: stay silent unless strict matching would
|
|
199
|
+
select a different tag, so repositories the change cannot affect are
|
|
200
|
+
never nagged (#1495).
|
|
201
|
+
"""
|
|
202
|
+
permissive_tag = select_tag(tags, config, strict=False)
|
|
203
|
+
strict_tag = select_tag(tags, config, strict=True)
|
|
204
|
+
|
|
205
|
+
if permissive_tag is not None and permissive_tag == strict_tag:
|
|
206
|
+
# the changeset is tagged with a version-shaped tag either way
|
|
207
|
+
return
|
|
208
|
+
if permissive_tag is None and strict_tag is None:
|
|
209
|
+
# neither takes the exact-tag path, so compare the latest tags
|
|
210
|
+
permissive_tag = self.get_latest_normalizable_tag(config, strict=False)
|
|
211
|
+
if permissive_tag is None:
|
|
212
|
+
return
|
|
213
|
+
if matches_tag_pattern(permissive_tag, config, strict=True):
|
|
214
|
+
# the permissive answer is itself version-shaped, and strict
|
|
215
|
+
# matches a subset, so both paths land on the same tag
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# whichever side found no tag on this changeset falls through to the
|
|
219
|
+
# distance path, so name the tag it would count from there
|
|
220
|
+
if permissive_tag is None:
|
|
221
|
+
permissive_tag = self.get_latest_normalizable_tag(config, strict=False)
|
|
222
|
+
if strict_tag is None:
|
|
223
|
+
strict_tag = self.get_latest_normalizable_tag(config, strict=True)
|
|
224
|
+
|
|
225
|
+
strict_config = dataclasses.replace(
|
|
226
|
+
config, tag=dataclasses.replace(config.tag, strict=True)
|
|
227
|
+
)
|
|
228
|
+
strict_result = self.get_meta(strict_config)
|
|
229
|
+
if (
|
|
230
|
+
result is not None
|
|
231
|
+
and strict_result is not None
|
|
232
|
+
and format_version(result) == format_version(strict_result)
|
|
233
|
+
):
|
|
234
|
+
# different tags, same resulting version -- nothing to act on
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
report_once(
|
|
238
|
+
f"strict-divergence:{self.path}:{permissive_tag}:{strict_tag}",
|
|
239
|
+
STRICT_DIAGNOSTIC,
|
|
240
|
+
_rendered(result, permissive_tag),
|
|
241
|
+
_rendered(strict_result, strict_tag),
|
|
242
|
+
config_location(config),
|
|
243
|
+
)
|
|
104
244
|
|
|
105
245
|
def _get_node_info(self) -> tuple[str, str, str] | None:
|
|
106
246
|
"""Get node, tags, and date information from mercurial log."""
|
|
@@ -163,24 +303,11 @@ class HgWorkdir(Workdir):
|
|
|
163
303
|
def _get_version_from_tags(
|
|
164
304
|
self, tags: list[str], config: Configuration
|
|
165
305
|
) -> Version | None:
|
|
166
|
-
"""Try to get a version from the current tags.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"""
|
|
172
|
-
tag_prefix = config.tag.prefix
|
|
173
|
-
for tag_str in tags:
|
|
174
|
-
check_str = tag_str
|
|
175
|
-
if tag_prefix and tag_str.startswith(tag_prefix):
|
|
176
|
-
check_str = tag_str[len(tag_prefix) :]
|
|
177
|
-
if not config.tag.regex.match(check_str):
|
|
178
|
-
log.debug("skipping non-version tag %r", tag_str)
|
|
179
|
-
continue
|
|
180
|
-
version = tag_to_version(tag_str, config)
|
|
181
|
-
if version is not None:
|
|
182
|
-
return version
|
|
183
|
-
return None
|
|
306
|
+
"""Try to get a version from the current tags."""
|
|
307
|
+
tag_str = select_tag(tags, config)
|
|
308
|
+
if tag_str is None:
|
|
309
|
+
return None
|
|
310
|
+
return tag_to_version(tag_str, config)
|
|
184
311
|
|
|
185
312
|
def _get_distance_based_version(
|
|
186
313
|
self,
|
|
@@ -234,20 +361,11 @@ class HgWorkdir(Workdir):
|
|
|
234
361
|
check=True,
|
|
235
362
|
).stdout
|
|
236
363
|
|
|
237
|
-
def _hg_tag_pattern(self, config: Configuration) -> str:
|
|
238
|
-
"""Build a Mercurial regex pattern from tag configuration."""
|
|
239
|
-
prefix = re.escape(config.tag.prefix) if config.tag.prefix else ""
|
|
240
|
-
if config.tag.strict:
|
|
241
|
-
# Require at least one dot in the version part
|
|
242
|
-
return rf"{prefix}\d+\.\d+"
|
|
243
|
-
else:
|
|
244
|
-
return rf"{prefix}\d+"
|
|
245
|
-
|
|
246
364
|
def get_latest_normalizable_tag(
|
|
247
|
-
self, config: Configuration | None = None
|
|
365
|
+
self, config: Configuration | None = None, strict: bool | None = _KEEP
|
|
248
366
|
) -> str | None:
|
|
249
367
|
if config is not None:
|
|
250
|
-
pattern =
|
|
368
|
+
pattern = hg_tag_pattern(config, strict)
|
|
251
369
|
else:
|
|
252
370
|
pattern = r"\."
|
|
253
371
|
result = self.hg_log(
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import warnings
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from dataclasses import field as dc_field
|
|
6
7
|
from datetime import date, datetime, timezone
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING, overload
|
|
9
|
+
from typing import TYPE_CHECKING, Any, overload
|
|
9
10
|
|
|
10
11
|
from .._scm_version import ScmVersion
|
|
11
12
|
|
|
@@ -77,6 +78,68 @@ def get_latest_file_mtime(changed_files: list[str], base_path: Path) -> date | N
|
|
|
77
78
|
return None
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Configuration diagnostics shared by the backends
|
|
83
|
+
#
|
|
84
|
+
# These report settings whose effect can only be judged once the SCM has been
|
|
85
|
+
# consulted -- most of all the coming ``tag.strict`` default, which is only
|
|
86
|
+
# worth mentioning when it changes the version at hand (#1429, #1495).
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
_diagnostics_reported: set[str] = set()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def report_once(key: str, message: str, *args: object) -> None:
|
|
93
|
+
"""Log a config diagnostic at most once per process.
|
|
94
|
+
|
|
95
|
+
Unlike ``warnings.warn`` the logging module does not deduplicate, and a
|
|
96
|
+
build constructs the configuration more than once (metadata hook plus
|
|
97
|
+
build hook), so the guard is explicit.
|
|
98
|
+
"""
|
|
99
|
+
if key in _diagnostics_reported:
|
|
100
|
+
return
|
|
101
|
+
_diagnostics_reported.add(key)
|
|
102
|
+
log.warning(message, *args)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def config_location(config: Configuration) -> str:
|
|
106
|
+
"""Where the user should go to change the setting being reported."""
|
|
107
|
+
return str(config.relative_to or "your pyproject.toml")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def version_outcome(config: Configuration, tag: str | None, **meta_kw: Any) -> str:
|
|
111
|
+
"""Render the version *tag* would produce, for a diagnostic message.
|
|
112
|
+
|
|
113
|
+
Shows the resulting version string rather than just the tag, since the
|
|
114
|
+
version is what the user actually cares about. Falls back to prose when
|
|
115
|
+
no tag matched or the tag does not parse as a version. Never raises and
|
|
116
|
+
never emits warnings of its own: this describes a path *not* taken.
|
|
117
|
+
"""
|
|
118
|
+
if tag is None:
|
|
119
|
+
return "no matching tag, falling back to the fallback version"
|
|
120
|
+
|
|
121
|
+
from .._scm_version import meta
|
|
122
|
+
from .._version_schemes import format_version
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
with warnings.catch_warnings():
|
|
126
|
+
warnings.simplefilter("ignore")
|
|
127
|
+
version = format_version(meta(tag, config=config, **meta_kw))
|
|
128
|
+
except (ValueError, TypeError):
|
|
129
|
+
return f"no usable version -- tag {tag!r} does not parse as a version"
|
|
130
|
+
return f"{version} (from tag {tag!r})"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
STRICT_DIAGNOSTIC = (
|
|
134
|
+
"tag.strict is not set, and the future default changes this"
|
|
135
|
+
" repository's version:\n"
|
|
136
|
+
" now (tag.strict = false): %s\n"
|
|
137
|
+
" future default (tag.strict = true): %s\n"
|
|
138
|
+
"Set tag.strict explicitly in the [tool.setuptools_scm] table of %s."
|
|
139
|
+
)
|
|
140
|
+
"""Message shared by the backends so their advice reads identically."""
|
|
141
|
+
|
|
142
|
+
|
|
80
143
|
@dataclass()
|
|
81
144
|
class ScmWorkdir:
|
|
82
145
|
"""Base class for VCS work directories.
|
|
@@ -343,20 +343,9 @@ class Configuration:
|
|
|
343
343
|
self.tag, regex=_check_tag_regex(tag_regex)
|
|
344
344
|
)
|
|
345
345
|
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"tag.strict is not set — defaults to False (permissive tag matching)"
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
self.tag.prefix or self.tag.strict is not None
|
|
354
|
-
) and self.scm.git.describe_command is not None:
|
|
355
|
-
log.debug(
|
|
356
|
-
"Both tag.prefix/tag.strict and scm.git.describe_command are set. "
|
|
357
|
-
"The explicit describe_command takes precedence; tag.prefix and "
|
|
358
|
-
"tag.strict will have no effect on the git describe match pattern."
|
|
359
|
-
)
|
|
346
|
+
# tag.strict and describe_command diagnostics deliberately do not live
|
|
347
|
+
# here: they are only actionable once the SCM has been consulted, so the
|
|
348
|
+
# git backend reports them when they actually change the version (#1429)
|
|
360
349
|
|
|
361
350
|
self._resolved_paths = resolve_paths(
|
|
362
351
|
relative_to=self.relative_to,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""scikit-build ``dynamic-metadata`` provider for vcs-versioning.
|
|
2
|
+
|
|
3
|
+
Use this module as a provider for `scikit-build/dynamic-metadata
|
|
4
|
+
<https://github.com/scikit-build/dynamic-metadata>`_ so any backend supporting
|
|
5
|
+
that can fill in a VCS-derived ``version``::
|
|
6
|
+
|
|
7
|
+
[[tool.dynamic-metadata]]
|
|
8
|
+
provider = "vcs_versioning"
|
|
9
|
+
|
|
10
|
+
Configuration is read from ``[tool.vcs-versioning]``; any keys in the
|
|
11
|
+
``[[tool.dynamic-metadata]]`` table are passed through as overrides.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import vcs_versioning
|
|
19
|
+
import vcs_versioning.overrides
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from collections.abc import Mapping
|
|
23
|
+
|
|
24
|
+
__all__ = ["dynamic_metadata"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def __dir__() -> list[str]:
|
|
28
|
+
return __all__
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def dynamic_metadata(
|
|
32
|
+
settings: Mapping[str, Any],
|
|
33
|
+
project: Mapping[str, Any],
|
|
34
|
+
) -> dict[str, Any]:
|
|
35
|
+
"""Return the ``version`` field for a dynamic-metadata consumer."""
|
|
36
|
+
|
|
37
|
+
dist_name = project.get("name")
|
|
38
|
+
# dynamic-metadata runs hooks with cwd at the project root.
|
|
39
|
+
with vcs_versioning.overrides.GlobalOverrides.from_env(
|
|
40
|
+
"VCS_VERSIONING", dist_name=dist_name
|
|
41
|
+
):
|
|
42
|
+
pyproject = vcs_versioning.PyProjectData.from_file("pyproject.toml")
|
|
43
|
+
version = vcs_versioning.infer_version_string(
|
|
44
|
+
dist_name=dist_name,
|
|
45
|
+
pyproject_data=pyproject,
|
|
46
|
+
overrides=dict(settings) or None,
|
|
47
|
+
force_write_version_files=True,
|
|
48
|
+
)
|
|
49
|
+
return {"version": version}
|