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
1 change: 1 addition & 0 deletions changelog.d/pr480.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(:meth:`~semver.version.Version.bump_build`) will now add ``.0`` to an existing build when the last segment of the current build metadata, split by dots (``.``), is not numeric. This is to ensure the bumped version contains a raised build instead of silently returning an unchanged version. For example, ``1.2.3+alpha`` is bumped to ``1.2.3+alpha.0``. This mirrors the corresponding fix for the prerelease part (:gh:`460`). Related to :gh:`466`.
11 changes: 11 additions & 0 deletions docs/usage/raise-parts-of-a-version.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,14 @@ lower than `rc10` (non-numeric identifiers are compared alphabetically):
>>> str(Version.parse("3.4.5-rc.9").bump_prerelease())
'3.4.5-rc.10'

The same applies to :meth:`~semver.version.Version.bump_build`: if the
last part of the dot-separated build metadata is not numeric, ``.0`` is
appended to ensure a raised build:

.. code-block:: python

>>> str(Version.parse("3.4.5+build.4").bump_build())
'3.4.5+build.5'
>>> str(Version.parse("3.4.5+alpha").bump_build())
'3.4.5+alpha.0'

7 changes: 7 additions & 0 deletions src/semver/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ def bump_build(self, token: Optional[str] = "build") -> "Version":
>>> ver.bump_build()
Version(major=3, minor=4, patch=5, prerelease='rc.1', \
build='build.10')
>>> str(semver.Version.parse("3.4.5-rc.1+alpha").bump_build())
'3.4.5-rc.1+alpha.0'
"""
cls = type(self)
if self._build is not None:
Expand All @@ -401,6 +403,11 @@ def bump_build(self, token: Optional[str] = "build") -> "Version":
build = str(token) + ".0"

build = cls._increment_string(build)
if build == self._build:
# _increment_string leaves a build without any digits
# unchanged. Raise it nevertheless by appending ".0",
# analogous to _increment_prerelease (see :gh:`460`).
build += ".0"
return cls(self._major, self._minor, self._patch, self._prerelease, build)

def compare(self, other: Comparable) -> int:
Expand Down
78 changes: 78 additions & 0 deletions tests/test_bump_build_digitless.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Regression tests for bump_build() with digitless build metadata.

Related to :gh:`466`. While investigating how the ``token`` parameter of
:meth:`semver.version.Version.bump_build` is used, it turned out that
``bump_build()`` silently returns an *unchanged* version when the build
metadata contains no digits at all (for example ``1.2.3+alpha``).

That contradicts the documented contract that bumping raises the
respective part of the version: the returned object is identical to the
original one, so callers cannot tell that anything was bumped.

The analogous problem for the prerelease part was fixed in :gh:`460`
by appending ``.0`` when the last identifier is not numeric. These
tests require the same behavior for the build part.

Note: build metadata does not take part in SemVer precedence, so the
assertions compare the resulting build strings (and string
representations) instead of using :meth:`Version.compare`.
"""

from semver import Version


def test_bump_build_digitless_metadata_should_append_0():
"""bump_build() must raise a digitless build, not return it unchanged."""
v = Version.parse("1.2.3+alpha")
bumped = v.bump_build()
assert bumped.build == "alpha.0", (
f"Expected build 'alpha.0', but got {bumped.build!r}"
)
assert str(bumped) == "1.2.3+alpha.0"


def test_bump_build_digitless_metadata_should_return_new_version():
"""bump_build() must not silently return an equal version."""
v = Version.parse("1.2.3+alpha")
bumped = v.bump_build()
assert bumped is not v
assert bumped.build != v.build
assert bumped.build == "alpha.0"


def test_bump_build_digitless_multi_identifier_should_append_0():
"""The last identifier is raised, also for dotted digitless builds."""
v = Version.parse("1.2.3+exp.sha")
assert str(v.bump_build()) == "1.2.3+exp.sha.0"


def test_bump_build_numeric_ending_unchanged():
"""Builds with a numeric last identifier keep the incrementing behavior."""
v = Version.parse("1.2.3+build.9")
assert str(v.bump_build()) == "1.2.3+build.10"
v2 = Version.parse("1.2.3+alpha.1")
assert str(v2.bump_build()) == "1.2.3+alpha.2"


def test_bump_build_token_ignored_for_existing_digitless_build():
"""The token only constructs a build when none exists.

An existing (digitless) build must still be raised, not replaced
by the token or returned unchanged.
"""
v = Version.parse("1.2.3+alpha")
bumped = v.bump_build("b")
assert str(bumped) == "1.2.3+alpha.0"


def test_bump_build_digitless_bump_is_repeatable():
"""Repeated bumping of a digitless build keeps raising it."""
v = Version.parse("1.2.3+alpha")
assert str(v.bump_build().bump_build()) == "1.2.3+alpha.1"


def test_bump_build_no_build_uses_token_unchanged():
"""Without any build, the token logic is unchanged (sanity check)."""
assert str(Version.parse("1.2.3").bump_build()) == "1.2.3+build.1"
assert str(Version.parse("1.2.3").bump_build("b")) == "1.2.3+b.1"
assert str(Version.parse("1.2.3").bump_build("")) == "1.2.3+1"
Loading