vcs-versioning 2.1.2__tar.gz → 2.2.1__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 (99) hide show
  1. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/CHANGELOG.md +20 -0
  2. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/MANIFEST.in +1 -0
  3. {vcs_versioning-2.1.2/src/vcs_versioning.egg-info → vcs_versioning-2.2.1}/PKG-INFO +6 -4
  4. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/pyproject.toml +26 -23
  5. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/_hg.py +0 -3
  6. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_cli/__init__.py +9 -2
  7. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_compat.py +31 -16
  8. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_config.py +0 -3
  9. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_entrypoints.py +3 -2
  10. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_file_finders/__init__.py +39 -3
  11. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_file_finders/_git.py +2 -16
  12. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_file_finders/_hg.py +2 -12
  13. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_file_finders/_jj.py +2 -14
  14. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_get_version_impl.py +32 -8
  15. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_legacy_parse.py +11 -4
  16. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_overrides.py +15 -5
  17. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_pyproject_reading.py +7 -3
  18. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_run_cmd.py +7 -3
  19. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_scm_metadata.py +8 -7
  20. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_scm_version.py +9 -9
  21. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_toml.py +17 -7
  22. vcs_versioning-2.2.1/src/vcs_versioning/_types.py +40 -0
  23. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_cls.py +9 -3
  24. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_schemes/_standard.py +0 -4
  25. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_worktree_discovery.py +3 -3
  26. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1/src/vcs_versioning.egg-info}/PKG-INFO +6 -4
  27. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning.egg-info/SOURCES.txt +1 -0
  28. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning.egg-info/requires.txt +1 -1
  29. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_file_finders.py +1 -1
  30. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_overrides_api.py +1 -4
  31. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_regressions.py +35 -5
  32. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_tag_config.py +46 -0
  33. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_workdir_discovery.py +0 -45
  34. vcs_versioning-2.2.1/uv.lock +314 -0
  35. vcs_versioning-2.1.2/src/vcs_versioning/_types.py +0 -29
  36. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/LICENSE.txt +0 -0
  37. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/README.md +0 -0
  38. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/setup.cfg +0 -0
  39. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/setup.py +0 -0
  40. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/__init__.py +0 -0
  41. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/__main__.py +0 -0
  42. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/__init__.py +0 -0
  43. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
  44. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/_git.py +0 -0
  45. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/_hg_git.py +0 -0
  46. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/_jj.py +0 -0
  47. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_backends/_scm_workdir.py +0 -0
  48. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_cli/_args.py +0 -0
  49. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
  50. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
  51. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_discover.py +0 -0
  52. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_dump_version.py +0 -0
  53. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_environment.py +0 -0
  54. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_exceptions.py +0 -0
  55. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_fallback_workdir.py +0 -0
  56. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_fallbacks.py +0 -0
  57. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_integration.py +0 -0
  58. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_integrator_helpers.py +0 -0
  59. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_log.py +0 -0
  60. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_modify_version.py +0 -0
  61. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_node_utils.py +0 -0
  62. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_paths.py +0 -0
  63. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_project_overrides.py +0 -0
  64. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_protocols.py +0 -0
  65. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_requirement_cls.py +0 -0
  66. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_test_utils.py +0 -0
  67. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_fields.py +0 -0
  68. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_inference.py +0 -0
  69. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
  70. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_schemes/_common.py +0 -0
  71. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
  72. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/overrides.py +0 -0
  73. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/py.typed +0 -0
  74. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning/test_api.py +0 -0
  75. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
  76. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning.egg-info/entry_points.txt +0 -0
  77. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/src/vcs_versioning.egg-info/top_level.txt +0 -0
  78. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/__init__.py +0 -0
  79. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/conftest.py +0 -0
  80. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_better_root_errors.py +0 -0
  81. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_chain_api.py +0 -0
  82. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_compat.py +0 -0
  83. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_config.py +0 -0
  84. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_expect_parse.py +0 -0
  85. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_git.py +0 -0
  86. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_hg_git.py +0 -0
  87. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_integrator_helpers.py +0 -0
  88. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_internal_log_level.py +0 -0
  89. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_jj.py +0 -0
  90. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_legacy_parse.py +0 -0
  91. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_mercurial.py +0 -0
  92. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_overrides_env_reader.py +0 -0
  93. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_project_overrides.py +0 -0
  94. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_project_path.py +0 -0
  95. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_scm_metadata.py +0 -0
  96. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_version.py +0 -0
  97. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_version_scheme_towncrier.py +0 -0
  98. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_version_schemes.py +0 -0
  99. {vcs_versioning-2.1.2 → vcs_versioning-2.2.1}/testing_vcs/test_workdir_api.py +0 -0
@@ -2,11 +2,31 @@
2
2
 
3
3
  <!-- towncrier release notes start -->
4
4
 
5
+ ## 2.2.1 (2026-06-29)
6
+
7
+ ### Fixed
8
+
9
+ - Fix crash in `_warn_if_tracked` when the version file target is a relative path
10
+ by resolving it against the project root before comparison.
11
+ Also warn (instead of crashing) when the target resolves outside the project root. ([#1451](https://github.com/pypa/setuptools-scm/issues/1451))
12
+
13
+ ## 2.2.0 (2026-06-24)
14
+
15
+ ### Added
16
+
17
+ - 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))
18
+
19
+
20
+ ### Miscellaneous
21
+
22
+ - Move PKG-INFO discovery tests to setuptools-scm where the entry points are registered. ([#1446](https://github.com/pypa/setuptools-scm/issues/1446))
23
+
5
24
  ## 2.1.2 (2026-06-23)
6
25
 
7
26
  ### Fixed
8
27
 
9
28
  - 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))
29
+ - Scope `list_tracked_files` to `project_root` in monorepo setups so that sdists no longer include files from sibling projects when `root=".."` is configured. ([#1440](https://github.com/pypa/setuptools-scm/issues/1440))
10
30
 
11
31
  ## 2.1.1 (2026-06-23)
12
32
 
@@ -9,5 +9,6 @@ include *.toml
9
9
  include LICENSE.txt
10
10
  include README.md
11
11
  include CHANGELOG.md
12
+ include uv.lock
12
13
 
13
14
  prune .cursor
@@ -1,26 +1,28 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.1.2
3
+ Version: 2.2.1
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,13 +40,12 @@ 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]
45
47
  test = [
46
48
  "pytest",
47
- "pytest-cov",
48
49
  "pytest-xdist",
49
50
  ]
50
51
 
@@ -103,27 +104,29 @@ tag_regex = "^vcs-versioning-(?P<version>v?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)
103
104
  fallback_version = "0.1.0"
104
105
  scm.git.describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "vcs-versioning-*"]
105
106
 
106
- [tool.coverage.run]
107
- branch = true
108
- parallel = true
109
- omit = [
110
- "vcs_versioning/__about__.py",
111
- ]
112
-
113
- [tool.coverage.report]
114
- exclude_lines = [
115
- "no cov",
116
- "if __name__ == .__main__.:",
117
- "if TYPE_CHECKING:",
118
- ]
119
-
120
107
  [tool.pytest.ini_options]
108
+ minversion = "8.2"
121
109
  testpaths = ["testing_vcs"]
122
110
  python_files = ["test_*.py"]
123
- addopts = ["-ra", "--strict-markers", "-p", "vcs_versioning.test_api"]
111
+ xfail_strict = true
112
+ addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "vcs_versioning.test_api"]
113
+ timeout = 300
114
+ filterwarnings = [
115
+ "error",
116
+ "ignore:.*tool\\.setuptools_scm.*",
117
+ "ignore:.*git archive did not support describe output.*:UserWarning",
118
+ "ignore:.*unprocessed git archival found.*:UserWarning",
119
+ "ignore:.*without VcsEnvironment.*:DeprecationWarning",
120
+ "ignore:.*Passing config to workdir.*:DeprecationWarning",
121
+ "ignore:.*tag_regex.*is deprecated.*:DeprecationWarning",
122
+ "ignore:.*write_to_source.*:DeprecationWarning",
123
+ "ignore:.*relative_to is expected to be a file.*:UserWarning",
124
+ ]
125
+ log_level = "debug"
126
+ log_cli_level = "info"
124
127
  markers = [
125
- "issue: marks tests related to specific issues",
126
- "no_init: use setup_hg(init=False) for wd fixture - hg check without worktree",
128
+ "issue(id): reference to github issue",
129
+ "no_init: skip initializing the VCS repository in the test fixture",
127
130
  "skip_commit: allows to skip committing in the helpers",
128
131
  ]
129
132
 
@@ -22,9 +22,6 @@ log = logging.getLogger(__name__)
22
22
  _HG_PSEUDO_TAGS = frozenset({"tip", "qbase", "qtip", "qparent"})
23
23
 
24
24
 
25
- _DEFAULT_HG_COMMAND = "hg"
26
-
27
-
28
25
  def _get_hg_command() -> str:
29
26
  """Read the hg command from resolved runtime settings.
30
27
 
@@ -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
6
+ import sys
7
+ from importlib.metadata import entry_points as _stdlib_entry_points
8
+ from typing import TYPE_CHECKING, Union
7
9
 
8
- # Path type for accepting both strings and PathLike objects
9
- PathT: TypeAlias = os.PathLike[str] | str
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:
@@ -58,19 +86,6 @@ def strip_path_suffix(
58
86
  return full_path[: -len(suffix_path)]
59
87
 
60
88
 
61
- # Legacy aliases for backward compatibility during transition
62
- def assert_path_endswith(
63
- full_path: str, suffix_path: str, error_msg: str | None = None
64
- ) -> None:
65
- """Legacy alias - use strip_path_suffix instead."""
66
- strip_path_suffix(full_path, suffix_path, error_msg)
67
-
68
-
69
- def compute_path_prefix(full_path: str, suffix_path: str) -> str:
70
- """Legacy alias - use strip_path_suffix instead."""
71
- return strip_path_suffix(full_path, suffix_path)
72
-
73
-
74
89
  def norm_real(path: PathT) -> str:
75
90
  """Normalize and resolve a path (combining normcase and realpath).
76
91
 
@@ -203,9 +203,6 @@ class TagConfiguration:
203
203
  return cls(**tag_data)
204
204
 
205
205
 
206
- _SENTINEL_TAG_CONFIG = TagConfiguration()
207
-
208
-
209
206
  class _TagRegexDescriptor:
210
207
  """Data descriptor for deprecated top-level tag_regex field.
211
208
 
@@ -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)
@@ -2,8 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import logging
4
4
  import os
5
- from collections.abc import Callable, Mapping
6
- from typing import TypeGuard
5
+ import sys
6
+ from collections.abc import Callable, Iterable, Mapping
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
@@ -135,4 +140,35 @@ def find_files(path: _t.PathT = "") -> list[str]:
135
140
  return []
136
141
 
137
142
 
138
- __all__ = ["scm_find_files", "is_toplevel_acceptable", "find_files"]
143
+ def collect_files_and_dirs(
144
+ raw_names: Iterable[str], toplevel: str
145
+ ) -> tuple[set[str], set[str]]:
146
+ """Normalize VCS file listings into absolute ``(files, dirs)`` sets.
147
+
148
+ Each backend produces a list of relative paths from its own command
149
+ (``git ls-files``, ``hg files``, ``jj file list``). This helper
150
+ normalizes case and separators, joins with *toplevel*, and collects
151
+ the directory ancestry — the same loop that was previously duplicated
152
+ in every backend.
153
+ """
154
+ files: set[str] = set()
155
+ dirs: set[str] = {toplevel}
156
+ for name in raw_names:
157
+ if not name:
158
+ continue
159
+ name = os.path.normcase(name).replace("/", os.path.sep)
160
+ fullname = os.path.join(toplevel, name)
161
+ files.add(fullname)
162
+ dirname = os.path.dirname(fullname)
163
+ while len(dirname) > len(toplevel) and dirname not in dirs:
164
+ dirs.add(dirname)
165
+ dirname = os.path.dirname(dirname)
166
+ return files, dirs
167
+
168
+
169
+ __all__ = [
170
+ "scm_find_files",
171
+ "is_toplevel_acceptable",
172
+ "find_files",
173
+ "collect_files_and_dirs",
174
+ ]
@@ -10,7 +10,7 @@ from .._backends._git import run_git
10
10
  from .._compat import norm_real, strip_path_suffix
11
11
  from .._integration import data_from_mime
12
12
  from .._run_cmd import run as _run
13
- from . import is_toplevel_acceptable, scm_find_files
13
+ from . import collect_files_and_dirs, is_toplevel_acceptable, scm_find_files
14
14
 
15
15
  log = logging.getLogger(__name__)
16
16
 
@@ -85,22 +85,8 @@ 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
88
  toplevel = norm_real(toplevel)
91
- git_files: set[str] = set()
92
- git_dirs: set[str] = {toplevel}
93
- for name in res.stdout.rstrip("\0").split("\0"):
94
- if not name:
95
- continue
96
- name = os.path.normcase(name).replace("/", os.path.sep)
97
- fullname = os.path.join(toplevel, name)
98
- git_files.add(fullname)
99
- dirname = os.path.dirname(fullname)
100
- while len(dirname) > len(toplevel) and dirname not in git_dirs:
101
- git_dirs.add(dirname)
102
- dirname = os.path.dirname(dirname)
103
- return git_files, git_dirs
89
+ return collect_files_and_dirs(res.stdout.rstrip("\0").split("\0"), toplevel)
104
90
 
105
91
 
106
92
  def git_find_files(path: _t.PathT = "") -> list[str]:
@@ -8,7 +8,7 @@ from .. import _types as _t
8
8
  from .._backends._hg import run_hg
9
9
  from .._compat import norm_real
10
10
  from .._integration import data_from_mime
11
- from . import is_toplevel_acceptable, scm_find_files
11
+ from . import collect_files_and_dirs, is_toplevel_acceptable, scm_find_files
12
12
 
13
13
  log = logging.getLogger(__name__)
14
14
 
@@ -34,20 +34,10 @@ def _hg_ls_files_and_dirs(
34
34
  hg_command: str | None = None,
35
35
  timeout: int | None = None,
36
36
  ) -> tuple[set[str], set[str]]:
37
- hg_files: set[str] = set()
38
- hg_dirs = {toplevel}
39
37
  res = run_hg(["files"], cwd=toplevel, hg_command=hg_command, timeout=timeout)
40
38
  if res.returncode:
41
39
  return set(), set()
42
- for name in res.stdout.splitlines():
43
- name = os.path.normcase(name).replace("/", os.path.sep)
44
- fullname = os.path.join(toplevel, name)
45
- hg_files.add(fullname)
46
- dirname = os.path.dirname(fullname)
47
- while len(dirname) > len(toplevel) and dirname not in hg_dirs:
48
- hg_dirs.add(dirname)
49
- dirname = os.path.dirname(dirname)
50
- return hg_files, hg_dirs
40
+ return collect_files_and_dirs(res.stdout.splitlines(), toplevel)
51
41
 
52
42
 
53
43
  def hg_find_files(path: str = "") -> list[str]:
@@ -13,7 +13,7 @@ import subprocess
13
13
  from .. import _types as _t
14
14
  from .._compat import norm_real
15
15
  from .._run_cmd import run as _run
16
- from . import is_toplevel_acceptable, scm_find_files
16
+ from . import collect_files_and_dirs, is_toplevel_acceptable, scm_find_files
17
17
 
18
18
  log = logging.getLogger(__name__)
19
19
 
@@ -51,19 +51,7 @@ def _jj_ls_files_and_dirs(
51
51
  log.error("listing jj files failed - pretending there aren't any")
52
52
  return set(), set()
53
53
 
54
- jj_files: set[str] = set()
55
- jj_dirs: set[str] = {toplevel}
56
- for name in res.stdout.strip().split("\n"):
57
- if not name:
58
- continue
59
- name = os.path.normcase(name).replace("/", os.path.sep)
60
- fullname = os.path.join(toplevel, name)
61
- jj_files.add(fullname)
62
- dirname = os.path.dirname(fullname)
63
- while len(dirname) > len(toplevel) and dirname not in jj_dirs:
64
- jj_dirs.add(dirname)
65
- dirname = os.path.dirname(dirname)
66
- return jj_files, jj_dirs
54
+ return collect_files_and_dirs(res.stdout.strip().split("\n"), toplevel)
67
55
 
68
56
 
69
57
  def jj_find_files(path: _t.PathT = "") -> list[str]:
@@ -6,13 +6,18 @@ import re
6
6
  import warnings
7
7
  from pathlib import Path
8
8
  from re import Pattern
9
- from typing import Any, NoReturn
9
+ from typing import TYPE_CHECKING, Any, NoReturn
10
10
 
11
11
  from . import _config
12
12
  from . import _types as _t
13
13
  from ._config import Configuration, TagConfiguration
14
14
  from ._environment import resolve_runtime_env
15
15
 
16
+ if TYPE_CHECKING:
17
+ from ._config import ParseFunction
18
+ from ._environment import VcsEnvironment
19
+ from ._version_cls import Version as _VersionType
20
+
16
21
  # Backward-compat re-export used by vcs-versioning/setup.py
17
22
  from ._legacy_parse import (
18
23
  resolved_fallback_root as resolved_fallback_root, # noqa: F401
@@ -90,15 +95,31 @@ def _resolve_version(config: Configuration) -> ScmVersion | None:
90
95
  return None
91
96
 
92
97
 
93
- def _warn_if_tracked(target: Path, root: Path, config: Configuration) -> None:
98
+ def _warn_if_tracked(target: Path, root: Path, config: Configuration) -> bool:
94
99
  """Warn when *target* is tracked in version control (#468).
95
100
 
96
101
  Writing a version file that is tracked makes ``git describe --dirty``
97
102
  report a dirty tree on tag checkouts, causing wrong version numbers.
103
+
104
+ Returns False if target resolves outside root (caller should skip the write).
98
105
  """
99
106
  from ._backends._git import GitWorkdir
100
107
  from ._backends._hg import HgWorkdir
101
108
 
109
+ if not target.is_absolute():
110
+ target = root / target
111
+ resolved_target = target.resolve()
112
+ resolved_root = root.resolve()
113
+ try:
114
+ resolved_target.relative_to(resolved_root)
115
+ except ValueError:
116
+ # todo: emit as GitHub Actions warning via ::warning:: syntax
117
+ warnings.warn(
118
+ f"version file target {target} resolves outside of the project root {root}",
119
+ stacklevel=2,
120
+ )
121
+ return False
122
+
102
123
  for workdir_cls in (GitWorkdir, HgWorkdir):
103
124
  try:
104
125
  wd = workdir_cls.from_potential_worktree(root, config)
@@ -113,7 +134,8 @@ def _warn_if_tracked(target: Path, root: Path, config: Configuration) -> None:
113
134
  " See https://github.com/pypa/setuptools-scm/issues/468",
114
135
  stacklevel=3,
115
136
  )
116
- return
137
+ return True
138
+ return True
117
139
 
118
140
 
119
141
  def write_version_files(
@@ -125,7 +147,8 @@ def write_version_files(
125
147
 
126
148
  write_to = Path(config.write_to)
127
149
  target = root / write_to if not write_to.is_absolute() else write_to
128
- _warn_if_tracked(target, root, config)
150
+ if not _warn_if_tracked(target, root, config):
151
+ return
129
152
 
130
153
  dump_version(
131
154
  root=config.root,
@@ -142,7 +165,8 @@ def write_version_files(
142
165
  # todo: use a better name than fallback root
143
166
  assert config.relative_to is not None
144
167
  target = Path(config.relative_to).parent.joinpath(version_file)
145
- _warn_if_tracked(target, root, config)
168
+ if not _warn_if_tracked(target, root, config):
169
+ return
146
170
 
147
171
  write_version_to_path(
148
172
  target,
@@ -252,14 +276,14 @@ def get_version(
252
276
  parentdir_prefix_version: str | None = None,
253
277
  fallback_version: str | None = None,
254
278
  fallback_root: _t.PathT = ".",
255
- parse: Any | None = None,
279
+ parse: ParseFunction | None = None,
256
280
  git_describe_command: _t.CMD_TYPE | None = None,
257
281
  dist_name: str | None = None,
258
- version_cls: Any | None = None,
282
+ version_cls: type[_VersionType] | str | None = None,
259
283
  normalize: bool = True,
260
284
  search_parent_directories: bool = False,
261
285
  scm: dict[str, Any] | None = None,
262
- _env: Any | None = None,
286
+ _env: VcsEnvironment | None = None,
263
287
  ) -> str:
264
288
  """
265
289
  If supplied, relative_to should be a file from which root may
@@ -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,7 +35,17 @@ class LegacyParseWorkdir(ScmWorkdir):
38
35
  entry-point group.
39
36
  """
40
37
 
41
- parse_fn: ParseFunction = dc_field(repr=False, kw_only=True)
38
+ parse_fn: ParseFunction
39
+
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
42
49
 
43
50
  def get_scm_version(self) -> ScmVersion | None:
44
51
  warnings.warn(
@@ -19,7 +19,7 @@ from packaging.utils import canonicalize_name
19
19
 
20
20
  from . import _config
21
21
  from . import _types as _t
22
- from ._scm_version import ScmVersion, meta # noqa: F401 - for type checking
22
+ from ._scm_version import ScmVersion, meta
23
23
  from ._version_cls import Version as _Version
24
24
 
25
25
  log = logging.getLogger(__name__)
@@ -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__"):
@@ -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: