From 7bb3160ef13cd27606d7e861fec7721822e9d545 Mon Sep 17 00:00:00 2001 From: Bas Roos Date: Sun, 26 Jan 2025 16:52:04 +0100 Subject: [PATCH 1/3] Fix python-semver#460 Raising a prerelease version always results in a newer version, and raising an empty prerelease version has the option to raise the patch version as well --- changelog.d/460.bugfix.rst | 9 ++++ docs/usage/raise-parts-of-a-version.rst | 18 +++++-- src/semver/version.py | 63 +++++++++++++++++++------ tests/test_bump.py | 55 ++++++++++++++++++--- tests/test_pysemver-cli.py | 2 +- tox.ini | 1 + 6 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 changelog.d/460.bugfix.rst 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/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst index be89cf8d..d162bf2c 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 + You can work around this by supplying the `bump_when_empty=true` argument to 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,15 @@ is not taken into account: >>> str(Version.parse("3.4.5-rc.1").bump_prerelease('')) '3.4.5-rc.2' +If the last part of the existing prerelease, split by dots (`.`), is not numeric, +we will add `.0` to ensure the new prerelease is higher than the previous one +(otherwise, raising `rc9` to `rc10` would result in a lower version, as non-numeric +parts are sorted 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 ec24fbb3..8625f5a7 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,24 @@ 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 +325,50 @@ 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:: VERSION + 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' """ 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": """ @@ -445,10 +480,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] From cfff984d0167695a96d1046c13b7c2b0daeccfce Mon Sep 17 00:00:00 2001 From: Learloj Date: Mon, 27 Jan 2025 15:01:55 +0100 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Tom Schraitle --- docs/usage/raise-parts-of-a-version.rst | 9 ++++----- src/semver/version.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/usage/raise-parts-of-a-version.rst b/docs/usage/raise-parts-of-a-version.rst index d162bf2c..101a8c35 100644 --- a/docs/usage/raise-parts-of-a-version.rst +++ b/docs/usage/raise-parts-of-a-version.rst @@ -9,7 +9,7 @@ Raising Parts of a Version 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``. - You can work around this by supplying the `bump_when_empty=true` argument to 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`. @@ -68,10 +68,9 @@ is not taken into account: >>> str(Version.parse("3.4.5-rc.1").bump_prerelease('')) '3.4.5-rc.2' -If the last part of the existing prerelease, split by dots (`.`), is not numeric, -we will add `.0` to ensure the new prerelease is higher than the previous one -(otherwise, raising `rc9` to `rc10` would result in a lower version, as non-numeric -parts are sorted alphabetically): +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 diff --git a/src/semver/version.py b/src/semver/version.py index 8625f5a7..5c08344d 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -334,7 +334,7 @@ def bump_prerelease( Raise the prerelease part of the version, return a new object but leave self untouched. - .. versionchanged:: VERSION + .. 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. From 9bd4085788584cc7324f763e84d80659d952ae87 Mon Sep 17 00:00:00 2001 From: Bas Roos Date: Mon, 27 Jan 2025 15:06:43 +0100 Subject: [PATCH 3/3] Apply other PR change requests --- src/semver/version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/semver/version.py b/src/semver/version.py index 5c08344d..f9450f95 100644 --- a/src/semver/version.py +++ b/src/semver/version.py @@ -255,7 +255,6 @@ def _increment_prerelease(string: str) -> str: :param string: the prerelease version to increment :return: the incremented string - """ match = Version._LAST_PRERELEASE.search(string) if match: @@ -353,6 +352,8 @@ def bump_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 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