Skip to content

Commit 56864d1

Browse files
tomschrgsakkis
andcommitted
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 * Document it in usage.rst Co-authored-by: George Sakkis <gsakkis@users.noreply.github.com>
1 parent 3f92aa5 commit 56864d1

File tree

3 files changed

+163
-10
lines changed

3 files changed

+163
-10
lines changed

docs/usage.rst

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ Depending which function you call, you get different types
213213
{'major': 3, 'minor': 4, 'patch': 5, 'prerelease': None, 'build': None}
214214

215215

216-
Increasing Parts of a Version
217-
-----------------------------
216+
Raising Parts of a Version
217+
--------------------------
218218

219219
The ``semver`` module contains the following functions to raise parts of
220220
a version:
@@ -242,6 +242,29 @@ a version:
242242
>>> semver.bump_build("3.4.5-pre.2+build.4")
243243
'3.4.5-pre.2+build.5'
244244
245+
246+
Increasing Parts of a Version Taking into Account Prereleases
247+
-------------------------------------------------------------
248+
249+
.. versionadded:: 2.9.2
250+
Added :func:`semver.next_version`.
251+
252+
If you want to raise your version and take prereleases into account,
253+
the :func:`semver.next_version` or :func:`semver.VersionInfo.next_version`
254+
would perhaps a better fit.
255+
256+
257+
.. code-block:: python
258+
259+
>>> semver.next_version("3.4.5-pre.2+build.4", part="prerelease"))
260+
'3.4.5-pre.3'
261+
>>> semver.next_version("3.4.5-pre.2+build.4", part="patch")
262+
'3.5.0'
263+
>>> semver.next_version("3.4.5+build.4", part="patch")
264+
'3.4.5'
265+
>>> semver.next_version("0.1.4", "prerelease")
266+
'0.1.5-rc.1'
267+
245268
246269
Comparing Versions
247270
------------------

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, taking 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, taking 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 or version.build) 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, build=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: 70 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,71 @@ 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+
("3.4.5-pre.2+build.4", "prerelease", "3.4.5-pre.3"),
859+
("3.4.5-pre.2+build.4", "patch", "3.4.5"),
860+
("3.4.5+build.4", "patch", "3.4.5"),
861+
],
862+
)
863+
def test_next_version(version, part, expected):
864+
assert next_version(version, part) == expected
865+
866+
867+
def test_next_version_with_invalid_parts():
868+
with pytest.raises(ValueError):
869+
next_version("1.0.1", "invalid")
870+
871+
872+
@pytest.mark.parametrize(
873+
"version, part, expected",
874+
[
875+
# major
876+
("1.0.4-rc.1", "major", "2.0.0"),
877+
("1.1.0-rc.1", "major", "2.0.0"),
878+
("1.1.4-rc.1", "major", "2.0.0"),
879+
("1.2.3", "major", "2.0.0"),
880+
("1.0.0-rc.1", "major", "1.0.0"),
881+
# minor
882+
("0.2.0-rc.1", "minor", "0.2.0"),
883+
("0.2.5-rc.1", "minor", "0.3.0"),
884+
("1.3.1", "minor", "1.4.0"),
885+
# patch
886+
("1.3.2", "patch", "1.3.3"),
887+
("0.1.5-rc.2", "patch", "0.1.5"),
888+
# prerelease
889+
("0.1.4", "prerelease", "0.1.5-rc.1"),
890+
("0.1.5-rc.1", "prerelease", "0.1.5-rc.2"),
891+
# special cases
892+
("0.2.0-rc.1", "patch", "0.2.0"), # same as "minor"
893+
("1.0.0-rc.1", "patch", "1.0.0"), # same as "major"
894+
("1.0.0-rc.1", "minor", "1.0.0"), # same as "major"
895+
],
896+
)
897+
def test_next_version_with_versioninfo(version, part, expected):
898+
ver = VersionInfo.parse(version)
899+
assert str(ver.next_version(part)) == expected

0 commit comments

Comments
 (0)