Skip to content

Commit 6c48f6d

Browse files
tomschrgsakkisppkt
committed
Implement of VersionInfo.next_version() function
Synopsis: semver.VersionInfo.next_version(version, part, prerelease_token="rc") * Add test cases * test_next_version * test_next_version_with_invalid_parts * Reformat code with black * Document it in usage.rst * Implement "nextver" subcommand for pysemver command Co-authored-by: George Sakkis <gsakkis@users.noreply.github.com> Co-authored-By: Karol <ppkt@users.noreply.github.com>
1 parent d69e7c4 commit 6c48f6d

File tree

3 files changed

+130
-2
lines changed

3 files changed

+130
-2
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.9.2
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,51 @@ 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+
This function is taking prereleases into account.
430+
The "major", "minor", and "patch" raises the respective parts like
431+
the ``bump_*`` functions. The real difference is using the
432+
"preprelease" part. It gives you the next patch version of the prerelease,
433+
for example:
434+
435+
>>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease"))
436+
'0.1.5-rc.1'
437+
438+
:param part: One of "major", "minor", "patch", or "prerelease"
439+
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
440+
:return:
441+
"""
442+
validparts = {
443+
"major",
444+
"minor",
445+
"patch",
446+
"prerelease",
447+
# "build", # currently not used
448+
}
449+
if part not in validparts:
450+
raise ValueError(
451+
"Invalid part. Expected one of {validparts}, but got {part!r}".format(
452+
validparts=validparts, part=part
453+
)
454+
)
455+
version = self
456+
if (version.prerelease or version.build) and (
457+
part == "patch"
458+
or (part == "minor" and version.patch == 0)
459+
or (part == "major" and version.minor == version.patch == 0)
460+
):
461+
return version.replace(prerelease=None, build=None)
462+
463+
if part in ("major", "minor", "patch"):
464+
return str(getattr(version, "bump_" + part)())
465+
466+
if not version.prerelease:
467+
version = version.bump_patch()
468+
return version.bump_prerelease(prerelease_token)
469+
425470
@comparator
426471
def __eq__(self, other):
427472
return self.compare(other) == 0
@@ -898,6 +943,7 @@ def replace(version, **parts):
898943
return str(VersionInfo.parse(version).replace(**parts))
899944

900945

946+
# ---- CLI
901947
def cmd_bump(args):
902948
"""
903949
Subcommand: Bumps a version.
@@ -953,6 +999,19 @@ def cmd_compare(args):
953999
return str(compare(args.version1, args.version2))
9541000

9551001

1002+
def cmd_nextver(args):
1003+
"""
1004+
Subcommand: Determines the next version, taking prereleases into account.
1005+
1006+
Synopsis: nextver <VERSION> <PART>
1007+
1008+
:param args: The parsed arguments
1009+
:type args: :class:`argparse.Namespace`
1010+
"""
1011+
version = VersionInfo.parse(args.version)
1012+
return str(version.next_version(args.part))
1013+
1014+
9561015
def createparser():
9571016
"""
9581017
Create an :class:`argparse.ArgumentParser` instance.
@@ -995,6 +1054,15 @@ def createparser():
9951054
parser_check.set_defaults(func=cmd_check)
9961055
parser_check.add_argument("version", help="Version to check")
9971056

1057+
# Create the nextver subcommand
1058+
parser_nextver = s.add_parser(
1059+
"nextver", help="Determines the next version, taking prereleases into account."
1060+
)
1061+
parser_nextver.set_defaults(func=cmd_nextver)
1062+
parser_nextver.add_argument("version", help="Version to raise")
1063+
parser_nextver.add_argument(
1064+
"part", help="One of 'major', 'minor', 'patch', or 'prerelease'"
1065+
)
9981066
return parser
9991067

10001068

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)