Skip to content

Commit 8d4336e

Browse files
authored
Merge pull request #222 from tomschr/feature/221-bump-prerelease-and-build
First implementation of next_version
2 parents d69e7c4 + 5b04960 commit 8d4336e

File tree

3 files changed

+136
-3
lines changed

3 files changed

+136
-3
lines changed

docs/usage.rst

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ It is possible to convert a :class:`semver.VersionInfo` instance:
244244
(5, 4, 2, None, None)
245245

246246

247-
Increasing Parts of a Version
248-
-----------------------------
247+
Raising Parts of a Version
248+
--------------------------
249249

250250
The ``semver`` module contains the following functions to raise parts of
251251
a version:
@@ -276,6 +276,30 @@ a version:
276276
Likewise the module level functions :func:`semver.bump_major`.
277277

278278

279+
Increasing Parts of a Version Taking into Account Prereleases
280+
-------------------------------------------------------------
281+
282+
.. versionadded:: 2.10.0
283+
Added :func:`semver.VersionInfo.next_version`.
284+
285+
If you want to raise your version and take prereleases into account,
286+
the function :func:`semver.VersionInfo.next_version` would perhaps a
287+
better fit.
288+
289+
290+
.. code-block:: python
291+
292+
>>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4")
293+
>>> str(v.next_version(part="prerelease"))
294+
'3.4.5-pre.3'
295+
>>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").next_version(part="patch"))
296+
'3.4.5'
297+
>>> str(semver.VersionInfo.parse("3.4.5+build.4").next_version(part="patch"))
298+
'3.4.5'
299+
>>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease"))
300+
'0.1.5-rc.1'
301+
302+
279303
Comparing Versions
280304
------------------
281305

semver.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,53 @@ def compare(self, other):
422422

423423
return rccmp
424424

425+
def next_version(self, part, prerelease_token="rc"):
426+
"""
427+
Determines next version, preserving natural order.
428+
429+
.. versionadded:: 2.10.0
430+
431+
This function is taking prereleases into account.
432+
The "major", "minor", and "patch" raises the respective parts like
433+
the ``bump_*`` functions. The real difference is using the
434+
"preprelease" part. It gives you the next patch version of the prerelease,
435+
for example:
436+
437+
>>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease"))
438+
'0.1.5-rc.1'
439+
440+
:param part: One of "major", "minor", "patch", or "prerelease"
441+
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
442+
:return:
443+
"""
444+
validparts = {
445+
"major",
446+
"minor",
447+
"patch",
448+
"prerelease",
449+
# "build", # currently not used
450+
}
451+
if part not in validparts:
452+
raise ValueError(
453+
"Invalid part. Expected one of {validparts}, but got {part!r}".format(
454+
validparts=validparts, part=part
455+
)
456+
)
457+
version = self
458+
if (version.prerelease or version.build) and (
459+
part == "patch"
460+
or (part == "minor" and version.patch == 0)
461+
or (part == "major" and version.minor == version.patch == 0)
462+
):
463+
return version.replace(prerelease=None, build=None)
464+
465+
if part in ("major", "minor", "patch"):
466+
return str(getattr(version, "bump_" + part)())
467+
468+
if not version.prerelease:
469+
version = version.bump_patch()
470+
return version.bump_prerelease(prerelease_token)
471+
425472
@comparator
426473
def __eq__(self, other):
427474
return self.compare(other) == 0
@@ -709,7 +756,10 @@ def max_ver(ver1, ver2):
709756
>>> semver.max_ver("1.0.0", "2.0.0")
710757
'2.0.0'
711758
"""
712-
ver1 = VersionInfo.parse(ver1)
759+
if isinstance(ver1, str):
760+
ver1 = VersionInfo.parse(ver1)
761+
elif not isinstance(ver1, VersionInfo):
762+
raise TypeError()
713763
cmp_res = ver1.compare(ver2)
714764
if cmp_res >= 0:
715765
return str(ver1)
@@ -898,6 +948,7 @@ def replace(version, **parts):
898948
return str(VersionInfo.parse(version).replace(**parts))
899949

900950

951+
# ---- CLI
901952
def cmd_bump(args):
902953
"""
903954
Subcommand: Bumps a version.
@@ -953,6 +1004,19 @@ def cmd_compare(args):
9531004
return str(compare(args.version1, args.version2))
9541005

9551006

1007+
def cmd_nextver(args):
1008+
"""
1009+
Subcommand: Determines the next version, taking prereleases into account.
1010+
1011+
Synopsis: nextver <VERSION> <PART>
1012+
1013+
:param args: The parsed arguments
1014+
:type args: :class:`argparse.Namespace`
1015+
"""
1016+
version = VersionInfo.parse(args.version)
1017+
return str(version.next_version(args.part))
1018+
1019+
9561020
def createparser():
9571021
"""
9581022
Create an :class:`argparse.ArgumentParser` instance.
@@ -995,6 +1059,15 @@ def createparser():
9951059
parser_check.set_defaults(func=cmd_check)
9961060
parser_check.add_argument("version", help="Version to check")
9971061

1062+
# Create the nextver subcommand
1063+
parser_nextver = s.add_parser(
1064+
"nextver", help="Determines the next version, taking prereleases into account."
1065+
)
1066+
parser_nextver.set_defaults(func=cmd_nextver)
1067+
parser_nextver.add_argument("version", help="Version to raise")
1068+
parser_nextver.add_argument(
1069+
"part", help="One of 'major', 'minor', 'patch', or 'prerelease'"
1070+
)
9981071
return parser
9991072

10001073

test_semver.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,3 +881,39 @@ def mock_func():
881881

882882
with pytest.deprecated_call():
883883
assert mock_func()
884+
885+
886+
def test_next_version_with_invalid_parts():
887+
version = VersionInfo.parse("1.0.1")
888+
with pytest.raises(ValueError):
889+
version.next_version("invalid")
890+
891+
892+
@pytest.mark.parametrize(
893+
"version, part, expected",
894+
[
895+
# major
896+
("1.0.4-rc.1", "major", "2.0.0"),
897+
("1.1.0-rc.1", "major", "2.0.0"),
898+
("1.1.4-rc.1", "major", "2.0.0"),
899+
("1.2.3", "major", "2.0.0"),
900+
("1.0.0-rc.1", "major", "1.0.0"),
901+
# minor
902+
("0.2.0-rc.1", "minor", "0.2.0"),
903+
("0.2.5-rc.1", "minor", "0.3.0"),
904+
("1.3.1", "minor", "1.4.0"),
905+
# patch
906+
("1.3.2", "patch", "1.3.3"),
907+
("0.1.5-rc.2", "patch", "0.1.5"),
908+
# prerelease
909+
("0.1.4", "prerelease", "0.1.5-rc.1"),
910+
("0.1.5-rc.1", "prerelease", "0.1.5-rc.2"),
911+
# special cases
912+
("0.2.0-rc.1", "patch", "0.2.0"), # same as "minor"
913+
("1.0.0-rc.1", "patch", "1.0.0"), # same as "major"
914+
("1.0.0-rc.1", "minor", "1.0.0"), # same as "major"
915+
],
916+
)
917+
def test_next_version_with_versioninfo(version, part, expected):
918+
ver = VersionInfo.parse(version)
919+
assert str(ver.next_version(part)) == expected

0 commit comments

Comments
 (0)