Skip to content

Commit f3aa4ab

Browse files
committed
First implementation of next_version
* Implement semver.next_version(version, part, prerelease_token="rc") * Add test cases * test_next_version * test_next_version_with_invalid_parts * Reformat code with black Co-authored-by: George Sakkis <gsakkis>
1 parent 3f92aa5 commit f3aa4ab

File tree

2 files changed

+104
-8
lines changed

2 files changed

+104
-8
lines changed

semver.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,26 @@ def bump_build(self, token="build"):
257257
"""
258258
return parse_version_info(bump_build(str(self), token))
259259

260+
def next_version(self, part, prerelease_token="rc"):
261+
"""
262+
Determines the next version, takeing prereleases into account.
263+
264+
The "major", "minor", and "patch" raises the respective parts like
265+
the ``bump_*`` functions. The real difference is using the
266+
"preprelease" part. It gives you the next patch version of the prerelease,
267+
for example:
268+
269+
>>> semver.next_version("0.1.4", "prerelease")
270+
'0.1.5-rc.1'
271+
272+
:param version: a semver version string
273+
:param part: One of "major", "minor", "patch", or "prerelease"
274+
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
275+
:return:
276+
"""
277+
nxt = next_version(str(self), part, prerelease_token)
278+
return parse_version_info(nxt)
279+
260280
@comparator
261281
def __eq__(self, other):
262282
return _compare_by_keys(self._asdict(), _to_dict(other)) == 0
@@ -374,14 +394,7 @@ def parse_version_info(version):
374394
'build.4'
375395
"""
376396
parts = parse(version)
377-
version_info = VersionInfo(
378-
parts["major"],
379-
parts["minor"],
380-
parts["patch"],
381-
parts["prerelease"],
382-
parts["build"],
383-
)
384-
397+
version_info = VersionInfo(**parts)
385398
return version_info
386399

387400

@@ -684,6 +697,53 @@ def finalize_version(version):
684697
return format_version(verinfo["major"], verinfo["minor"], verinfo["patch"])
685698

686699

700+
def next_version(version, part, prerelease_token="rc"):
701+
"""
702+
Determines the next version, takeing prereleases into account.
703+
704+
The "major", "minor", and "patch" raises the respective parts like
705+
the ``bump_*`` functions. The real difference is using the
706+
"preprelease" part. It gives you the next patch version of the prerelease,
707+
for example:
708+
709+
>>> semver.next_version("0.1.4", "prerelease")
710+
'0.1.5-rc.1'
711+
712+
:param version: a semver version string
713+
:param part: One of "major", "minor", "patch", or "prerelease"
714+
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
715+
:return:
716+
"""
717+
validparts = {
718+
"major",
719+
"minor",
720+
"patch",
721+
"prerelease",
722+
# "build", # ???
723+
}
724+
if part not in validparts:
725+
raise ValueError(
726+
"Invalid part. Expected one of {validparts}, but got {part!r}".format(
727+
validparts=validparts, part=part
728+
)
729+
)
730+
version = VersionInfo.parse(version)
731+
if version.prerelease and (
732+
part == "patch"
733+
or (part == "minor" and version.patch == 0)
734+
or (part == "major" and version.minor == version.patch == 0)
735+
):
736+
return str(version.replace(prerelease=None))
737+
738+
if part in ("major", "minor", "patch"):
739+
return str(getattr(version, "bump_" + part)())
740+
741+
if not version.prerelease:
742+
version = version.bump_patch()
743+
return str(version.bump_prerelease(prerelease_token))
744+
745+
746+
# ---- CLI
687747
def cmd_bump(args):
688748
"""
689749
Subcommand: Bumps a version.

test_semver.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
match,
2121
max_ver,
2222
min_ver,
23+
next_version,
2324
parse,
2425
parse_version_info,
2526
process,
@@ -39,6 +40,7 @@
3940
match,
4041
max_ver,
4142
min_ver,
43+
next_version,
4244
parse,
4345
process,
4446
replace,
@@ -827,3 +829,37 @@ def test_replace_raises_ValueError_for_non_numeric_values():
827829
def test_should_versioninfo_isvalid():
828830
assert VersionInfo.isvalid("1.0.0") is True
829831
assert VersionInfo.isvalid("foo") is False
832+
833+
834+
@pytest.mark.parametrize(
835+
"version, part, expected",
836+
[
837+
# major
838+
("1.0.4-rc.1", "major", "2.0.0"),
839+
("1.1.0-rc.1", "major", "2.0.0"),
840+
("1.1.4-rc.1", "major", "2.0.0"),
841+
("1.2.3", "major", "2.0.0"),
842+
("1.0.0-rc.1", "major", "1.0.0"),
843+
# minor
844+
("0.2.0-rc.1", "minor", "0.2.0"),
845+
("0.2.5-rc.1", "minor", "0.3.0"),
846+
("1.3.1", "minor", "1.4.0"),
847+
# patch
848+
("1.3.2", "patch", "1.3.3"),
849+
("0.1.5-rc.2", "patch", "0.1.5"),
850+
# prerelease
851+
("0.1.4", "prerelease", "0.1.5-rc.1"),
852+
("0.1.5-rc.1", "prerelease", "0.1.5-rc.2"),
853+
# special cases
854+
("0.2.0-rc.1", "patch", "0.2.0"), # same as "minor"
855+
("1.0.0-rc.1", "patch", "1.0.0"), # same as "major"
856+
("1.0.0-rc.1", "minor", "1.0.0"), # same as "major"
857+
],
858+
)
859+
def test_next_version(version, part, expected):
860+
assert next_version(version, part) == expected
861+
862+
863+
def test_next_version_with_invalid_parts():
864+
with pytest.raises(ValueError):
865+
next_version("1.0.1", "invalid")

0 commit comments

Comments
 (0)