Skip to content

Commit db167ce

Browse files
committed
try validating earlier
1 parent be8216d commit db167ce

3 files changed

Lines changed: 103 additions & 26 deletions

File tree

‎.ado/publish.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ extends:
9393
versionSpec: "3.11"
9494
displayName: Set Python version
9595

96+
- script: |
97+
python set_version.py --validate-only
98+
env:
99+
BUILD_TYPE: ${{ parameters.Build_Type }}
100+
VERSION: ${{ parameters.Version }}
101+
displayName: Validate version input
102+
96103
- task: PipAuthenticate@1
97104
displayName: Authenticate pip to Azure Artifacts feed
98105
inputs:

‎set_version.py‎

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,53 @@ def get_build_version(version_type: str, build_type: str) -> str:
207207
return build_version
208208

209209

210+
def validate_specified_version(build_type: str, version: str) -> str:
211+
"""Validate a manually specified version and return it stripped.
212+
213+
Returns "" when no version is specified (blank/whitespace), signalling that the
214+
version should be computed automatically. Raises ``ValueError`` when a version is
215+
specified but is malformed or disagrees with ``build_type``.
216+
217+
This performs only local checks (no network), so it is safe to run as an early
218+
fail-fast step before any package-index access.
219+
220+
:param build_type: Build type ("stable"/"dev"/"rc"). Determines which pre-release
221+
suffix a specified version must carry.
222+
:param version: Candidate version string, or "" to compute automatically.
223+
:return: The stripped version, or "" if none was specified.
224+
:rtype: str
225+
"""
226+
specified_version = (version or "").strip()
227+
if not specified_version:
228+
return ""
229+
230+
if not VERSION_INPUT_RE.match(specified_version):
231+
raise ValueError(
232+
f"Version \"{specified_version}\" is not a valid version. Expected "
233+
f"\"major.minor.patch\" optionally followed by \".devN\" or \".rcN\"."
234+
)
235+
236+
# The specified version must match the selected build type, so a build tagged
237+
# "dev"/"rc" can't ship a version that lacks (or mismatches) the suffix.
238+
if build_type == "dev" and ".dev" not in specified_version:
239+
raise ValueError(
240+
f"Build type \"dev\" requires a \".devN\" version, but got "
241+
f"\"{specified_version}\"."
242+
)
243+
if build_type == "rc" and ".rc" not in specified_version:
244+
raise ValueError(
245+
f"Build type \"rc\" requires a \".rcN\" version, but got "
246+
f"\"{specified_version}\"."
247+
)
248+
if build_type == "stable" and (".dev" in specified_version or ".rc" in specified_version):
249+
raise ValueError(
250+
f"Build type \"stable\" requires a \"major.minor.patch\" version "
251+
f"without a pre-release suffix, but got \"{specified_version}\"."
252+
)
253+
254+
return specified_version
255+
256+
210257
def resolve_build_version(version_type: str, build_type: str, version: str = "") -> str:
211258
"""Resolve the version to ship for this run.
212259
@@ -226,39 +273,24 @@ def resolve_build_version(version_type: str, build_type: str, version: str = "")
226273
:return: The version to ship.
227274
:rtype: str
228275
"""
229-
specified_version = (version or "").strip()
276+
specified_version = validate_specified_version(build_type, version)
230277
if specified_version:
231-
if not VERSION_INPUT_RE.match(specified_version):
232-
raise ValueError(
233-
f"Version \"{specified_version}\" is not a valid version. Expected "
234-
f"\"major.minor.patch\" optionally followed by \".devN\" or \".rcN\"."
235-
)
236-
237-
# The specified version must match the selected build type, so a build tagged
238-
# "dev"/"rc" can't ship a version that lacks (or mismatches) the suffix.
239-
if build_type == "dev" and ".dev" not in specified_version:
240-
raise ValueError(
241-
f"Build type \"dev\" requires a \".devN\" version, but got "
242-
f"\"{specified_version}\"."
243-
)
244-
if build_type == "rc" and ".rc" not in specified_version:
245-
raise ValueError(
246-
f"Build type \"rc\" requires a \".rcN\" version, but got "
247-
f"\"{specified_version}\"."
248-
)
249-
if build_type == "stable" and (".dev" in specified_version or ".rc" in specified_version):
250-
raise ValueError(
251-
f"Build type \"stable\" requires a \"major.minor.patch\" version "
252-
f"without a pre-release suffix, but got \"{specified_version}\"."
253-
)
254-
255278
print(f"Using manually specified version: {specified_version}")
256279
return specified_version
257280

258281
return get_build_version(version_type, build_type)
259282

260283

261284
if __name__ == "__main__":
285+
import sys
286+
287+
# Early fail-fast mode: validate the manually specified version (if any) without
288+
# touching the network, so a bad input stops the run before expensive setup.
289+
if "--validate-only" in sys.argv:
290+
validate_specified_version(BUILD_TYPE, VERSION)
291+
print("Version input is valid.")
292+
sys.exit(0)
293+
262294
build_version = resolve_build_version(RELEASE_TYPE, BUILD_TYPE, VERSION)
263295

264296
print(f"Package version: {build_version}")

‎test_set_version.py‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
_version_sort_key,
1010
get_build_version,
1111
resolve_build_version,
12+
validate_specified_version,
1213
VERSION_RE,
1314
)
1415

@@ -214,4 +215,41 @@ def test_resolve_build_version_accepts_matching_build_type(monkeypatch, build_ty
214215
monkeypatch.setattr(
215216
set_version, "get_build_version", lambda *a, **k: "should-not-be-used"
216217
)
217-
assert resolve_build_version("patch", build_type, version) == version
218+
assert resolve_build_version("patch", build_type, version) == version
219+
220+
221+
@pytest.mark.parametrize("blank", ["", " ", None])
222+
def test_validate_specified_version_blank_returns_empty(blank):
223+
# A blank/whitespace/None version returns "" (signalling automatic computation)
224+
# and never touches the network.
225+
assert validate_specified_version("dev", blank) == ""
226+
227+
228+
@pytest.mark.parametrize(
229+
"build_type,version",
230+
[
231+
("dev", "1.2.3.dev0"),
232+
("rc", "1.2.3.rc7"),
233+
("stable", "1.2.3"),
234+
("dev", " 1.2.3.dev0 "),
235+
],
236+
)
237+
def test_validate_specified_version_returns_stripped(build_type, version):
238+
# A valid version is returned stripped of surrounding whitespace.
239+
assert validate_specified_version(build_type, version) == version.strip()
240+
241+
242+
@pytest.mark.parametrize(
243+
"build_type,version",
244+
[
245+
("dev", "1.2"),
246+
("dev", "v1.2.3"),
247+
("dev", "1.2.3"),
248+
("rc", "1.2.3.dev0"),
249+
("stable", "1.2.3.rc0"),
250+
],
251+
)
252+
def test_validate_specified_version_rejects_invalid(build_type, version):
253+
# Malformed or build-type-mismatched versions fail loud.
254+
with pytest.raises(ValueError):
255+
validate_specified_version(build_type, version)

0 commit comments

Comments
 (0)