Skip to content

Commit d8813b6

Browse files
LearlojBas Roostomschr
authored
Fix #460 Improve bump_prerelease to alway get a newer version (#462)
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 Co-authored-by: Tom Schraitle <tomschr@users.noreply.github.com> --------- Co-authored-by: Bas Roos <bas.roos@enreach.com> Co-authored-by: Tom Schraitle <tomschr@users.noreply.github.com>
1 parent d959aa7 commit d8813b6

File tree

6 files changed

+122
-26
lines changed

6 files changed

+122
-26
lines changed

changelog.d/460.bugfix.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
:meth:`~semver.version.Version.bump_prerelease` will now add `.0` to an
2+
existing prerelease when the last segment of the current prerelease, split by
3+
dots (`.`), is not numeric. This is to ensure the new prerelease is considered
4+
higher than the previous one.
5+
6+
:meth:`~semver.version.Version.bump_prerelease` now also support an argument
7+
`bump_when_empty` which will bump the patch version if there is no existing
8+
prerelease, to ensure the resulting version is considered a higher version than
9+
the previous one.

docs/usage/raise-parts-of-a-version.rst

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ Raising Parts of a Version
33

44
.. note::
55

6-
Keep in mind, "raising" the pre-release only will make your
7-
complete version *lower* than before.
6+
Keep in mind, by default, "raising" the pre-release for a version without an existing
7+
prerelease part, only will make your complete version *lower* than before.
88

99
For example, having version ``1.0.0`` and raising the pre-release
1010
will lead to ``1.0.0-rc.1``, but ``1.0.0-rc.1`` is smaller than ``1.0.0``.
1111

12-
If you search for a way to take into account this behavior, look for the
12+
To avoid this, set `bump_when_empty=True` in the
13+
:meth:`~semver.version.Version.bump_prerelease` method, or by using the
1314
method :meth:`~semver.version.Version.next_version`
1415
in section :ref:`increase-parts-of-a-version`.
1516

@@ -67,4 +68,14 @@ is not taken into account:
6768
>>> str(Version.parse("3.4.5-rc.1").bump_prerelease(''))
6869
'3.4.5-rc.2'
6970
71+
To ensure correct ordering, we append `.0` to the last prerelease identifier
72+
if it's not numeric. This prevents cases where `rc9` would incorrectly sort
73+
lower than `rc10` (non-numeric identifiers are compared alphabetically):
74+
75+
.. code-block:: python
76+
77+
>>> str(Version.parse("3.4.5-rc9").bump_prerelease())
78+
'3.4.5-rc9.0'
79+
>>> str(Version.parse("3.4.5-rc.9").bump_prerelease())
80+
'3.4.5-rc.10'
7081

src/semver/version.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ class Version:
7777
#: The names of the different parts of a version
7878
NAMES: ClassVar[Tuple[str, ...]] = tuple([item[1:] for item in __slots__])
7979

80-
#: Regex for number in a prerelease
80+
#: Regex for number in a build
8181
_LAST_NUMBER: ClassVar[Pattern[str]] = re.compile(r"(?:[^\d]*(\d+)[^\d]*)+")
82+
#: Regex for number in a prerelease
83+
_LAST_PRERELEASE: ClassVar[Pattern[str]] = re.compile(r"^(.*\.)?(\d+)$")
8284
#: Regex template for a semver version
8385
_REGEX_TEMPLATE: ClassVar[
8486
str
@@ -245,6 +247,23 @@ def __iter__(self) -> VersionIterator:
245247
"""Return iter(self)."""
246248
yield from self.to_tuple()
247249

250+
@staticmethod
251+
def _increment_prerelease(string: str) -> str:
252+
"""
253+
Check if the last part of a dot-separated string is numeric. If yes,
254+
increase them. Else, add '.0'
255+
256+
:param string: the prerelease version to increment
257+
:return: the incremented string
258+
"""
259+
match = Version._LAST_PRERELEASE.search(string)
260+
if match:
261+
next_ = str(int(match.group(2)) + 1)
262+
string = match.group(1) + next_ if match.group(1) else next_
263+
else:
264+
string += ".0"
265+
return string
266+
248267
@staticmethod
249268
def _increment_string(string: str) -> str:
250269
"""
@@ -305,35 +324,52 @@ def bump_patch(self) -> "Version":
305324
cls = type(self)
306325
return cls(self._major, self._minor, self._patch + 1)
307326

308-
def bump_prerelease(self, token: Optional[str] = "rc") -> "Version":
327+
def bump_prerelease(
328+
self,
329+
token: Optional[str] = "rc",
330+
bump_when_empty: Optional[bool] = False
331+
) -> "Version":
309332
"""
310333
Raise the prerelease part of the version, return a new object but leave
311334
self untouched.
312335
336+
.. versionchanged:: 3.1.0
337+
Parameter `bump_when_empty` added. When set to true, bumps the patch version
338+
when called with a version that has no prerelease segment, so the return
339+
value will be considered a newer version.
340+
341+
Adds `.0` to the prerelease if the last part of the dot-separated
342+
prerelease is not a number.
343+
313344
:param token: defaults to ``'rc'``
314345
:return: new :class:`Version` object with the raised prerelease part.
315346
The original object is not modified.
316347
317348
>>> ver = semver.parse("3.4.5")
318349
>>> ver.bump_prerelease().prerelease
319-
'rc.2'
350+
'rc.1'
320351
>>> ver.bump_prerelease('').prerelease
321352
'1'
322353
>>> ver.bump_prerelease(None).prerelease
323354
'rc.1'
355+
>>> str(ver.bump_prerelease(bump_when_empty=True))
356+
'3.4.6-rc.1'
324357
"""
325358
cls = type(self)
359+
patch = self._patch
326360
if self._prerelease is not None:
327-
prerelease = self._prerelease
328-
elif token == "":
329-
prerelease = "0"
330-
elif token is None:
331-
prerelease = "rc.0"
361+
prerelease = cls._increment_prerelease(self._prerelease)
332362
else:
333-
prerelease = str(token) + ".0"
363+
if bump_when_empty:
364+
patch += 1
365+
if token == "":
366+
prerelease = "1"
367+
elif token is None:
368+
prerelease = "rc.1"
369+
else:
370+
prerelease = str(token) + ".1"
334371

335-
prerelease = cls._increment_string(prerelease)
336-
return cls(self._major, self._minor, self._patch, prerelease)
372+
return cls(self._major, self._minor, patch, prerelease)
337373

338374
def bump_build(self, token: Optional[str] = "build") -> "Version":
339375
"""
@@ -445,10 +481,8 @@ def next_version(self, part: str, prerelease_token: str = "rc") -> "Version":
445481
# Only check the main parts:
446482
if part in cls.NAMES[:3]:
447483
return getattr(version, "bump_" + part)()
448-
449-
if not version.prerelease:
450-
version = version.bump_patch()
451-
return version.bump_prerelease(prerelease_token)
484+
else:
485+
return version.bump_prerelease(prerelease_token, bump_when_empty=True)
452486

453487
@_comparator
454488
def __eq__(self, other: Comparable) -> bool: # type: ignore

tests/test_bump.py

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
bump_minor,
77
bump_patch,
88
bump_prerelease,
9+
compare,
910
parse_version_info,
1011
)
1112

@@ -32,81 +33,120 @@ def test_should_versioninfo_bump_minor_and_patch():
3233
v = parse_version_info("3.4.5")
3334
expected = parse_version_info("3.5.1")
3435
assert v.bump_minor().bump_patch() == expected
36+
assert v.compare(expected) == -1
3537

3638

3739
def test_should_versioninfo_bump_patch_and_prerelease():
3840
v = parse_version_info("3.4.5-rc.1")
3941
expected = parse_version_info("3.4.6-rc.1")
4042
assert v.bump_patch().bump_prerelease() == expected
43+
assert v.compare(expected) == -1
4144

4245

4346
def test_should_versioninfo_bump_patch_and_prerelease_with_token():
4447
v = parse_version_info("3.4.5-dev.1")
4548
expected = parse_version_info("3.4.6-dev.1")
4649
assert v.bump_patch().bump_prerelease("dev") == expected
50+
assert v.compare(expected) == -1
4751

4852

4953
def test_should_versioninfo_bump_prerelease_and_build():
5054
v = parse_version_info("3.4.5-rc.1+build.1")
5155
expected = parse_version_info("3.4.5-rc.2+build.2")
5256
assert v.bump_prerelease().bump_build() == expected
57+
assert v.compare(expected) == -1
5358

5459

5560
def test_should_versioninfo_bump_prerelease_and_build_with_token():
5661
v = parse_version_info("3.4.5-rc.1+b.1")
5762
expected = parse_version_info("3.4.5-rc.2+b.2")
5863
assert v.bump_prerelease().bump_build("b") == expected
64+
assert v.compare(expected) == -1
5965

6066

6167
def test_should_versioninfo_bump_multiple():
6268
v = parse_version_info("3.4.5-rc.1+build.1")
6369
expected = parse_version_info("3.4.5-rc.2+build.2")
6470
assert v.bump_prerelease().bump_build().bump_build() == expected
71+
assert v.compare(expected) == -1
6572
expected = parse_version_info("3.4.5-rc.3")
6673
assert v.bump_prerelease().bump_build().bump_build().bump_prerelease() == expected
74+
assert v.compare(expected) == -1
6775

6876

6977
def test_should_versioninfo_bump_prerelease_with_empty_str():
7078
v = parse_version_info("3.4.5")
7179
expected = parse_version_info("3.4.5-1")
7280
assert v.bump_prerelease("") == expected
81+
assert v.compare(expected) == 1
7382

7483

7584
def test_should_versioninfo_bump_prerelease_with_none():
7685
v = parse_version_info("3.4.5")
7786
expected = parse_version_info("3.4.5-rc.1")
7887
assert v.bump_prerelease(None) == expected
88+
assert v.compare(expected) == 1
89+
90+
91+
def test_should_versioninfo_bump_prerelease_nonnumeric():
92+
v = parse_version_info("3.4.5-rc1")
93+
expected = parse_version_info("3.4.5-rc1.0")
94+
assert v.bump_prerelease(None) == expected
95+
assert v.compare(expected) == -1
96+
97+
98+
def test_should_versioninfo_bump_prerelease_nonnumeric_nine():
99+
v = parse_version_info("3.4.5-rc9")
100+
expected = parse_version_info("3.4.5-rc9.0")
101+
assert v.bump_prerelease(None) == expected
102+
assert v.compare(expected) == -1
103+
104+
105+
def test_should_versioninfo_bump_prerelease_bump_patch():
106+
v = parse_version_info("3.4.5")
107+
expected = parse_version_info("3.4.6-rc.1")
108+
assert v.bump_prerelease(bump_when_empty=True) == expected
109+
assert v.compare(expected) == -1
110+
111+
112+
def test_should_versioninfo_bump_patch_and_prerelease_bump_patch():
113+
v = parse_version_info("3.4.5")
114+
expected = parse_version_info("3.4.7-rc.1")
115+
assert v.bump_patch().bump_prerelease(bump_when_empty=True) == expected
116+
assert v.compare(expected) == -1
79117

80118

81119
def test_should_versioninfo_bump_build_with_empty_str():
82120
v = parse_version_info("3.4.5")
83121
expected = parse_version_info("3.4.5+1")
84122
assert v.bump_build("") == expected
123+
assert v.compare(expected) == 0
85124

86125

87126
def test_should_versioninfo_bump_build_with_none():
88127
v = parse_version_info("3.4.5")
89128
expected = parse_version_info("3.4.5+build.1")
90129
assert v.bump_build(None) == expected
130+
assert v.compare(expected) == 0
91131

92132

93133
def test_should_ignore_extensions_for_bump():
94134
assert bump_patch("3.4.5-rc1+build4") == "3.4.6"
95135

96136

97137
@pytest.mark.parametrize(
98-
"version,token,expected",
138+
"version,token,expected,expected_compare",
99139
[
100-
("3.4.5-rc.9", None, "3.4.5-rc.10"),
101-
("3.4.5", None, "3.4.5-rc.1"),
102-
("3.4.5", "dev", "3.4.5-dev.1"),
103-
("3.4.5", "", "3.4.5-rc.1"),
140+
("3.4.5-rc.9", None, "3.4.5-rc.10", -1),
141+
("3.4.5", None, "3.4.5-rc.1", 1),
142+
("3.4.5", "dev", "3.4.5-dev.1", 1),
143+
("3.4.5", "", "3.4.5-rc.1", 1),
104144
],
105145
)
106-
def test_should_bump_prerelease(version, token, expected):
146+
def test_should_bump_prerelease(version, token, expected, expected_compare):
107147
token = "rc" if not token else token
108148
assert bump_prerelease(version, token) == expected
109-
149+
assert compare(version, expected) == expected_compare
110150

111151
def test_should_ignore_build_on_prerelease_bump():
112152
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():
123163
)
124164
def test_should_bump_build(version, expected):
125165
assert bump_build(version) == expected
166+
assert compare(version, expected) == 0

tests/test_pysemver-cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_should_parse_cli_arguments(cli, expected):
5555
(
5656
cmd_bump,
5757
Namespace(bump="prerelease", version="1.2.3-rc1"),
58-
does_not_raise("1.2.3-rc2"),
58+
does_not_raise("1.2.3-rc1.0"),
5959
),
6060
(
6161
cmd_bump,

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ deps =
2929
setuptools-scm
3030
setenv =
3131
PIP_DISABLE_PIP_VERSION_CHECK = 1
32+
downloads = true
3233

3334

3435
[testenv:mypy]

0 commit comments

Comments
 (0)
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