diff --git a/docs/usage.rst b/docs/usage.rst index 91e4d069..fd2af5fa 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -244,8 +244,8 @@ It is possible to convert a :class:`semver.VersionInfo` instance: (5, 4, 2, None, None) -Increasing Parts of a Version ------------------------------ +Raising Parts of a Version +-------------------------- The ``semver`` module contains the following functions to raise parts of a version: @@ -276,6 +276,30 @@ a version: Likewise the module level functions :func:`semver.bump_major`. +Increasing Parts of a Version Taking into Account Prereleases +------------------------------------------------------------- + +.. versionadded:: 2.10.0 + Added :func:`semver.VersionInfo.next_version`. + +If you want to raise your version and take prereleases into account, +the function :func:`semver.VersionInfo.next_version` would perhaps a +better fit. + + +.. code-block:: python + + >>> v = semver.VersionInfo.parse("3.4.5-pre.2+build.4") + >>> str(v.next_version(part="prerelease")) + '3.4.5-pre.3' + >>> str(semver.VersionInfo.parse("3.4.5-pre.2+build.4").next_version(part="patch")) + '3.4.5' + >>> str(semver.VersionInfo.parse("3.4.5+build.4").next_version(part="patch")) + '3.4.5' + >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' + + Comparing Versions ------------------ diff --git a/semver.py b/semver.py index a5739b16..35192ed9 100644 --- a/semver.py +++ b/semver.py @@ -422,6 +422,53 @@ def compare(self, other): return rccmp + def next_version(self, part, prerelease_token="rc"): + """ + Determines next version, preserving natural order. + + .. versionadded:: 2.10.0 + + This function is taking prereleases into account. + The "major", "minor", and "patch" raises the respective parts like + the ``bump_*`` functions. The real difference is using the + "preprelease" part. It gives you the next patch version of the prerelease, + for example: + + >>> str(semver.VersionInfo.parse("0.1.4").next_version("prerelease")) + '0.1.5-rc.1' + + :param part: One of "major", "minor", "patch", or "prerelease" + :param prerelease_token: prefix string of prerelease, defaults to 'rc' + :return: + """ + validparts = { + "major", + "minor", + "patch", + "prerelease", + # "build", # currently not used + } + if part not in validparts: + raise ValueError( + "Invalid part. Expected one of {validparts}, but got {part!r}".format( + validparts=validparts, part=part + ) + ) + version = self + if (version.prerelease or version.build) and ( + part == "patch" + or (part == "minor" and version.patch == 0) + or (part == "major" and version.minor == version.patch == 0) + ): + return version.replace(prerelease=None, build=None) + + if part in ("major", "minor", "patch"): + return str(getattr(version, "bump_" + part)()) + + if not version.prerelease: + version = version.bump_patch() + return version.bump_prerelease(prerelease_token) + @comparator def __eq__(self, other): return self.compare(other) == 0 @@ -709,7 +756,10 @@ def max_ver(ver1, ver2): >>> semver.max_ver("1.0.0", "2.0.0") '2.0.0' """ - ver1 = VersionInfo.parse(ver1) + if isinstance(ver1, str): + ver1 = VersionInfo.parse(ver1) + elif not isinstance(ver1, VersionInfo): + raise TypeError() cmp_res = ver1.compare(ver2) if cmp_res >= 0: return str(ver1) @@ -898,6 +948,7 @@ def replace(version, **parts): return str(VersionInfo.parse(version).replace(**parts)) +# ---- CLI def cmd_bump(args): """ Subcommand: Bumps a version. @@ -953,6 +1004,19 @@ def cmd_compare(args): return str(compare(args.version1, args.version2)) +def cmd_nextver(args): + """ + Subcommand: Determines the next version, taking prereleases into account. + + Synopsis: nextver + + :param args: The parsed arguments + :type args: :class:`argparse.Namespace` + """ + version = VersionInfo.parse(args.version) + return str(version.next_version(args.part)) + + def createparser(): """ Create an :class:`argparse.ArgumentParser` instance. @@ -995,6 +1059,15 @@ def createparser(): parser_check.set_defaults(func=cmd_check) parser_check.add_argument("version", help="Version to check") + # Create the nextver subcommand + parser_nextver = s.add_parser( + "nextver", help="Determines the next version, taking prereleases into account." + ) + parser_nextver.set_defaults(func=cmd_nextver) + parser_nextver.add_argument("version", help="Version to raise") + parser_nextver.add_argument( + "part", help="One of 'major', 'minor', 'patch', or 'prerelease'" + ) return parser diff --git a/test_semver.py b/test_semver.py index 5daf3f1a..8ac42c81 100644 --- a/test_semver.py +++ b/test_semver.py @@ -881,3 +881,39 @@ def mock_func(): with pytest.deprecated_call(): assert mock_func() + + +def test_next_version_with_invalid_parts(): + version = VersionInfo.parse("1.0.1") + with pytest.raises(ValueError): + version.next_version("invalid") + + +@pytest.mark.parametrize( + "version, part, expected", + [ + # major + ("1.0.4-rc.1", "major", "2.0.0"), + ("1.1.0-rc.1", "major", "2.0.0"), + ("1.1.4-rc.1", "major", "2.0.0"), + ("1.2.3", "major", "2.0.0"), + ("1.0.0-rc.1", "major", "1.0.0"), + # minor + ("0.2.0-rc.1", "minor", "0.2.0"), + ("0.2.5-rc.1", "minor", "0.3.0"), + ("1.3.1", "minor", "1.4.0"), + # patch + ("1.3.2", "patch", "1.3.3"), + ("0.1.5-rc.2", "patch", "0.1.5"), + # prerelease + ("0.1.4", "prerelease", "0.1.5-rc.1"), + ("0.1.5-rc.1", "prerelease", "0.1.5-rc.2"), + # special cases + ("0.2.0-rc.1", "patch", "0.2.0"), # same as "minor" + ("1.0.0-rc.1", "patch", "1.0.0"), # same as "major" + ("1.0.0-rc.1", "minor", "1.0.0"), # same as "major" + ], +) +def test_next_version_with_versioninfo(version, part, expected): + ver = VersionInfo.parse(version) + assert str(ver.next_version(part)) == expected