Skip to content

Commit 85d4ff1

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 9691b41 commit 85d4ff1

File tree

3 files changed

+129
-2
lines changed

3 files changed

+129
-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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,50 @@ def bump_build(self, token="build"):
371371
build = cls._increment_string(self._build or (token or "build") + ".0")
372372
return cls(self._major, self._minor, self._patch, self._prerelease, build)
373373

374+
def next_version(self, part, prerelease_token="rc"):
375+
"""
376+
Determines the next version, taking prereleases into account.
377+
378+
The "major", "minor", and "patch" raises the respective parts like
379+
the ``bump_*`` functions. The real difference is using the
380+
"preprelease" part. It gives you the next patch version of the prerelease,
381+
for example:
382+
383+
>>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease"))
384+
'0.1.5-rc.1'
385+
386+
:param part: One of "major", "minor", "patch", or "prerelease"
387+
:param prerelease_token: prefix string of prerelease, defaults to 'rc'
388+
:return:
389+
"""
390+
validparts = {
391+
"major",
392+
"minor",
393+
"patch",
394+
"prerelease",
395+
# "build", # currently not used
396+
}
397+
if part not in validparts:
398+
raise ValueError(
399+
"Invalid part. Expected one of {validparts}, but got {part!r}".format(
400+
validparts=validparts, part=part
401+
)
402+
)
403+
version = self
404+
if (version.prerelease or version.build) and (
405+
part == "patch"
406+
or (part == "minor" and version.patch == 0)
407+
or (part == "major" and version.minor == version.patch == 0)
408+
):
409+
return version.replace(prerelease=None, build=None)
410+
411+
if part in ("major", "minor", "patch"):
412+
return str(getattr(version, "bump_" + part)())
413+
414+
if not version.prerelease:
415+
version = version.bump_patch()
416+
return version.bump_prerelease(prerelease_token)
417+
374418
@comparator
375419
def __eq__(self, other):
376420
return _compare_by_keys(self.to_dict(), _to_dict(other)) == 0
@@ -851,6 +895,7 @@ def replace(version, **parts):
851895
return str(VersionInfo.parse(version).replace(**parts))
852896

853897

898+
# ---- CLI
854899
def cmd_bump(args):
855900
"""
856901
Subcommand: Bumps a version.
@@ -906,6 +951,19 @@ def cmd_compare(args):
906951
return str(compare(args.version1, args.version2))
907952

908953

954+
def cmd_nextver(args):
955+
"""
956+
Subcommand: Determines the next version, taking prereleases into account.
957+
958+
Synopsis: nextver <VERSION> <PART>
959+
960+
:param args: The parsed arguments
961+
:type args: :class:`argparse.Namespace`
962+
"""
963+
version = VersionInfo.parse(args.version)
964+
return str(version.next_version(args.part))
965+
966+
909967
def createparser():
910968
"""
911969
Create an :class:`argparse.ArgumentParser` instance.
@@ -948,6 +1006,15 @@ def createparser():
9481006
parser_check.set_defaults(func=cmd_check)
9491007
parser_check.add_argument("version", help="Version to check")
9501008

1009+
# Create the nextver subcommand
1010+
parser_nextver = s.add_parser(
1011+
"nextver", help="Determines the next version, taking prereleases into account."
1012+
)
1013+
parser_nextver.set_defaults(func=cmd_nextver)
1014+
parser_nextver.add_argument("version", help="Version to raise")
1015+
parser_nextver.add_argument(
1016+
"part", help="One of 'major', 'minor', 'patch', or 'prerelease'"
1017+
)
9511018
return parser
9521019

9531020

test_semver.py

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

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

0 commit comments

Comments
 (0)