Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions vcs-versioning/changelog.d/523.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The ``semver-pep440`` and ``semver-pep440-release-branch`` version schemes now
correctly handle ``.dev0`` tags and pre-release tags. Exact checkout on a tag
returns the tag as-is (``2.0.dev0`` stays ``2.0.dev0``, ``1.0.0rc1`` stays
``1.0.0rc1``). Non-exact ``.dev0`` tags are treated as explicit version anchors,
producing ``X.Y.0.devN`` instead of incorrectly bumping past the anchored version.
14 changes: 8 additions & 6 deletions vcs-versioning/src/vcs_versioning/_version_schemes/_standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ def guess_next_dev_version(version: ScmVersion) -> str:
def guess_next_simple_semver(
version: ScmVersion, retain: int, increment: bool = True
) -> str:
if increment and getattr(version.tag, "dev", None) == 0:
parts = list(version.tag.release)
while len(parts) < SEMVER_LEN:
parts.append(0)
return ".".join(str(i) for i in parts)
Comment on lines +48 to +51
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

guess_next_simple_semver treats any PEP 440 dev release tag as an anchor via is_devrelease. That includes tags like 1.0.dev1, which can produce a guessed version that is not guaranteed to sort higher than the tag (e.g., distance=1 would yield 1.0.0.dev1, effectively equal to the tag). If the intended behavior is specifically for .dev0 anchors, gate this branch on version.tag.dev == 0 (or an equivalent .dev0 check) and either fall back to the normal incrementing behavior or raise a ValueError for nonzero .devN to match the existing .devX-unsupported policy in _modify_version._bump_dev.

Suggested change
parts = list(version.tag.release)
while len(parts) < SEMVER_LEN:
parts.append(0)
return ".".join(str(i) for i in parts)
dev_number = getattr(version.tag, "dev", None)
if dev_number == 0:
parts = list(version.tag.release)
while len(parts) < SEMVER_LEN:
parts.append(0)
return ".".join(str(i) for i in parts)

Copilot uses AI. Check for mistakes.
parts = list(version.tag.release[:retain])
while len(parts) < retain:
parts.append(0)
Expand All @@ -56,15 +61,12 @@ def guess_next_simple_semver(

def simplified_semver_version(version: ScmVersion) -> str:
if version.exact:
return guess_next_simple_semver(version, retain=SEMVER_LEN, increment=False)
elif version.branch is not None and "feature" in version.branch:
return version.format_with("{tag}")
if version.branch is not None and "feature" in version.branch:
return version.format_next_version(
guess_next_simple_semver, retain=SEMVER_MINOR
)
else:
return version.format_next_version(
guess_next_simple_semver, retain=SEMVER_PATCH
)
return version.format_next_version(guess_next_simple_semver, retain=SEMVER_PATCH)


def release_branch_semver_version(version: ScmVersion) -> str:
Expand Down
31 changes: 30 additions & 1 deletion vcs-versioning/testing_vcs/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
("version", "expected_next"),
[
pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"),
pytest.param(meta("1.0", config=c), "1.0.0", id="short_tag"),
pytest.param(meta("1.0", config=c), "1.0", id="short_tag"),
pytest.param(meta("1.0.0rc1", config=c), "1.0.0rc1", id="exact_rc"),
pytest.param(meta("2.0.dev0", config=c), "2.0.dev0", id="exact_dev0"),
pytest.param(
meta("1.0.0", distance=2, branch="default", config=c),
"1.0.1.dev2",
Expand Down Expand Up @@ -58,6 +60,21 @@
"1.0.1.dev2",
id="non-normalized-allowed",
),
pytest.param(
meta("2.0.dev0", distance=5, branch="default", config=c),
"2.0.0.dev5",
id="dev0_anchor_default_branch",
),
pytest.param(
meta("2.0.dev0", distance=5, branch="feature/fun", config=c),
"2.0.0.dev5",
id="dev0_anchor_feature_branch",
),
pytest.param(
meta("3.0.dev0", distance=1, branch="default", config=c),
"3.0.0.dev1",
id="dev0_anchor_single_commit",
),
],
)
def test_next_semver(version: ScmVersion, expected_next: str) -> None:
Expand All @@ -69,6 +86,8 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None:
("version", "expected_next"),
[
pytest.param(meta("1.0.0", config=c), "1.0.0", id="exact"),
pytest.param(meta("2.0.dev0", config=c), "2.0.dev0", id="exact_dev0"),
pytest.param(meta("1.0.0rc1", config=c), "1.0.0rc1", id="exact_rc"),
pytest.param(
meta("1.0.0", distance=2, branch="master", config=c),
"1.1.0.dev2",
Expand Down Expand Up @@ -99,6 +118,16 @@ def test_next_semver(version: ScmVersion, expected_next: str) -> None:
"1.1.0.dev2",
id="false_positive_release_branch",
),
pytest.param(
meta("2.0.dev0", distance=5, branch="master", config=c),
"2.0.0.dev5",
id="dev0_anchor_development_branch",
),
pytest.param(
meta("2.0.dev0", distance=5, branch="release-2.0", config=c),
"2.0.dev5",
id="dev0_anchor_release_branch",
),
],
)
def test_next_release_branch_semver(version: ScmVersion, expected_next: str) -> None:
Expand Down
Loading