vcs-versioning 2.1.1__tar.gz → 2.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 (98) hide show
  1. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/CHANGELOG.md +17 -0
  2. {vcs_versioning-2.1.1/src/vcs_versioning.egg-info → vcs_versioning-2.2.0}/PKG-INFO +6 -4
  3. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/pyproject.toml +7 -5
  4. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/_git.py +6 -2
  5. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/_hg.py +1 -1
  6. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/_hg_git.py +1 -1
  7. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/_jj.py +1 -1
  8. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/_scm_workdir.py +33 -7
  9. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_cli/__init__.py +9 -2
  10. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_compat.py +32 -4
  11. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_entrypoints.py +3 -2
  12. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_fallback_workdir.py +18 -1
  13. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_file_finders/__init__.py +6 -1
  14. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_file_finders/_git.py +3 -0
  15. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_get_version_impl.py +4 -4
  16. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_legacy_parse.py +11 -10
  17. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_overrides.py +14 -4
  18. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_protocols.py +10 -3
  19. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_pyproject_reading.py +7 -3
  20. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_run_cmd.py +7 -1
  21. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_scm_metadata.py +8 -7
  22. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_scm_version.py +17 -17
  23. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_toml.py +17 -7
  24. vcs_versioning-2.2.0/src/vcs_versioning/_types.py +40 -0
  25. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_cls.py +9 -3
  26. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_worktree_discovery.py +4 -4
  27. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0/src/vcs_versioning.egg-info}/PKG-INFO +6 -4
  28. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning.egg-info/requires.txt +1 -1
  29. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_legacy_parse.py +5 -5
  30. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_overrides_api.py +1 -4
  31. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_workdir_discovery.py +60 -42
  32. vcs_versioning-2.1.1/src/vcs_versioning/_types.py +0 -29
  33. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/LICENSE.txt +0 -0
  34. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/MANIFEST.in +0 -0
  35. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/README.md +0 -0
  36. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/setup.cfg +0 -0
  37. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/setup.py +0 -0
  38. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/__init__.py +0 -0
  39. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/__main__.py +0 -0
  40. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/__init__.py +0 -0
  41. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
  42. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_cli/_args.py +0 -0
  43. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
  44. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
  45. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_config.py +0 -0
  46. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_discover.py +0 -0
  47. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_dump_version.py +0 -0
  48. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_environment.py +0 -0
  49. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_exceptions.py +0 -0
  50. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_fallbacks.py +0 -0
  51. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_file_finders/_hg.py +0 -0
  52. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_file_finders/_jj.py +0 -0
  53. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_integration.py +0 -0
  54. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_integrator_helpers.py +0 -0
  55. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_log.py +0 -0
  56. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_modify_version.py +0 -0
  57. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_node_utils.py +0 -0
  58. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_paths.py +0 -0
  59. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_project_overrides.py +0 -0
  60. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_requirement_cls.py +0 -0
  61. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_test_utils.py +0 -0
  62. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_fields.py +0 -0
  63. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_inference.py +0 -0
  64. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
  65. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_schemes/_common.py +0 -0
  66. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_schemes/_standard.py +0 -0
  67. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
  68. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/overrides.py +0 -0
  69. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/py.typed +0 -0
  70. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning/test_api.py +0 -0
  71. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning.egg-info/SOURCES.txt +0 -0
  72. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
  73. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning.egg-info/entry_points.txt +0 -0
  74. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/src/vcs_versioning.egg-info/top_level.txt +0 -0
  75. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/__init__.py +0 -0
  76. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/conftest.py +0 -0
  77. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_better_root_errors.py +0 -0
  78. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_chain_api.py +0 -0
  79. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_compat.py +0 -0
  80. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_config.py +0 -0
  81. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_expect_parse.py +0 -0
  82. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_file_finders.py +0 -0
  83. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_git.py +0 -0
  84. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_hg_git.py +0 -0
  85. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_integrator_helpers.py +0 -0
  86. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_internal_log_level.py +0 -0
  87. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_jj.py +0 -0
  88. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_mercurial.py +0 -0
  89. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_overrides_env_reader.py +0 -0
  90. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_project_overrides.py +0 -0
  91. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_project_path.py +0 -0
  92. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_regressions.py +0 -0
  93. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_scm_metadata.py +0 -0
  94. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_tag_config.py +0 -0
  95. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_version.py +0 -0
  96. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_version_scheme_towncrier.py +0 -0
  97. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_version_schemes.py +0 -0
  98. {vcs_versioning-2.1.1 → vcs_versioning-2.2.0}/testing_vcs/test_workdir_api.py +0 -0
@@ -2,6 +2,23 @@
2
2
 
3
3
  <!-- towncrier release notes start -->
4
4
 
5
+ ## 2.2.0 (2026-06-24)
6
+
7
+ ### Added
8
+
9
+ - Restore Python 3.8 and 3.9 support, re-enabling use as a build dependency for projects like scikit-build that still support these versions. ([#1445](https://github.com/pypa/setuptools-scm/issues/1445))
10
+
11
+
12
+ ### Miscellaneous
13
+
14
+ - Move PKG-INFO discovery tests to setuptools-scm where the entry points are registered. ([#1446](https://github.com/pypa/setuptools-scm/issues/1446))
15
+
16
+ ## 2.1.2 (2026-06-23)
17
+
18
+ ### Fixed
19
+
20
+ - Fix MetadataWorkdir crash when using custom `tag_regex` — stored tags are already parsed version strings and no longer re-parsed through the tag regex. ([#1439](https://github.com/pypa/setuptools-scm/issues/1439))
21
+
5
22
  ## 2.1.1 (2026-06-23)
6
23
 
7
24
  ### Fixed
@@ -1,26 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: the blessed package to manage your versions by vcs metadata
5
5
  Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
6
- License-Expression: MIT
6
+ License: MIT
7
7
  Project-URL: Documentation, https://github.com/pypa/setuptools-scm#readme
8
8
  Project-URL: Issues, https://github.com/pypa/setuptools-scm/issues
9
9
  Project-URL: Source, https://github.com/pypa/setuptools-scm
10
10
  Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
13
15
  Classifier: Programming Language :: Python :: 3.10
14
16
  Classifier: Programming Language :: Python :: 3.11
15
17
  Classifier: Programming Language :: Python :: 3.12
16
18
  Classifier: Programming Language :: Python :: 3.13
17
19
  Classifier: Programming Language :: Python :: 3.14
18
- Requires-Python: >=3.10
20
+ Requires-Python: >=3.8
19
21
  Description-Content-Type: text/markdown
20
22
  License-File: LICENSE.txt
21
23
  Requires-Dist: packaging>=20
22
24
  Requires-Dist: tomli>=1; python_version < "3.11"
23
- Requires-Dist: typing-extensions; python_version < "3.11"
25
+ Requires-Dist: typing-extensions>=4.1; python_version < "3.11"
24
26
  Dynamic: license-file
25
27
 
26
28
  # vcs-versioning
@@ -6,9 +6,9 @@
6
6
  [build-system]
7
7
  build-backend = "setuptools.build_meta"
8
8
  requires = [
9
- "setuptools>=77.0.3",
9
+ "setuptools>=45",
10
10
  "packaging>=20",
11
- 'typing-extensions; python_version < "3.11"',
11
+ 'typing-extensions>=4.1; python_version < "3.11"',
12
12
  ]
13
13
 
14
14
  [project]
@@ -17,15 +17,17 @@ description = "the blessed package to manage your versions by vcs metadata"
17
17
  readme = "README.md"
18
18
  keywords = [
19
19
  ]
20
- license = "MIT"
20
+ license = {text = "MIT"}
21
21
  authors = [
22
22
  { name = "Ronny Pfannschmidt", email = "opensource@ronnypfannschmidt.de" },
23
23
  ]
24
- requires-python = ">=3.10"
24
+ requires-python = ">=3.8"
25
25
  classifiers = [
26
26
  "Development Status :: 4 - Beta",
27
27
  "Programming Language :: Python",
28
28
  "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.8",
30
+ "Programming Language :: Python :: 3.9",
29
31
  "Programming Language :: Python :: 3.10",
30
32
  "Programming Language :: Python :: 3.11",
31
33
  "Programming Language :: Python :: 3.12",
@@ -38,7 +40,7 @@ dynamic = [
38
40
  dependencies = [
39
41
  "packaging>=20",
40
42
  'tomli>=1; python_version < "3.11"',
41
- 'typing-extensions; python_version < "3.11"',
43
+ 'typing-extensions>=4.1; python_version < "3.11"',
42
44
  ]
43
45
 
44
46
  [dependency-groups]
@@ -238,11 +238,15 @@ class GitWorkdir(Workdir):
238
238
  return _git_parse_inner(config, self, pre_parse=effective_pre_parse)
239
239
 
240
240
  def list_tracked_files(self, path: Path | str = "") -> list[str]:
241
- """List files tracked by git, honoring export-ignore."""
241
+ """List files tracked by git, honoring export-ignore.
242
+
243
+ When no path is given, scopes to ``project_root`` (not the VCS root)
244
+ so that monorepo projects only list their own files.
245
+ """
242
246
  from .._file_finders import scm_find_files
243
247
  from .._file_finders._git import _git_ls_files_and_dirs
244
248
 
245
- base = str(path) if path else str(self.path)
249
+ base = str(path) if path else str(self.project_root)
246
250
  git_files, git_dirs = _git_ls_files_and_dirs(
247
251
  str(self.path), timeout=self._subprocess_timeout
248
252
  )
@@ -293,7 +293,7 @@ class HgWorkdir(Workdir):
293
293
  from .._file_finders import scm_find_files
294
294
  from .._file_finders._hg import _hg_ls_files_and_dirs
295
295
 
296
- base = str(path) if path else str(self.path)
296
+ base = str(path) if path else str(self.project_root)
297
297
  hg_files, hg_dirs = _hg_ls_files_and_dirs(
298
298
  str(self.path),
299
299
  hg_command=self._hg_command,
@@ -95,7 +95,7 @@ class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
95
95
  from .._file_finders import scm_find_files
96
96
  from .._file_finders._hg import _hg_ls_files_and_dirs
97
97
 
98
- base = str(path) if path else str(self.path)
98
+ base = str(path) if path else str(self.project_root)
99
99
  hg_files, hg_dirs = _hg_ls_files_and_dirs(
100
100
  str(self.path),
101
101
  hg_command=self._hg_command,
@@ -263,7 +263,7 @@ class JjWorkdir(Workdir):
263
263
  from .._file_finders import scm_find_files
264
264
  from .._file_finders._jj import _jj_ls_files_and_dirs
265
265
 
266
- base = str(path) if path else str(self.path)
266
+ base = str(path) if path else str(self.project_root)
267
267
  jj_files, jj_dirs = _jj_ls_files_and_dirs(
268
268
  str(self.path), timeout=self._subprocess_timeout
269
269
  )
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  from dataclasses import field as dc_field
6
6
  from datetime import date, datetime, timezone
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING
8
+ from typing import TYPE_CHECKING, overload
9
9
 
10
10
  from .._scm_version import ScmVersion
11
11
 
@@ -16,6 +16,37 @@ if TYPE_CHECKING:
16
16
  log = logging.getLogger(__name__)
17
17
 
18
18
 
19
+ class _ProjectRootDescriptor:
20
+ """Descriptor for ``project_root`` that defaults to ``path``.
21
+
22
+ Acts as default when no value has been set on the instance.
23
+ Stores explicitly assigned values in the instance ``__dict__``.
24
+ """
25
+
26
+ def __set_name__(self, owner: type, name: str) -> None:
27
+ self._name = name
28
+
29
+ @overload
30
+ def __get__(self, obj: None, objtype: type) -> _ProjectRootDescriptor: ...
31
+
32
+ @overload
33
+ def __get__(self, obj: ScmWorkdir, objtype: type | None = None) -> Path: ...
34
+
35
+ def __get__(
36
+ self, obj: ScmWorkdir | None, objtype: type | None = None
37
+ ) -> Path | _ProjectRootDescriptor:
38
+ if obj is None:
39
+ return self
40
+ value: Path | None = obj.__dict__.get(self._name)
41
+ if value is None:
42
+ return obj.path
43
+ return value
44
+
45
+ def __set__(self, obj: ScmWorkdir, value: Path | None) -> None:
46
+ if isinstance(value, Path):
47
+ obj.__dict__[self._name] = value
48
+
49
+
19
50
  def get_latest_file_mtime(changed_files: list[str], base_path: Path) -> date | None:
20
51
  """Get the latest modification time of the given files.
21
52
 
@@ -61,15 +92,11 @@ class ScmWorkdir:
61
92
  """
62
93
 
63
94
  path: Path
64
- project_root: Path | None = dc_field(default=None)
95
+ project_root: Path = _ProjectRootDescriptor() # type: ignore[assignment]
65
96
 
66
97
  _config: Configuration | None = dc_field(default=None, repr=False, compare=False)
67
98
  """Back-reference to the ``Configuration`` that discovered this workdir."""
68
99
 
69
- def __post_init__(self) -> None:
70
- if self.project_root is None:
71
- self.project_root = self.path
72
-
73
100
  @property
74
101
  def _subprocess_timeout(self) -> int | None:
75
102
  """Subprocess timeout from ``config.env``.
@@ -95,7 +122,6 @@ class ScmWorkdir:
95
122
  @property
96
123
  def project_path(self) -> str:
97
124
  """Discovered relative path from VCS root to project directory."""
98
- assert self.project_root is not None
99
125
  if self.path == self.project_root:
100
126
  return ""
101
127
  from .._paths import relative_project_path
@@ -4,7 +4,11 @@ import json
4
4
  import os
5
5
  import sys
6
6
  from collections.abc import Iterable
7
- from importlib.resources import files
7
+
8
+ if sys.version_info >= (3, 9):
9
+ from importlib.resources import files
10
+ else:
11
+ from importlib.resources import read_text as _read_text
8
12
  from pathlib import Path
9
13
  from typing import TypedDict
10
14
 
@@ -182,7 +186,10 @@ def _create_archival_file(opts: CliNamespace, config: Configuration) -> int:
182
186
  assert opts.archival_template is not None
183
187
 
184
188
  # Load template content from package resources
185
- content = files(__package__).joinpath(opts.archival_template).read_text("utf-8")
189
+ if sys.version_info >= (3, 9):
190
+ content = files(__package__).joinpath(opts.archival_template).read_text("utf-8")
191
+ else:
192
+ content = _read_text(__package__, opts.archival_template, encoding="utf-8")
186
193
 
187
194
  # Print appropriate message based on template
188
195
  if opts.archival_template == "git_archival_stable.txt":
@@ -3,10 +3,38 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
- from typing import TypeAlias
7
-
8
- # Path type for accepting both strings and PathLike objects
9
- PathT: TypeAlias = os.PathLike[str] | str
6
+ import sys
7
+ from importlib.metadata import entry_points as _stdlib_entry_points
8
+ from typing import TYPE_CHECKING, Union
9
+
10
+ if sys.version_info >= (3, 10):
11
+ from typing import TypeAlias
12
+ else:
13
+ from typing_extensions import TypeAlias
14
+
15
+ if TYPE_CHECKING:
16
+ from importlib.metadata import EntryPoint
17
+
18
+ PathT: TypeAlias = os.PathLike[str] | str
19
+ else:
20
+ PathT: TypeAlias = Union[os.PathLike, str]
21
+
22
+
23
+ if sys.version_info >= (3, 10):
24
+ from importlib.metadata import entry_points as entry_points
25
+ else:
26
+
27
+ def entry_points(
28
+ **params: str,
29
+ ) -> list[EntryPoint]:
30
+ """Backport of entry_points(group=...) for Python 3.8/3.9."""
31
+ groups = _stdlib_entry_points()
32
+ group = params.get("group", "")
33
+ name = params.get("name")
34
+ eps = list(groups.get(group, [])) # type: ignore[call-overload]
35
+ if name is not None:
36
+ eps = [ep for ep in eps if ep.name == name]
37
+ return eps
10
38
 
11
39
 
12
40
  def normalize_path_for_assertion(path: str) -> str:
@@ -3,9 +3,10 @@ from __future__ import annotations
3
3
  import logging
4
4
  from collections.abc import Callable, Iterator
5
5
  from importlib import metadata as im
6
- from importlib.metadata import entry_points
7
6
  from typing import TYPE_CHECKING, Any, cast
8
7
 
8
+ from ._compat import entry_points
9
+
9
10
  __all__ = [
10
11
  "entry_points",
11
12
  "im",
@@ -65,7 +66,7 @@ def _iter_version_schemes(
65
66
  or _get_from_object_reference_str(scheme_value, entrypoint),
66
67
  )
67
68
 
68
- if isinstance(scheme_value, list | tuple):
69
+ if isinstance(scheme_value, (list, tuple)):
69
70
  for variant in scheme_value:
70
71
  if variant not in _memo:
71
72
  _memo.add(variant)
@@ -34,6 +34,11 @@ class FallbackWorkdir:
34
34
  _config: Configuration | None = dc_field(default=None, repr=False, compare=False)
35
35
  """Back-reference to the ``Configuration`` that discovered this workdir."""
36
36
 
37
+ @property
38
+ def project_root(self) -> Path:
39
+ """Fallback workdirs always live at the project root."""
40
+ return self.path
41
+
37
42
  @property
38
43
  def config(self) -> Configuration:
39
44
  if self._config is None:
@@ -76,8 +81,20 @@ class MetadataWorkdir(FallbackWorkdir):
76
81
  data.node_date,
77
82
  self.metadata_dir,
78
83
  )
84
+ # The tag in scm_version.json is already a parsed version string
85
+ # (e.g. "1.5.5"), not a raw VCS tag (e.g. "cuda-pathfinder-v1.5.5").
86
+ # Convert to version_cls so _parse_tag skips tag_regex matching.
87
+ try:
88
+ tag = self.config.version_cls(data.tag)
89
+ except Exception:
90
+ log.warning(
91
+ "cannot parse stored tag %r in metadata at %s",
92
+ data.tag,
93
+ self.metadata_dir,
94
+ )
95
+ return None
79
96
  return meta(
80
- tag=data.tag,
97
+ tag=tag,
81
98
  distance=data.distance,
82
99
  node=data.node,
83
100
  dirty=data.dirty,
@@ -2,8 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import os
5
+ import sys
5
6
  from collections.abc import Callable, Mapping
6
- from typing import TypeGuard
7
+
8
+ if sys.version_info >= (3, 10):
9
+ from typing import TypeGuard
10
+ else:
11
+ from typing_extensions import TypeGuard
7
12
 
8
13
  from .. import _types as _t
9
14
  from .._compat import norm_real
@@ -85,6 +85,9 @@ def _git_ls_files_and_dirs(
85
85
  log.error("listing git files failed - pretending there aren't any")
86
86
  return set(), set()
87
87
 
88
+ # Normalize toplevel to match what scm_find_files expects:
89
+ # absolute, symlinks resolved, case-normalized (normcase).
90
+ toplevel = norm_real(toplevel)
88
91
  git_files: set[str] = set()
89
92
  git_dirs: set[str] = {toplevel}
90
93
  for name in res.stdout.rstrip("\0").split("\0"):
@@ -90,7 +90,7 @@ def _resolve_version(config: Configuration) -> ScmVersion | None:
90
90
  return None
91
91
 
92
92
 
93
- def _warn_if_tracked(target: Path, root: Path) -> None:
93
+ def _warn_if_tracked(target: Path, root: Path, config: Configuration) -> None:
94
94
  """Warn when *target* is tracked in version control (#468).
95
95
 
96
96
  Writing a version file that is tracked makes ``git describe --dirty``
@@ -101,7 +101,7 @@ def _warn_if_tracked(target: Path, root: Path) -> None:
101
101
 
102
102
  for workdir_cls in (GitWorkdir, HgWorkdir):
103
103
  try:
104
- wd = workdir_cls.from_potential_worktree(root)
104
+ wd = workdir_cls.from_potential_worktree(root, config)
105
105
  except Exception:
106
106
  continue
107
107
  if wd is not None and wd.is_file_tracked(target):
@@ -125,7 +125,7 @@ def write_version_files(
125
125
 
126
126
  write_to = Path(config.write_to)
127
127
  target = root / write_to if not write_to.is_absolute() else write_to
128
- _warn_if_tracked(target, root)
128
+ _warn_if_tracked(target, root, config)
129
129
 
130
130
  dump_version(
131
131
  root=config.root,
@@ -142,7 +142,7 @@ def write_version_files(
142
142
  # todo: use a better name than fallback root
143
143
  assert config.relative_to is not None
144
144
  target = Path(config.relative_to).parent.joinpath(version_file)
145
- _warn_if_tracked(target, root)
145
+ _warn_if_tracked(target, root, config)
146
146
 
147
147
  write_version_to_path(
148
148
  target,
@@ -13,8 +13,6 @@ from __future__ import annotations
13
13
 
14
14
  import logging
15
15
  import warnings
16
- from dataclasses import dataclass
17
- from dataclasses import field as dc_field
18
16
  from pathlib import Path
19
17
  from typing import TYPE_CHECKING
20
18
 
@@ -29,7 +27,6 @@ if TYPE_CHECKING:
29
27
  log = logging.getLogger(__name__)
30
28
 
31
29
 
32
- @dataclass()
33
30
  class LegacyParseWorkdir(ScmWorkdir):
34
31
  """Wraps a legacy ``config.parse`` callable as a workdir.
35
32
 
@@ -38,12 +35,17 @@ class LegacyParseWorkdir(ScmWorkdir):
38
35
  entry-point group.
39
36
  """
40
37
 
41
- _parse_fn: ParseFunction | None = dc_field(default=None, repr=False)
38
+ parse_fn: ParseFunction
42
39
 
43
- def __post_init__(self) -> None:
44
- super().__post_init__()
45
- if self._parse_fn is None:
46
- raise TypeError("LegacyParseWorkdir requires a _parse_fn")
40
+ def __init__(
41
+ self,
42
+ path: Path,
43
+ *,
44
+ parse_fn: ParseFunction,
45
+ _config: Configuration | None = None,
46
+ ) -> None:
47
+ super().__init__(path=path, _config=_config)
48
+ self.parse_fn = parse_fn
47
49
 
48
50
  def get_scm_version(self) -> ScmVersion | None:
49
51
  warnings.warn(
@@ -53,8 +55,7 @@ class LegacyParseWorkdir(ScmWorkdir):
53
55
  DeprecationWarning,
54
56
  stacklevel=2,
55
57
  )
56
- assert self._parse_fn is not None
57
- result = self._parse_fn(self.config.absolute_root, config=self.config)
58
+ result = self.parse_fn(self.config.absolute_root, config=self.config)
58
59
  if result is not None and not isinstance(result, ScmVersion):
59
60
  raise TypeError(
60
61
  f"version parse result was {result!r}\n"
@@ -207,15 +207,25 @@ def _apply_metadata_overrides(
207
207
 
208
208
  log.info("Applying metadata overrides: %s", metadata_overrides)
209
209
 
210
- # Get type hints from PretendMetadataDict for validation
211
- field_types = get_type_hints(PretendMetadataDict)
210
+ # Get valid field names from PretendMetadataDict for validation.
211
+ # Use __annotations__ keys directly instead of get_type_hints() to avoid
212
+ # evaluating PEP 604 forward references on Python <3.10.
213
+ valid_fields = frozenset(PretendMetadataDict.__annotations__)
214
+
215
+ # Try to resolve actual types for runtime validation (best-effort)
216
+ try:
217
+ field_types: dict[str, Any] | None = get_type_hints(PretendMetadataDict)
218
+ except TypeError:
219
+ field_types = None
212
220
 
213
221
  # Apply each override individually using dataclasses.replace
214
222
  result = scm_version
215
223
 
216
224
  for field, value in metadata_overrides.items():
217
- # Validate field types using the TypedDict annotations
218
- if field in field_types:
225
+ if field not in valid_fields:
226
+ continue
227
+ # Runtime type validation (only when type hints are resolvable)
228
+ if field_types is not None and field in field_types:
219
229
  expected_type = field_types[field]
220
230
  # Handle Optional/Union types (e.g., str | None)
221
231
  if hasattr(expected_type, "__args__"):
@@ -32,6 +32,16 @@ class WorkdirProtocol(Protocol):
32
32
  @property
33
33
  def path(self) -> Path: ...
34
34
 
35
+ @property
36
+ def project_root(self) -> Path:
37
+ """The project directory (where pyproject.toml lives).
38
+
39
+ For SCM workdirs this may differ from ``path`` (the VCS root).
40
+ For fallback workdirs this is always equal to ``path``.
41
+ Guaranteed non-None after construction.
42
+ """
43
+ ...
44
+
35
45
  def get_scm_version(self) -> ScmVersion | None:
36
46
  """Raw version metadata before overrides / formatting."""
37
47
  ...
@@ -49,9 +59,6 @@ class WorkdirProtocol(Protocol):
49
59
  class ScmWorkdirProtocol(WorkdirProtocol, Protocol):
50
60
  """Live VCS checkout: adds monorepo / nested-project fields."""
51
61
 
52
- @property
53
- def project_root(self) -> Path: ...
54
-
55
62
  @property
56
63
  def project_path(self) -> str:
57
64
  """POSIX relative path from VCS root to project_root (empty when equal)."""
@@ -9,7 +9,11 @@ import warnings
9
9
  from collections.abc import Sequence
10
10
  from dataclasses import dataclass
11
11
  from pathlib import Path
12
- from typing import TypeAlias
12
+
13
+ if sys.version_info >= (3, 10):
14
+ from typing import TypeAlias
15
+ else:
16
+ from typing_extensions import TypeAlias
13
17
 
14
18
  if sys.version_info >= (3, 11):
15
19
  from typing import Self
@@ -158,7 +162,7 @@ class PyProjectData:
158
162
 
159
163
  # Testing injection type for configuration reading
160
164
  GivenPyProjectResult: TypeAlias = (
161
- PyProjectData | InvalidTomlError | FileNotFoundError | None
165
+ "PyProjectData | InvalidTomlError | FileNotFoundError | None"
162
166
  )
163
167
 
164
168
 
@@ -201,7 +205,7 @@ def read_pyproject(
201
205
  if _given_result is not None:
202
206
  if isinstance(_given_result, PyProjectData):
203
207
  return _given_result
204
- if isinstance(_given_result, InvalidTomlError | FileNotFoundError):
208
+ if isinstance(_given_result, (InvalidTomlError, FileNotFoundError)):
205
209
  raise _given_result
206
210
 
207
211
  if _given_definition is not None:
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import shlex
6
6
  import subprocess
7
+ import sys
7
8
  import textwrap
8
9
  import warnings
9
10
  from collections.abc import Callable, Mapping, Sequence
@@ -35,8 +36,13 @@ def _get_timeout(env: Mapping[str, str]) -> int:
35
36
  PARSE_RESULT = TypeVar("PARSE_RESULT")
36
37
  T = TypeVar("T")
37
38
 
39
+ if sys.version_info >= (3, 9):
40
+ _CompletedProcessBase = subprocess.CompletedProcess[str]
41
+ else:
42
+ _CompletedProcessBase = subprocess.CompletedProcess
38
43
 
39
- class CompletedProcess(subprocess.CompletedProcess[str]):
44
+
45
+ class CompletedProcess(_CompletedProcessBase):
40
46
  @classmethod
41
47
  def from_raw(
42
48
  cls, input: subprocess.CompletedProcess[str], strip: bool = True
@@ -99,13 +99,14 @@ def scm_version_data_from_scm_version(
99
99
  scm_version: ScmVersion | VersionFields,
100
100
  ) -> ScmVersionData:
101
101
  """Build ``ScmVersionData`` from a live ``ScmVersion`` object."""
102
- match scm_version.node_date:
103
- case date() as d:
104
- node_date_str: str | None = d.isoformat()
105
- case str() as s:
106
- node_date_str = s
107
- case _:
108
- node_date_str = None
102
+ node_date = scm_version.node_date
103
+ node_date_str: str | None
104
+ if isinstance(node_date, date):
105
+ node_date_str = node_date.isoformat()
106
+ elif isinstance(node_date, str):
107
+ node_date_str = node_date
108
+ else:
109
+ node_date_str = None
109
110
  return ScmVersionData(
110
111
  tag=str(scm_version.tag),
111
112
  distance=scm_version.distance,
@@ -9,10 +9,16 @@ from __future__ import annotations
9
9
 
10
10
  import dataclasses
11
11
  import logging
12
+ import sys
12
13
  import warnings
13
14
  from collections.abc import Callable
14
15
  from datetime import date, datetime
15
- from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypedDict
16
+ from typing import TYPE_CHECKING, Any, TypedDict
17
+
18
+ if sys.version_info >= (3, 10):
19
+ from typing import Concatenate, ParamSpec
20
+ else:
21
+ from typing_extensions import Concatenate, ParamSpec
16
22
 
17
23
  from . import _config
18
24
  from . import _version_cls as _v
@@ -20,8 +26,6 @@ from ._node_utils import _format_node_for_output
20
26
  from ._version_cls import _Version
21
27
 
22
28
  if TYPE_CHECKING:
23
- import sys
24
-
25
29
  if sys.version_info >= (3, 11):
26
30
  from typing import Unpack
27
31
  else:
@@ -347,20 +351,14 @@ class ScmVersion:
347
351
 
348
352
  def _parse_tag(
349
353
  tag: _Version | str, preformatted: bool, config: _config.Configuration
350
- ) -> _Version:
354
+ ) -> _Version | None:
351
355
  if preformatted:
352
- # For preformatted versions, tag should already be validated as a version object
353
- # String validation is handled in meta function before calling this
354
356
  if isinstance(tag, str):
355
- # This should not happen with enhanced meta, but kept for safety
356
357
  return _v.NonNormalizedVersion(tag)
357
358
  else:
358
- # Already a version object (including test mocks), return as-is
359
359
  return tag
360
360
  elif not isinstance(tag, config.version_cls):
361
- version = tag_to_version(tag, config)
362
- assert version is not None
363
- return version
361
+ return tag_to_version(tag, config)
364
362
  else:
365
363
  return tag
366
364
 
@@ -389,18 +387,20 @@ def meta(
389
387
  node_date: date | None = None,
390
388
  time: datetime | None = None,
391
389
  ) -> ScmVersion:
392
- parsed_version: _Version
393
- # Enhanced string validation for preformatted versions
390
+ parsed_version: _Version | None
394
391
  if preformatted and isinstance(tag, str):
395
- # Validate PEP 440 compliance using NonNormalizedVersion
396
- # Let validation errors bubble up to the caller
397
392
  parsed_version = _v.NonNormalizedVersion(tag)
398
393
  else:
399
- # Use existing _parse_tag logic for non-preformatted or already validated inputs
400
394
  parsed_version = _parse_tag(tag, preformatted, config)
401
395
 
396
+ if parsed_version is None:
397
+ raise ValueError(
398
+ f"Can't parse version from tag {tag!r}"
399
+ f" (tag_regex={config.tag.regex.pattern!r},"
400
+ f" tag_prefix={config.tag.prefix!r})"
401
+ )
402
+
402
403
  log.info("version %s -> %s", tag, parsed_version)
403
- assert parsed_version is not None, f"Can't parse version {tag}"
404
404
 
405
405
  kwargs: _ScmVersionKwargs = {
406
406
  "distance": distance,