vcs-versioning 2.2.0__tar.gz → 2.2.2__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.2.0 → vcs_versioning-2.2.2}/CHANGELOG.md +15 -0
  2. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/MANIFEST.in +1 -0
  3. {vcs_versioning-2.2.0/src/vcs_versioning.egg-info → vcs_versioning-2.2.2}/PKG-INFO +1 -1
  4. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/pyproject.toml +20 -18
  5. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/_hg.py +0 -3
  6. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_compat.py +0 -13
  7. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_config.py +0 -3
  8. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_file_finders/__init__.py +33 -2
  9. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_file_finders/_git.py +2 -16
  10. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_file_finders/_hg.py +2 -12
  11. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_file_finders/_jj.py +2 -14
  12. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_get_version_impl.py +32 -8
  13. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_overrides.py +1 -1
  14. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_run_cmd.py +0 -2
  15. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_scm_version.py +2 -6
  16. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_schemes/_standard.py +0 -4
  17. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2/src/vcs_versioning.egg-info}/PKG-INFO +1 -1
  18. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning.egg-info/SOURCES.txt +1 -0
  19. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_file_finders.py +1 -1
  20. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_regressions.py +35 -5
  21. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_tag_config.py +46 -0
  22. vcs_versioning-2.2.2/uv.lock +330 -0
  23. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/LICENSE.txt +0 -0
  24. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/README.md +0 -0
  25. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/setup.cfg +0 -0
  26. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/setup.py +0 -0
  27. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/__init__.py +0 -0
  28. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/__main__.py +0 -0
  29. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/__init__.py +0 -0
  30. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
  31. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/_git.py +0 -0
  32. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/_hg_git.py +0 -0
  33. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/_jj.py +0 -0
  34. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_backends/_scm_workdir.py +0 -0
  35. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_cli/__init__.py +0 -0
  36. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_cli/_args.py +0 -0
  37. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
  38. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
  39. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_discover.py +0 -0
  40. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_dump_version.py +0 -0
  41. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_entrypoints.py +0 -0
  42. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_environment.py +0 -0
  43. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_exceptions.py +0 -0
  44. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_fallback_workdir.py +0 -0
  45. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_fallbacks.py +0 -0
  46. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_integration.py +0 -0
  47. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_integrator_helpers.py +0 -0
  48. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_legacy_parse.py +0 -0
  49. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_log.py +0 -0
  50. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_modify_version.py +0 -0
  51. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_node_utils.py +0 -0
  52. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_paths.py +0 -0
  53. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_project_overrides.py +0 -0
  54. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_protocols.py +0 -0
  55. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_pyproject_reading.py +0 -0
  56. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_requirement_cls.py +0 -0
  57. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_scm_metadata.py +0 -0
  58. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_test_utils.py +0 -0
  59. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_toml.py +0 -0
  60. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_types.py +0 -0
  61. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_cls.py +0 -0
  62. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_fields.py +0 -0
  63. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_inference.py +0 -0
  64. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
  65. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_schemes/_common.py +0 -0
  66. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
  67. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/_worktree_discovery.py +0 -0
  68. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/overrides.py +0 -0
  69. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/py.typed +0 -0
  70. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning/test_api.py +0 -0
  71. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
  72. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning.egg-info/entry_points.txt +0 -0
  73. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning.egg-info/requires.txt +0 -0
  74. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/src/vcs_versioning.egg-info/top_level.txt +0 -0
  75. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/__init__.py +0 -0
  76. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/conftest.py +0 -0
  77. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_better_root_errors.py +0 -0
  78. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_chain_api.py +0 -0
  79. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_compat.py +0 -0
  80. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_config.py +0 -0
  81. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_expect_parse.py +0 -0
  82. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_git.py +0 -0
  83. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_hg_git.py +0 -0
  84. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_integrator_helpers.py +0 -0
  85. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_internal_log_level.py +0 -0
  86. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_jj.py +0 -0
  87. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_legacy_parse.py +0 -0
  88. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_mercurial.py +0 -0
  89. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_overrides_api.py +0 -0
  90. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_overrides_env_reader.py +0 -0
  91. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_project_overrides.py +0 -0
  92. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_project_path.py +0 -0
  93. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_scm_metadata.py +0 -0
  94. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_version.py +0 -0
  95. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_version_scheme_towncrier.py +0 -0
  96. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_version_schemes.py +0 -0
  97. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_workdir_api.py +0 -0
  98. {vcs_versioning-2.2.0 → vcs_versioning-2.2.2}/testing_vcs/test_workdir_discovery.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  <!-- towncrier release notes start -->
4
4
 
5
+ ## 2.2.2 (2026-06-29)
6
+
7
+ ### Fixed
8
+
9
+ - Add missing pytest-timeout to vcs-versioning test dependencies. ([#1461](https://github.com/pypa/setuptools-scm/issues/1461))
10
+
11
+ ## 2.2.1 (2026-06-29)
12
+
13
+ ### Fixed
14
+
15
+ - Fix crash in `_warn_if_tracked` when the version file target is a relative path
16
+ by resolving it against the project root before comparison.
17
+ Also warn (instead of crashing) when the target resolves outside the project root. ([#1451](https://github.com/pypa/setuptools-scm/issues/1451))
18
+
5
19
  ## 2.2.0 (2026-06-24)
6
20
 
7
21
  ### Added
@@ -18,6 +32,7 @@
18
32
  ### Fixed
19
33
 
20
34
  - 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))
35
+ - 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))
21
36
 
22
37
  ## 2.1.1 (2026-06-23)
23
38
 
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: the blessed package to manage your versions by vcs metadata
5
5
  Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
6
6
  License: MIT
@@ -46,7 +46,7 @@ dependencies = [
46
46
  [dependency-groups]
47
47
  test = [
48
48
  "pytest",
49
- "pytest-cov",
49
+ "pytest-timeout",
50
50
  "pytest-xdist",
51
51
  ]
52
52
 
@@ -105,27 +105,29 @@ tag_regex = "^vcs-versioning-(?P<version>v?\\d+(?:\\.\\d+){0,2}[^\\+]*)(?:\\+.*)
105
105
  fallback_version = "0.1.0"
106
106
  scm.git.describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "vcs-versioning-*"]
107
107
 
108
- [tool.coverage.run]
109
- branch = true
110
- parallel = true
111
- omit = [
112
- "vcs_versioning/__about__.py",
113
- ]
114
-
115
- [tool.coverage.report]
116
- exclude_lines = [
117
- "no cov",
118
- "if __name__ == .__main__.:",
119
- "if TYPE_CHECKING:",
120
- ]
121
-
122
108
  [tool.pytest.ini_options]
109
+ minversion = "8.2"
123
110
  testpaths = ["testing_vcs"]
124
111
  python_files = ["test_*.py"]
125
- addopts = ["-ra", "--strict-markers", "-p", "vcs_versioning.test_api"]
112
+ xfail_strict = true
113
+ addopts = ["-ra", "--strict-config", "--strict-markers", "-p", "vcs_versioning.test_api"]
114
+ timeout = 300
115
+ filterwarnings = [
116
+ "error",
117
+ "ignore:.*tool\\.setuptools_scm.*",
118
+ "ignore:.*git archive did not support describe output.*:UserWarning",
119
+ "ignore:.*unprocessed git archival found.*:UserWarning",
120
+ "ignore:.*without VcsEnvironment.*:DeprecationWarning",
121
+ "ignore:.*Passing config to workdir.*:DeprecationWarning",
122
+ "ignore:.*tag_regex.*is deprecated.*:DeprecationWarning",
123
+ "ignore:.*write_to_source.*:DeprecationWarning",
124
+ "ignore:.*relative_to is expected to be a file.*:UserWarning",
125
+ ]
126
+ log_level = "debug"
127
+ log_cli_level = "info"
126
128
  markers = [
127
- "issue: marks tests related to specific issues",
128
- "no_init: use setup_hg(init=False) for wd fixture - hg check without worktree",
129
+ "issue(id): reference to github issue",
130
+ "no_init: skip initializing the VCS repository in the test fixture",
129
131
  "skip_commit: allows to skip committing in the helpers",
130
132
  ]
131
133
 
@@ -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
 
@@ -86,19 +86,6 @@ def strip_path_suffix(
86
86
  return full_path[: -len(suffix_path)]
87
87
 
88
88
 
89
- # Legacy aliases for backward compatibility during transition
90
- def assert_path_endswith(
91
- full_path: str, suffix_path: str, error_msg: str | None = None
92
- ) -> None:
93
- """Legacy alias - use strip_path_suffix instead."""
94
- strip_path_suffix(full_path, suffix_path, error_msg)
95
-
96
-
97
- def compute_path_prefix(full_path: str, suffix_path: str) -> str:
98
- """Legacy alias - use strip_path_suffix instead."""
99
- return strip_path_suffix(full_path, suffix_path)
100
-
101
-
102
89
  def norm_real(path: PathT) -> str:
103
90
  """Normalize and resolve a path (combining normcase and realpath).
104
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,7 +3,7 @@ from __future__ import annotations
3
3
  import logging
4
4
  import os
5
5
  import sys
6
- from collections.abc import Callable, Mapping
6
+ from collections.abc import Callable, Iterable, Mapping
7
7
 
8
8
  if sys.version_info >= (3, 10):
9
9
  from typing import TypeGuard
@@ -140,4 +140,35 @@ def find_files(path: _t.PathT = "") -> list[str]:
140
140
  return []
141
141
 
142
142
 
143
- __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
@@ -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__)
@@ -14,8 +14,6 @@ from . import _types as _t
14
14
 
15
15
  log = logging.getLogger(__name__)
16
16
 
17
- _DEFAULT_SUBPROCESS_TIMEOUT = 40
18
-
19
17
 
20
18
  def _get_timeout(env: Mapping[str, str]) -> int:
21
19
  """Read subprocess timeout from resolved runtime settings.
@@ -152,19 +152,15 @@ def tag_to_version(
152
152
  version_str = tag_dict["version"]
153
153
  log.debug("version pre parse %s", version_str)
154
154
 
155
- # Try to create version from base version first
156
155
  try:
157
156
  version: _Version = config.version_cls(version_str)
158
157
  log.debug("version=%r", version)
159
158
  except Exception:
160
159
  warnings.warn(
161
- f"tag {tag!r} will be stripped of its suffix {tag_dict.get('suffix', '')!r}",
160
+ f"tag {tag!r} version {version_str!r} could not be parsed",
162
161
  stacklevel=2,
163
162
  )
164
- # Fall back to trying without any suffix
165
- version = config.version_cls(version_str)
166
- log.debug("version=%r", version)
167
- return version
163
+ return None
168
164
 
169
165
  # If base version is valid, check if we can preserve the suffix
170
166
  if suffix := tag_dict.get("suffix", ""):
@@ -11,7 +11,6 @@ import re
11
11
  import warnings
12
12
  from datetime import date, datetime, timedelta, timezone
13
13
  from re import Match
14
- from typing import TYPE_CHECKING
15
14
 
16
15
  from .. import _modify_version
17
16
  from .._exceptions import DirtyWorkingTreeError
@@ -19,9 +18,6 @@ from .._scm_version import ScmVersion, _parse_version_tag
19
18
  from .._version_cls import Version as PkgVersion
20
19
  from ._common import SEMVER_LEN, SEMVER_MINOR, SEMVER_PATCH
21
20
 
22
- if TYPE_CHECKING:
23
- pass
24
-
25
21
  log = logging.getLogger(__name__)
26
22
 
27
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: the blessed package to manage your versions by vcs metadata
5
5
  Author-email: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
6
6
  License: MIT
@@ -4,6 +4,7 @@ MANIFEST.in
4
4
  README.md
5
5
  pyproject.toml
6
6
  setup.py
7
+ uv.lock
7
8
  src/vcs_versioning/__init__.py
8
9
  src/vcs_versioning/__main__.py
9
10
  src/vcs_versioning/_compat.py
@@ -13,7 +13,7 @@ from vcs_versioning.test_api import WorkDir
13
13
  def inwd(
14
14
  request: pytest.FixtureRequest, wd: WorkDir, monkeypatch: pytest.MonkeyPatch
15
15
  ) -> WorkDir:
16
- param: str = request.param # type: ignore[attr-defined]
16
+ param: str = request.param
17
17
  if param == "git":
18
18
  wd.setup_git(monkeypatch)
19
19
  elif param == "hg":
@@ -82,11 +82,7 @@ def test_write_to_absolute_path_passes_when_subdir_of_root(tmp_path: Path) -> No
82
82
  write_version_files(replace(c, write_to="VERSION.py"), "1.0", v)
83
83
  subdir = tmp_path / "subdir"
84
84
  subdir.mkdir()
85
- with pytest.raises(
86
- # todo: python version specific error list
87
- ValueError,
88
- match=r".*VERSION.py' .* .*subdir.*",
89
- ):
85
+ with pytest.warns(UserWarning, match=r"resolves outside of"):
90
86
  write_version_files(replace(c, root=subdir), "1.0", v)
91
87
 
92
88
 
@@ -138,6 +134,40 @@ def test_no_warn_when_version_file_not_tracked(tmp_path: Path, scm: str) -> None
138
134
  assert tracked_warnings == []
139
135
 
140
136
 
137
+ @pytest.mark.issue(1451)
138
+ def test_warn_if_tracked_relative_target_no_crash(tmp_path: Path) -> None:
139
+ """_warn_if_tracked must not crash when target is relative and root is absolute."""
140
+ from vcs_versioning._get_version_impl import _warn_if_tracked
141
+
142
+ wd = WorkDir(tmp_path).setup_git()
143
+ version_file = tmp_path / "pkg" / "__version__.py"
144
+ version_file.parent.mkdir()
145
+ version_file.write_text("__version__ = '0.0.0'\n")
146
+ wd.add_and_commit("add tracked version file")
147
+
148
+ c = Configuration(root=tmp_path)
149
+ relative_target = Path("pkg/__version__.py")
150
+
151
+ with pytest.warns(UserWarning, match="tracked by version control"):
152
+ _warn_if_tracked(relative_target, tmp_path, c)
153
+
154
+
155
+ @pytest.mark.issue(1451)
156
+ def test_warn_if_tracked_warns_when_target_escapes_root(tmp_path: Path) -> None:
157
+ """_warn_if_tracked must warn when target resolves outside root."""
158
+ from vcs_versioning._get_version_impl import _warn_if_tracked
159
+
160
+ project = tmp_path / "project"
161
+ project.mkdir()
162
+ WorkDir(project).setup_git()
163
+
164
+ c = Configuration(root=project)
165
+ escaping_target = Path("../../etc/passwd")
166
+
167
+ with pytest.warns(UserWarning, match="resolves outside of"):
168
+ _warn_if_tracked(escaping_target, project, c)
169
+
170
+
141
171
  @pytest.mark.issue(1423)
142
172
  def test_parse_version_importable_from_get_version_impl() -> None:
143
173
  """setuptools-scm <=10.0.x imports parse_version from this module."""
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import warnings
6
6
 
7
+ import pytest
8
+ from packaging.version import Version
7
9
  from vcs_versioning import Configuration
8
10
  from vcs_versioning._config import TagConfiguration
9
11
  from vcs_versioning._scm_version import tag_to_version
@@ -109,6 +111,50 @@ class TestTagPrefixStripping:
109
111
  assert str(version) == "1.2.3"
110
112
 
111
113
 
114
+ class TestTagToVersionSuffixPreservation:
115
+ """Test that tag_to_version preserves +local suffixes when possible."""
116
+
117
+ def test_tag_with_build_metadata_preserved(self) -> None:
118
+ config = Configuration()
119
+ version = tag_to_version("1.2.3+build.123", config)
120
+ assert version is not None
121
+ assert str(version) == "1.2.3+build.123"
122
+
123
+ def test_tag_with_invalid_suffix_stripped(self) -> None:
124
+ config = Configuration()
125
+ # "+invalid!!" is captured as suffix by the default regex but
126
+ # Version("1.2.3+invalid!!") raises, so the suffix gets stripped
127
+ with pytest.warns(UserWarning, match="will be stripped of its suffix"):
128
+ version = tag_to_version("1.2.3+invalid!!", config)
129
+ assert version is not None
130
+ assert str(version) == "1.2.3"
131
+
132
+ @pytest.mark.parametrize(
133
+ ("tag", "expected"),
134
+ [
135
+ ("1.0.0+ci.456", "1.0.0+ci.456"),
136
+ ("v2.0.0+build.789", "2.0.0+build.789"),
137
+ ("release-1.0.0+metadata", "1.0.0+metadata"),
138
+ ],
139
+ )
140
+ def test_various_build_metadata_tags(self, tag: str, expected: str) -> None:
141
+ config = Configuration()
142
+ version = tag_to_version(tag, config)
143
+ assert version is not None
144
+ assert str(version) == expected
145
+
146
+ def test_custom_version_cls_parse_failure_returns_none(self) -> None:
147
+ """When version_cls rejects the base version string, return None."""
148
+
149
+ def rejecting_version_cls(version_str: str) -> Version:
150
+ raise ValueError(f"rejected: {version_str}")
151
+
152
+ config = Configuration(version_cls=rejecting_version_cls) # type: ignore[arg-type]
153
+ with pytest.warns(UserWarning, match="could not be parsed"):
154
+ result = tag_to_version("1.2.3", config)
155
+ assert result is None
156
+
157
+
112
158
  class TestConfigFromData:
113
159
  """Test that Configuration.from_data correctly parses tag config."""
114
160