vcs-versioning 2.1.1__tar.gz → 2.1.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 (97) hide show
  1. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/CHANGELOG.md +6 -0
  2. {vcs_versioning-2.1.1/src/vcs_versioning.egg-info → vcs_versioning-2.1.2}/PKG-INFO +1 -1
  3. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/_git.py +6 -2
  4. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/_hg.py +1 -1
  5. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/_hg_git.py +1 -1
  6. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/_jj.py +1 -1
  7. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/_scm_workdir.py +33 -7
  8. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_fallback_workdir.py +18 -1
  9. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_file_finders/_git.py +3 -0
  10. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_get_version_impl.py +4 -4
  11. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_legacy_parse.py +2 -8
  12. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_protocols.py +10 -3
  13. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_scm_version.py +10 -14
  14. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_worktree_discovery.py +1 -1
  15. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2/src/vcs_versioning.egg-info}/PKG-INFO +1 -1
  16. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_legacy_parse.py +5 -5
  17. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_workdir_discovery.py +63 -0
  18. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/LICENSE.txt +0 -0
  19. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/MANIFEST.in +0 -0
  20. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/README.md +0 -0
  21. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/pyproject.toml +0 -0
  22. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/setup.cfg +0 -0
  23. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/setup.py +0 -0
  24. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/__init__.py +0 -0
  25. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/__main__.py +0 -0
  26. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/__init__.py +0 -0
  27. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_backends/_discover_vcs.py +0 -0
  28. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_cli/__init__.py +0 -0
  29. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_cli/_args.py +0 -0
  30. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_cli/git_archival_full.txt +0 -0
  31. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_cli/git_archival_stable.txt +0 -0
  32. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_compat.py +0 -0
  33. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_config.py +0 -0
  34. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_discover.py +0 -0
  35. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_dump_version.py +0 -0
  36. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_entrypoints.py +0 -0
  37. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_environment.py +0 -0
  38. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_exceptions.py +0 -0
  39. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_fallbacks.py +0 -0
  40. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_file_finders/__init__.py +0 -0
  41. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_file_finders/_hg.py +0 -0
  42. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_file_finders/_jj.py +0 -0
  43. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_integration.py +0 -0
  44. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_integrator_helpers.py +0 -0
  45. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_log.py +0 -0
  46. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_modify_version.py +0 -0
  47. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_node_utils.py +0 -0
  48. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_overrides.py +0 -0
  49. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_paths.py +0 -0
  50. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_project_overrides.py +0 -0
  51. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_pyproject_reading.py +0 -0
  52. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_requirement_cls.py +0 -0
  53. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_run_cmd.py +0 -0
  54. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_scm_metadata.py +0 -0
  55. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_test_utils.py +0 -0
  56. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_toml.py +0 -0
  57. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_types.py +0 -0
  58. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_cls.py +0 -0
  59. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_fields.py +0 -0
  60. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_inference.py +0 -0
  61. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_schemes/__init__.py +0 -0
  62. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_schemes/_common.py +0 -0
  63. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_schemes/_standard.py +0 -0
  64. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/_version_schemes/_towncrier.py +0 -0
  65. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/overrides.py +0 -0
  66. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/py.typed +0 -0
  67. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning/test_api.py +0 -0
  68. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning.egg-info/SOURCES.txt +0 -0
  69. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning.egg-info/dependency_links.txt +0 -0
  70. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning.egg-info/entry_points.txt +0 -0
  71. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning.egg-info/requires.txt +0 -0
  72. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/src/vcs_versioning.egg-info/top_level.txt +0 -0
  73. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/__init__.py +0 -0
  74. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/conftest.py +0 -0
  75. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_better_root_errors.py +0 -0
  76. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_chain_api.py +0 -0
  77. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_compat.py +0 -0
  78. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_config.py +0 -0
  79. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_expect_parse.py +0 -0
  80. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_file_finders.py +0 -0
  81. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_git.py +0 -0
  82. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_hg_git.py +0 -0
  83. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_integrator_helpers.py +0 -0
  84. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_internal_log_level.py +0 -0
  85. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_jj.py +0 -0
  86. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_mercurial.py +0 -0
  87. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_overrides_api.py +0 -0
  88. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_overrides_env_reader.py +0 -0
  89. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_project_overrides.py +0 -0
  90. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_project_path.py +0 -0
  91. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_regressions.py +0 -0
  92. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_scm_metadata.py +0 -0
  93. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_tag_config.py +0 -0
  94. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_version.py +0 -0
  95. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_version_scheme_towncrier.py +0 -0
  96. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_version_schemes.py +0 -0
  97. {vcs_versioning-2.1.1 → vcs_versioning-2.1.2}/testing_vcs/test_workdir_api.py +0 -0
@@ -2,6 +2,12 @@
2
2
 
3
3
  <!-- towncrier release notes start -->
4
4
 
5
+ ## 2.1.2 (2026-06-23)
6
+
7
+ ### Fixed
8
+
9
+ - 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))
10
+
5
11
  ## 2.1.1 (2026-06-23)
6
12
 
7
13
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.1.1
3
+ Version: 2.1.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-Expression: MIT
@@ -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
@@ -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,
@@ -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,
@@ -38,12 +38,7 @@ class LegacyParseWorkdir(ScmWorkdir):
38
38
  entry-point group.
39
39
  """
40
40
 
41
- _parse_fn: ParseFunction | None = dc_field(default=None, repr=False)
42
-
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")
41
+ parse_fn: ParseFunction = dc_field(repr=False, kw_only=True)
47
42
 
48
43
  def get_scm_version(self) -> ScmVersion | None:
49
44
  warnings.warn(
@@ -53,8 +48,7 @@ class LegacyParseWorkdir(ScmWorkdir):
53
48
  DeprecationWarning,
54
49
  stacklevel=2,
55
50
  )
56
- assert self._parse_fn is not None
57
- result = self._parse_fn(self.config.absolute_root, config=self.config)
51
+ result = self.parse_fn(self.config.absolute_root, config=self.config)
58
52
  if result is not None and not isinstance(result, ScmVersion):
59
53
  raise TypeError(
60
54
  f"version parse result was {result!r}\n"
@@ -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)."""
@@ -347,20 +347,14 @@ class ScmVersion:
347
347
 
348
348
  def _parse_tag(
349
349
  tag: _Version | str, preformatted: bool, config: _config.Configuration
350
- ) -> _Version:
350
+ ) -> _Version | None:
351
351
  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
352
  if isinstance(tag, str):
355
- # This should not happen with enhanced meta, but kept for safety
356
353
  return _v.NonNormalizedVersion(tag)
357
354
  else:
358
- # Already a version object (including test mocks), return as-is
359
355
  return tag
360
356
  elif not isinstance(tag, config.version_cls):
361
- version = tag_to_version(tag, config)
362
- assert version is not None
363
- return version
357
+ return tag_to_version(tag, config)
364
358
  else:
365
359
  return tag
366
360
 
@@ -389,18 +383,20 @@ def meta(
389
383
  node_date: date | None = None,
390
384
  time: datetime | None = None,
391
385
  ) -> ScmVersion:
392
- parsed_version: _Version
393
- # Enhanced string validation for preformatted versions
386
+ parsed_version: _Version | None
394
387
  if preformatted and isinstance(tag, str):
395
- # Validate PEP 440 compliance using NonNormalizedVersion
396
- # Let validation errors bubble up to the caller
397
388
  parsed_version = _v.NonNormalizedVersion(tag)
398
389
  else:
399
- # Use existing _parse_tag logic for non-preformatted or already validated inputs
400
390
  parsed_version = _parse_tag(tag, preformatted, config)
401
391
 
392
+ if parsed_version is None:
393
+ raise ValueError(
394
+ f"Can't parse version from tag {tag!r}"
395
+ f" (tag_regex={config.tag.regex.pattern!r},"
396
+ f" tag_prefix={config.tag.prefix!r})"
397
+ )
398
+
402
399
  log.info("version %s -> %s", tag, parsed_version)
403
- assert parsed_version is not None, f"Can't parse version {tag}"
404
400
 
405
401
  kwargs: _ScmVersionKwargs = {
406
402
  "distance": distance,
@@ -84,7 +84,7 @@ def discover_workdir(config: Configuration) -> AnyWorkdir | None:
84
84
  return LegacyParseWorkdir(
85
85
  path=Path(config.absolute_root),
86
86
  _config=config,
87
- _parse_fn=config.parse,
87
+ parse_fn=config.parse,
88
88
  )
89
89
 
90
90
  factories = _load_discovery_factories()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vcs-versioning
3
- Version: 2.1.1
3
+ Version: 2.1.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-Expression: MIT
@@ -34,8 +34,8 @@ class TestLegacyParseWorkdir:
34
34
  return Configuration.from_file(pyproject)
35
35
 
36
36
  def test_requires_parse_fn(self, tmp_path: Path) -> None:
37
- with pytest.raises(TypeError, match="requires a _parse_fn"):
38
- LegacyParseWorkdir(path=tmp_path)
37
+ with pytest.raises(TypeError, match="parse_fn"):
38
+ LegacyParseWorkdir(path=tmp_path) # type: ignore[call-arg]
39
39
 
40
40
  def test_get_scm_version_emits_deprecation(self, tmp_path: Path) -> None:
41
41
  config = self._make_config(tmp_path)
@@ -43,7 +43,7 @@ class TestLegacyParseWorkdir:
43
43
  def fake_parse(root: _t.PathT, *, config: Configuration) -> ScmVersion | None:
44
44
  return ScmVersion(tag=Version("1.0.0"), config=config)
45
45
 
46
- wd = LegacyParseWorkdir(path=tmp_path, _parse_fn=fake_parse)
46
+ wd = LegacyParseWorkdir(path=tmp_path, parse_fn=fake_parse)
47
47
  wd._config = config
48
48
 
49
49
  with warnings.catch_warnings(record=True) as w:
@@ -62,7 +62,7 @@ class TestLegacyParseWorkdir:
62
62
  def fake_parse(root: _t.PathT, *, config: Configuration) -> ScmVersion | None:
63
63
  return None
64
64
 
65
- wd = LegacyParseWorkdir(path=tmp_path, _parse_fn=fake_parse)
65
+ wd = LegacyParseWorkdir(path=tmp_path, parse_fn=fake_parse)
66
66
  wd._config = config
67
67
 
68
68
  with warnings.catch_warnings(record=True):
@@ -75,7 +75,7 @@ class TestLegacyParseWorkdir:
75
75
  def bad_parse(root: _t.PathT, *, config: Configuration) -> ScmVersion | None:
76
76
  return "not-a-version" # type: ignore[return-value]
77
77
 
78
- wd = LegacyParseWorkdir(path=tmp_path, _parse_fn=bad_parse)
78
+ wd = LegacyParseWorkdir(path=tmp_path, parse_fn=bad_parse)
79
79
  wd._config = config
80
80
 
81
81
  with warnings.catch_warnings(record=True):
@@ -86,6 +86,48 @@ class TestDiscoverWorkdirGit:
86
86
  assert isinstance(result, ScmWorkdir)
87
87
  assert result.path == tmp_path
88
88
 
89
+ @pytest.mark.issue(1440)
90
+ def test_list_tracked_files_scoped_to_project_root(self, tmp_path: Path) -> None:
91
+ """In a monorepo, list_tracked_files() must only return files under project_root."""
92
+ # Resolve to real path to avoid Windows 8.3 short name mismatches
93
+ tmp_path = tmp_path.resolve()
94
+ _git_init(tmp_path)
95
+
96
+ # Create files in two sibling projects
97
+ proj_a = tmp_path / "project-a"
98
+ proj_b = tmp_path / "project-b"
99
+ proj_a.mkdir()
100
+ proj_b.mkdir()
101
+ (proj_a / "a.py").write_text("# a", encoding="utf-8")
102
+ (proj_b / "b.py").write_text("# b", encoding="utf-8")
103
+
104
+ subprocess.run(
105
+ ["git", "add", "."], cwd=tmp_path, check=True, capture_output=True
106
+ )
107
+ subprocess.run(
108
+ ["git", "commit", "-m", "add projects"],
109
+ cwd=tmp_path,
110
+ check=True,
111
+ capture_output=True,
112
+ )
113
+
114
+ # Discover from project-a with root=".." (monorepo pattern)
115
+ config = Configuration(
116
+ relative_to=str(proj_a / "pyproject.toml"),
117
+ root="..",
118
+ )
119
+ result = discover_workdir(config)
120
+ assert result is not None
121
+ assert isinstance(result, ScmWorkdir)
122
+ assert result.path == tmp_path
123
+ assert result.project_root == proj_a
124
+
125
+ files = list(result.list_tracked_files())
126
+ # Should only contain files under project-a, not project-b or repo root
127
+ assert any("a.py" in f for f in files), f"expected a.py in {files}"
128
+ assert not any("b.py" in f for f in files), f"unexpected b.py in {files}"
129
+ assert not any("dummy" in f for f in files), f"unexpected dummy in {files}"
130
+
89
131
 
90
132
  class TestDiscoverWorkdirFallback:
91
133
  def test_discovers_pkginfo(self, tmp_path: Path) -> None:
@@ -261,6 +303,27 @@ class TestMetadataWorkdir:
261
303
  files = wd.list_tracked_files()
262
304
  assert files == ["src/pkg/__init__.py"]
263
305
 
306
+ @pytest.mark.issue(1439)
307
+ def test_custom_tag_regex_does_not_break_metadata(self, tmp_path: Path) -> None:
308
+ """Stored tags are already parsed; custom tag_regex must not re-parse them."""
309
+ data = ScmVersionData(
310
+ tag="1.5.5",
311
+ distance=0,
312
+ node="gabc1234",
313
+ dirty=False,
314
+ branch="main",
315
+ node_date=None,
316
+ )
317
+ write_scm_version_data(tmp_path, data)
318
+
319
+ config = Configuration(
320
+ tag_regex=r"^cuda-pathfinder-(?P<version>v\d+\.\d+\.\d+(?:[ab]\d+)?)",
321
+ )
322
+ wd = MetadataWorkdir(path=tmp_path, metadata_dir=tmp_path, _config=config)
323
+ version = wd.get_scm_version()
324
+ assert version is not None
325
+ assert str(version.tag) == "1.5.5"
326
+
264
327
  def test_missing_metadata_returns_none(self, tmp_path: Path) -> None:
265
328
  config = Configuration()
266
329
  wd = MetadataWorkdir(path=tmp_path, metadata_dir=tmp_path, _config=config)
File without changes
File without changes
File without changes