Skip to content

Commit 3097c6e

Browse files
first working iteration on build tag passover from scm tags to version strings
addresses #1019 needs more iteration
1 parent 82587cc commit 3097c6e

File tree

7 files changed

+101
-39
lines changed

7 files changed

+101
-39
lines changed

src/setuptools_scm/_overrides.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ def _read_pretended_version_for(
164164
pretended = read_named_env(name="PRETEND_VERSION", dist_name=config.dist_name)
165165

166166
if pretended:
167-
# we use meta here since the pretended version
168-
# must adhere to the pep to begin with
167+
# Use enhanced meta function - let validation errors bubble up
169168
return version.meta(tag=pretended, preformatted=True, config=config)
170169
else:
171170
return None

src/setuptools_scm/fallbacks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def parse_pkginfo(root: _t.PathT, config: Configuration) -> ScmVersion | None:
2525
data = data_from_mime(pkginfo)
2626
version = data.get("Version", _UNKNOWN)
2727
if version != _UNKNOWN:
28+
# Use enhanced meta function - let validation errors bubble up
2829
return meta(version, preformatted=True, config=config)
2930
else:
3031
return None
@@ -38,8 +39,10 @@ def fallback_version(root: _t.PathT, config: Configuration) -> ScmVersion | None
3839
parent_name[len(config.parentdir_prefix_version) :], config
3940
)
4041
if version is not None:
42+
# Use enhanced meta function - let validation errors bubble up
4143
return meta(str(version), preformatted=True, config=config)
4244
if config.fallback_version is not None:
4345
log.debug("FALLBACK %s", config.fallback_version)
46+
# Use enhanced meta function - let validation errors bubble up
4447
return meta(config.fallback_version, preformatted=True, config=config)
4548
return None

src/setuptools_scm/version.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ def _source_epoch_or_utc_now() -> datetime:
152152
class ScmVersion:
153153
"""represents a parsed version from scm"""
154154

155-
tag: _v.Version | _v.NonNormalizedVersion | str
156-
"""the related tag or preformatted version string"""
155+
tag: _v.Version | _v.NonNormalizedVersion
156+
"""the related tag or preformatted version"""
157157
config: _config.Configuration
158158
"""the configuration used to parse the version"""
159159
distance: int = 0
@@ -223,9 +223,16 @@ def format_next_version(
223223

224224
def _parse_tag(
225225
tag: _VersionT | str, preformatted: bool, config: _config.Configuration
226-
) -> _VersionT | str:
226+
) -> _VersionT:
227227
if preformatted:
228-
return tag
228+
# For preformatted versions, tag should already be validated as a version object
229+
# String validation is handled in meta function before calling this
230+
if isinstance(tag, str):
231+
# This should not happen with enhanced meta, but kept for safety
232+
return _v.NonNormalizedVersion(tag)
233+
else:
234+
# Already a version object (including test mocks), return as-is
235+
return tag
229236
elif not isinstance(tag, config.version_cls):
230237
version = tag_to_version(tag, config)
231238
assert version is not None
@@ -246,7 +253,16 @@ def meta(
246253
node_date: date | None = None,
247254
time: datetime | None = None,
248255
) -> ScmVersion:
249-
parsed_version = _parse_tag(tag, preformatted, config)
256+
parsed_version: _VersionT
257+
# Enhanced string validation for preformatted versions
258+
if preformatted and isinstance(tag, str):
259+
# Validate PEP 440 compliance using NonNormalizedVersion
260+
# Let validation errors bubble up to the caller
261+
parsed_version = _v.NonNormalizedVersion(tag)
262+
else:
263+
# Use existing _parse_tag logic for non-preformatted or already validated inputs
264+
parsed_version = _parse_tag(tag, preformatted, config)
265+
250266
log.info("version %s -> %s", tag, parsed_version)
251267
assert parsed_version is not None, f"Can't parse version {tag}"
252268
scm_version = ScmVersion(
@@ -533,8 +549,7 @@ def format_version(version: ScmVersion) -> str:
533549
log.debug("scm version %s", version)
534550
log.debug("config %s", version.config)
535551
if version.preformatted:
536-
assert isinstance(version.tag, str)
537-
return version.tag
552+
return str(version.tag)
538553

539554
# Extract original tag's local data for later combination
540555
original_local = ""
@@ -544,23 +559,10 @@ def format_version(version: ScmVersion) -> str:
544559
# Create a patched ScmVersion with only the base version (no local data) for version schemes
545560
from dataclasses import replace
546561

547-
if version.tag:
548-
# Extract the base version (public part) from the tag using config's version_cls
549-
if hasattr(version.tag, "public"):
550-
# It's a Version object with a public attribute
551-
base_version_str = str(version.tag.public)
552-
elif isinstance(version.tag, str):
553-
# It's a string - strip any local part
554-
base_version_str = version.tag.split("+")[0]
555-
else:
556-
# It's some other type - use string representation and strip local part
557-
base_version_str = str(version.tag).split("+")[0]
558-
559-
# Create the base tag using the config's version class
560-
base_tag = version.config.version_cls(base_version_str)
561-
version_for_scheme = replace(version, tag=base_tag)
562-
else:
563-
version_for_scheme = version
562+
# Extract the base version (public part) from the tag using config's version_cls
563+
base_version_str = str(version.tag.public)
564+
base_tag = version.config.version_cls(base_version_str)
565+
version_for_scheme = replace(version, tag=base_tag)
564566

565567
main_version = _entrypoints._call_version_scheme(
566568
version_for_scheme,

testing/test_basic_api.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None:
5555

5656
def assertion(config: Configuration) -> ScmVersion:
5757
assert config.absolute_root == expected_root
58-
return ScmVersion("1.0", config=config)
58+
from packaging.version import Version
59+
60+
return ScmVersion(Version("1.0"), config=config)
5961

6062
monkeypatch.setattr(setuptools_scm._get_version_impl, "parse_version", assertion)
6163

@@ -250,6 +252,18 @@ def __init__(self, tag_str: str) -> None:
250252
def __repr__(self) -> str:
251253
return f"hello,{self.version}"
252254

255+
@property
256+
def public(self) -> str:
257+
"""The public portion of the version (without local part)."""
258+
return self.version.split("+")[0]
259+
260+
@property
261+
def local(self) -> str | None:
262+
"""The local version segment."""
263+
if "+" in self.version:
264+
return self.version.split("+", 1)[1]
265+
return None
266+
253267
# you can not use normalize=False and version_cls at the same time
254268
with pytest.raises(
255269
ValueError,

testing/test_git.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,16 +215,26 @@ def test_version_from_git(wd: WorkDir) -> None:
215215
setup(use_scm_version={'normalize': False, 'write_to': 'VERSION.txt'})
216216
""",
217217
"with_created_class": """
218-
from setuptools import setup
218+
from setuptools import setup
219+
220+
class MyVersion:
221+
def __init__(self, tag_str: str):
222+
self.version = tag_str
223+
224+
def __repr__(self):
225+
return self.version
219226
220-
class MyVersion:
221-
def __init__(self, tag_str: str):
222-
self.version = tag_str
227+
@property
228+
def public(self):
229+
return self.version.split('+')[0]
223230
224-
def __repr__(self):
225-
return self.version
231+
@property
232+
def local(self):
233+
if '+' in self.version:
234+
return self.version.split('+', 1)[1]
235+
return None
226236
227-
setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'})
237+
setup(use_scm_version={'version_cls': MyVersion, 'write_to': 'VERSION.txt'})
228238
""",
229239
"with_named_import": """
230240
from setuptools import setup

testing/test_integration.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,14 +193,16 @@ def test_pretend_version_name_takes_precedence(
193193
assert wd.get_version(dist_name="test") == "1.0.0"
194194

195195

196-
def test_pretend_version_accepts_bad_string(
196+
def test_pretend_version_rejects_invalid_string(
197197
monkeypatch: pytest.MonkeyPatch, wd: WorkDir
198198
) -> None:
199+
"""Test that invalid pretend versions raise errors and bubble up."""
199200
monkeypatch.setenv(PRETEND_KEY, "dummy")
200201
wd.write("setup.py", SETUP_PY_PLAIN)
201-
assert wd.get_version(write_to="test.py") == "dummy"
202-
pyver = wd([sys.executable, "setup.py", "--version"])
203-
assert pyver == "0.0.0"
202+
203+
# With strict validation, invalid pretend versions should raise errors
204+
with pytest.raises(Exception, match=r".*dummy.*"):
205+
wd.get_version(write_to="test.py")
204206

205207

206208
def test_pretend_metadata_with_version(

testing/test_version.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,27 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None:
7171

7272

7373
def test_next_semver_bad_tag() -> None:
74-
version = meta("1.0.0-foo", preformatted=True, config=c)
74+
# Create a mock version class that represents an invalid version for testing error handling
75+
from typing import cast
76+
77+
from setuptools_scm._version_cls import _VersionT
78+
79+
class BrokenVersionForTest:
80+
"""A mock version that behaves like a string but passes type checking."""
81+
82+
def __init__(self, version_str: str):
83+
self._version_str = version_str
84+
85+
def __str__(self) -> str:
86+
return self._version_str
87+
88+
def __repr__(self) -> str:
89+
return f"BrokenVersionForTest({self._version_str!r})"
90+
91+
# Cast to the expected type to avoid type checking issues
92+
broken_tag = cast(_VersionT, BrokenVersionForTest("1.0.0-foo"))
93+
version = meta(broken_tag, preformatted=True, config=c)
94+
7595
with pytest.raises(
7696
ValueError, match=r"1\.0\.0-foo.* can't be parsed as numeric version"
7797
):
@@ -471,6 +491,18 @@ def __str__(self) -> str:
471491
def __repr__(self) -> str:
472492
return f"MyVersion<Custom{self.tag}>"
473493

494+
@property
495+
def public(self) -> str:
496+
"""The public portion of the version (without local part)."""
497+
return self.tag.split("+")[0]
498+
499+
@property
500+
def local(self) -> str | None:
501+
"""The local version segment."""
502+
if "+" in self.tag:
503+
return self.tag.split("+", 1)[1]
504+
return None
505+
474506
config = Configuration(version_cls=MyVersion) # type: ignore[arg-type]
475507
scm_version = meta("1.0.0-foo", config=config)
476508

0 commit comments

Comments
 (0)