diff --git a/changelog.d/460.bugfix.rst b/changelog.d/460.bugfix.rst new file mode 100644 index 00000000..dc21db80 --- /dev/null +++ b/changelog.d/460.bugfix.rst @@ -0,0 +1,9 @@ +:meth:`~semver.version.Version.bump_prerelease` will now add `.0` to an +existing prerelease when the last segment of the current prerelease, split by +dots (`.`), is not numeric. This is to ensure the new prerelease is considered +higher than the previous one. + +:meth:`~semver.version.Version.bump_prerelease` now also support an argument +`bump_when_empty` which will bump the patch version if there is no existing +prerelease, to ensure the resulting version is considered a higher version than +the previous one. \ No newline at end of file diff --git a/changelog.d/463.trivial.rst b/changelog.d/463.trivial.rst new file mode 100644 index 00000000..3dcad2ae --- /dev/null +++ b/changelog.d/463.trivial.rst @@ -0,0 +1 @@ +Remove double code in :meth:`Version.bump_build` \ No newline at end of file diff --git a/docs/advanced/combine-pydantic-and-semver.rst b/docs/advanced/combine-pydantic-and-semver.rst index c7566e12..975fb418 100644 --- a/docs/advanced/combine-pydantic-and-semver.rst +++ b/docs/advanced/combine-pydantic-and-semver.rst @@ -9,7 +9,22 @@ According to its homepage, `Pydantic `_ "enforces type hints at runtime, and provides user friendly errors when data is invalid." -To work with Pydantic>2.0, use the following steps: +If you are working with Pydantic>2.0 and pydantic-extra-types>=2.10.5 use the built in `SemanticVersion` type, which wraps the :class:`Version ` class. + + .. code-block:: python + + from pydantic import BaseModel + from pydantic_extra_types.semantic_version import SemanticVersion + + class appVersion(BaseModel): + version: SemanticVersion + + app_version = appVersion(version="1.2.3") + + print(app_version.version) + # > 1.2.3 + +To work with Pydantic>2.0 and without pydantic-extra-types use the following example to define your own type: 1. Derive a new class from :class:`~semver.version.Version` diff --git a/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst index be89cf8d..101a8c35 100644 --- a/docs/usage/raise-parts-of-a-version.rst +++ b/docs/usage/raise-parts-of-a-version.rst @@ -3,13 +3,14 @@ Raising Parts of a Version .. note:: - Keep in mind, "raising" the pre-release only will make your - complete version *lower* than before. + Keep in mind, by default, "raising" the pre-release for a version without an existing + prerelease part, only will make your complete version *lower* than before. For example, having version ``1.0.0`` and raising the pre-release will lead to ``1.0.0-rc.1``, but ``1.0.0-rc.1`` is smaller than ``1.0.0``. - If you search for a way to take into account this behavior, look for the + To avoid this, set `bump_when_empty=True` in the + :meth:`~semver.version.Version.bump_prerelease` method, or by using the method :meth:`~semver.version.Version.next_version` in section :ref:`increase-parts-of-a-version`. @@ -67,4 +68,14 @@ is not taken into account: >>> str(Version.parse("3.4.5-rc.1").bump_prerelease('')) '3.4.5-rc.2' +To ensure correct ordering, we append `.0` to the last prerelease identifier +if it's not numeric. This prevents cases where `rc9` would incorrectly sort +lower than `rc10` (non-numeric identifiers are compared alphabetically): + +.. code-block:: python + + >>> str(Version.parse("3.4.5-rc9").bump_prerelease()) + '3.4.5-rc9.0' + >>> str(Version.parse("3.4.5-rc.9").bump_prerelease()) + '3.4.5-rc.10' diff --git a/src/semver/version.py b/src/semver/version.py index 0ff5b9f4..f9450f95 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -77,8 +77,10 @@ class Version: #: The names of the different parts of a version NAMES: ClassVar[Tuple[str, ...]] = tuple([item[1:] for item in __slots__]) - #: Regex for number in a prerelease + #: Regex for number in a build _LAST_NUMBER: ClassVar[Pattern[str]] = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+") + #: Regex for number in a prerelease + _LAST_PRERELEASE: ClassVar[Pattern[str]] = re.compile(r"^(.*\.)?(\d+)$") #: Regex template for a semver version _REGEX_TEMPLATE: ClassVar[ str @@ -245,6 +247,23 @@ def __iter__(self) -> VersionIterator: """Return iter(self).""" yield from self.to_tuple() + @staticmethod + def _increment_prerelease(string: str) -> str: + """ + Check if the last part of a dot-separated string is numeric. If yes, + increase them. Else, add '.0' + + :param string: the prerelease version to increment + :return: the incremented string + """ + match = Version._LAST_PRERELEASE.search(string) + if match: + next_ = str(int(match.group(2)) + 1) + string = match.group(1) + next_ if match.group(1) else next_ + else: + string += ".0" + return string + @staticmethod def _increment_string(string: str) -> str: """ @@ -305,35 +324,52 @@ def bump_patch(self) -> "Version": cls = type(self) return cls(self._major, self._minor, self._patch + 1) - def bump_prerelease(self, token: Optional[str] = "rc") -> "Version": + def bump_prerelease( + self, + token: Optional[str] = "rc", + bump_when_empty: Optional[bool] = False + ) -> "Version": """ Raise the prerelease part of the version, return a new object but leave self untouched. + .. versionchanged:: 3.1.0 + Parameter `bump_when_empty` added. When set to true, bumps the patch version + when called with a version that has no prerelease segment, so the return + value will be considered a newer version. + + Adds `.0` to the prerelease if the last part of the dot-separated + prerelease is not a number. + :param token: defaults to ``'rc'`` :return: new :class:`Version` object with the raised prerelease part. The original object is not modified. >>> ver = semver.parse("3.4.5") >>> ver.bump_prerelease().prerelease - 'rc.2' + 'rc.1' >>> ver.bump_prerelease('').prerelease '1' >>> ver.bump_prerelease(None).prerelease 'rc.1' + >>> str(ver.bump_prerelease(bump_when_empty=True)) + '3.4.6-rc.1' """ cls = type(self) + patch = self._patch if self._prerelease is not None: - prerelease = self._prerelease - elif token == "": - prerelease = "0" - elif token is None: - prerelease = "rc.0" + prerelease = cls._increment_prerelease(self._prerelease) else: - prerelease = str(token) + ".0" + if bump_when_empty: + patch += 1 + if token == "": + prerelease = "1" + elif token is None: + prerelease = "rc.1" + else: + prerelease = str(token) + ".1" - prerelease = cls._increment_string(prerelease) - return cls(self._major, self._minor, self._patch, prerelease) + return cls(self._major, self._minor, patch, prerelease) def bump_build(self, token: Optional[str] = "build") -> "Version": """ @@ -359,18 +395,6 @@ def bump_build(self, token: Optional[str] = "build") -> "Version": else: build = str(token) + ".0" - # self._build or (token or "build") + ".0" - build = cls._increment_string(build) - if self._build is not None: - build = self._build - elif token == "": - build = "0" - elif token is None: - build = "build.0" - else: - build = str(token) + ".0" - - # self._build or (token or "build") + ".0" build = cls._increment_string(build) return cls(self._major, self._minor, self._patch, self._prerelease, build) @@ -457,10 +481,8 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version": # Only check the main parts: if part in cls.NAMES[:3]: return getattr(version, "bump_" + part)() - - if not version.prerelease: - version = version.bump_patch() - return version.bump_prerelease(prerelease_token) + else: + return version.bump_prerelease(prerelease_token, bump_when_empty=True) @_comparator def __eq__(self, other: Comparable) -> bool: # type: ignore diff --git a/tests/test_bump.py b/tests/test_bump.py index 34e0b2ac..fcbedf4c 100644 --- a/tests/test_bump.py +++ b/tests/test_bump.py @@ -6,6 +6,7 @@ bump_minor, bump_patch, bump_prerelease, + compare, parse_version_info, ) @@ -32,62 +33,101 @@ def test_should_versioninfo_bump_minor_and_patch(): v = parse_version_info("3.4.5") expected = parse_version_info("3.5.1") assert v.bump_minor().bump_patch() == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_patch_and_prerelease(): v = parse_version_info("3.4.5-rc.1") expected = parse_version_info("3.4.6-rc.1") assert v.bump_patch().bump_prerelease() == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_patch_and_prerelease_with_token(): v = parse_version_info("3.4.5-dev.1") expected = parse_version_info("3.4.6-dev.1") assert v.bump_patch().bump_prerelease("dev") == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_prerelease_and_build(): v = parse_version_info("3.4.5-rc.1+build.1") expected = parse_version_info("3.4.5-rc.2+build.2") assert v.bump_prerelease().bump_build() == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_prerelease_and_build_with_token(): v = parse_version_info("3.4.5-rc.1+b.1") expected = parse_version_info("3.4.5-rc.2+b.2") assert v.bump_prerelease().bump_build("b") == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_multiple(): v = parse_version_info("3.4.5-rc.1+build.1") expected = parse_version_info("3.4.5-rc.2+build.2") assert v.bump_prerelease().bump_build().bump_build() == expected + assert v.compare(expected) == -1 expected = parse_version_info("3.4.5-rc.3") assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_prerelease_with_empty_str(): v = parse_version_info("3.4.5") expected = parse_version_info("3.4.5-1") assert v.bump_prerelease("") == expected + assert v.compare(expected) == 1 def test_should_versioninfo_bump_prerelease_with_none(): v = parse_version_info("3.4.5") expected = parse_version_info("3.4.5-rc.1") assert v.bump_prerelease(None) == expected + assert v.compare(expected) == 1 + + +def test_should_versioninfo_bump_prerelease_nonnumeric(): + v = parse_version_info("3.4.5-rc1") + expected = parse_version_info("3.4.5-rc1.0") + assert v.bump_prerelease(None) == expected + assert v.compare(expected) == -1 + + +def test_should_versioninfo_bump_prerelease_nonnumeric_nine(): + v = parse_version_info("3.4.5-rc9") + expected = parse_version_info("3.4.5-rc9.0") + assert v.bump_prerelease(None) == expected + assert v.compare(expected) == -1 + + +def test_should_versioninfo_bump_prerelease_bump_patch(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.4.6-rc.1") + assert v.bump_prerelease(bump_when_empty=True) == expected + assert v.compare(expected) == -1 + + +def test_should_versioninfo_bump_patch_and_prerelease_bump_patch(): + v = parse_version_info("3.4.5") + expected = parse_version_info("3.4.7-rc.1") + assert v.bump_patch().bump_prerelease(bump_when_empty=True) == expected + assert v.compare(expected) == -1 def test_should_versioninfo_bump_build_with_empty_str(): v = parse_version_info("3.4.5") expected = parse_version_info("3.4.5+1") assert v.bump_build("") == expected + assert v.compare(expected) == 0 def test_should_versioninfo_bump_build_with_none(): v = parse_version_info("3.4.5") expected = parse_version_info("3.4.5+build.1") assert v.bump_build(None) == expected + assert v.compare(expected) == 0 def test_should_ignore_extensions_for_bump(): @@ -95,18 +135,18 @@ def test_should_ignore_extensions_for_bump(): @pytest.mark.parametrize( - "version,token,expected", + "version,token,expected,expected_compare", [ - ("3.4.5-rc.9", None, "3.4.5-rc.10"), - ("3.4.5", None, "3.4.5-rc.1"), - ("3.4.5", "dev", "3.4.5-dev.1"), - ("3.4.5", "", "3.4.5-rc.1"), + ("3.4.5-rc.9", None, "3.4.5-rc.10", -1), + ("3.4.5", None, "3.4.5-rc.1", 1), + ("3.4.5", "dev", "3.4.5-dev.1", 1), + ("3.4.5", "", "3.4.5-rc.1", 1), ], ) -def test_should_bump_prerelease(version, token, expected): +def test_should_bump_prerelease(version, token, expected, expected_compare): token = "rc" if not token else token assert bump_prerelease(version, token) == expected - + assert compare(version, expected) == expected_compare def test_should_ignore_build_on_prerelease_bump(): assert bump_prerelease("3.4.5-rc.1+build.4") == "3.4.5-rc.2" @@ -123,3 +163,4 @@ def test_should_ignore_build_on_prerelease_bump(): ) def test_should_bump_build(version, expected): assert bump_build(version) == expected + assert compare(version, expected) == 0 \ No newline at end of file diff --git a/tests/test_pysemver-cli.py b/tests/test_pysemver-cli.py index e783a0b4..5a0a2f82 100644 --- a/tests/test_pysemver-cli.py +++ b/tests/test_pysemver-cli.py @@ -55,7 +55,7 @@ def test_should_parse_cli_arguments(cli, expected): ( cmd_bump, Namespace(bump="prerelease", version="1.2.3-rc1"), - does_not_raise("1.2.3-rc2"), + does_not_raise("1.2.3-rc1.0"), ), ( cmd_bump, diff --git a/tox.ini b/tox.ini index 5c9db174..d39ce81a 100644 --- a/tox.ini +++ b/tox.ini @@ -29,6 +29,7 @@ deps = setuptools-scm setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 +downloads = true [testenv:mypy] pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy