Skip to content

Commit 2edf32d

Browse files
bjoernricksy0urself
authored andcommitted
Add: Add a VersioningScheme based on Semantic Versioning
Implement the versioning scheme defined by the SemVer spec 2.0.0.
1 parent 15075ce commit 2edf32d

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed

pontos/version/scheme/_semantic.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# Copyright (C) 2023 Greenbone Networks GmbH
2+
#
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
import re
19+
from typing import Any, Optional, Tuple
20+
21+
from semver import VersionInfo
22+
23+
from pontos.version.calculator import VersionCalculator
24+
from pontos.version.errors import VersionError
25+
26+
from ..version import Version
27+
from ._scheme import VersioningScheme
28+
29+
_PRE_RELEASE_REGEXP = re.compile(
30+
r"^(?P<name>[a-zA-Z]+)(?P<version>0|[1-9][0-9]*)$"
31+
)
32+
33+
34+
class SemanticVersion(Version):
35+
"""
36+
A Version implementation based on
37+
`Semantic Versioning<https://semver.org/>`_
38+
"""
39+
40+
def __init__(
41+
self,
42+
version: str,
43+
) -> None:
44+
self._version_info = VersionInfo.parse(version)
45+
self._parse_pre_release()
46+
47+
def _parse_pre_release(self) -> None:
48+
if self._version_info.prerelease:
49+
match = _PRE_RELEASE_REGEXP.match(self._version_info.prerelease)
50+
if not match:
51+
raise VersionError(
52+
f"Invalid prerelease {self._version_info.prerelease} in "
53+
f"{self._version_info}"
54+
)
55+
56+
self._pre_release = (
57+
match.group("name"),
58+
int(match.group("version")),
59+
)
60+
else:
61+
self._pre_release = None
62+
63+
@property
64+
def pre(self) -> Optional[Tuple[str, int]]:
65+
"""The pre-release segment of the version."""
66+
return self._pre_release
67+
68+
@property
69+
def dev(self) -> Optional[int]:
70+
"""The development number of the version."""
71+
return self.pre[1] if self.is_dev_release else None
72+
73+
@property
74+
def local(self) -> Optional[str]:
75+
"""The local version segment of the version."""
76+
return self._version_info.build
77+
78+
@property
79+
def is_pre_release(self) -> bool:
80+
"""Whether this version is a pre-release."""
81+
return self.pre is not None
82+
83+
@property
84+
def is_dev_release(self) -> bool:
85+
"""Whether this version is a development release."""
86+
return self.pre and self.pre[0] == "dev"
87+
88+
@property
89+
def is_alpha_release(self) -> bool:
90+
"""Whether this version is a alpha release."""
91+
return self.pre is not None and self.pre[0] == "alpha"
92+
93+
@property
94+
def is_beta_release(self) -> bool:
95+
"""Whether this version is a beta release."""
96+
return self.pre is not None and self.pre[0] == "beta"
97+
98+
@property
99+
def is_release_candidate(self) -> bool:
100+
"""Whether this version is a release candidate."""
101+
return self.pre is not None and self.pre[0] == "rc"
102+
103+
@property
104+
def major(self) -> int:
105+
"""The first item of :attr:`release` or ``0`` if unavailable."""
106+
return self._version_info.major
107+
108+
@property
109+
def minor(self) -> int:
110+
"""The second item of :attr:`release` or ``0`` if unavailable."""
111+
return self._version_info.minor
112+
113+
@property
114+
def patch(self) -> int:
115+
"""The third item of :attr:`release` or ``0`` if unavailable."""
116+
return self._version_info.patch
117+
118+
def __eq__(self, other: Any) -> bool:
119+
if not isinstance(other, Version):
120+
raise ValueError(f"Can't compare {type(self)} with {type(other)}")
121+
if not isinstance(other, type(self)):
122+
other = self.from_version(other)
123+
124+
return self._version_info == other._version_info
125+
126+
def __ne__(self, other: Any) -> bool:
127+
if not isinstance(other, Version):
128+
raise ValueError(f"Can't compare {type(self)} with {type(other)}")
129+
if not isinstance(other, type(self)):
130+
other = self.from_version(other)
131+
132+
return self._version_info != other._version_info
133+
134+
def __str__(self) -> str:
135+
"""A string representation of the version"""
136+
return str(self._version_info)
137+
138+
@classmethod
139+
def from_string(cls, version: str) -> "SemanticVersion":
140+
"""
141+
Create a version from a version string
142+
143+
Args:
144+
version: Version string to parse
145+
146+
Raises:
147+
VersionError: If the version string is invalid.
148+
149+
Returns:
150+
A new version instance
151+
"""
152+
try:
153+
return cls(version)
154+
except ValueError as e:
155+
raise VersionError(e) from None
156+
157+
@classmethod
158+
def from_version(cls, version: "Version") -> "SemanticVersion":
159+
"""
160+
Convert a version (if necessary)
161+
162+
This method can be used to convert version instances from different
163+
versioning schemes.
164+
"""
165+
166+
if isinstance(version, cls):
167+
return version
168+
169+
if version.is_dev_release:
170+
return cls.from_string(
171+
f"{version.major}."
172+
f"{version.minor}."
173+
f"{version.patch}"
174+
f"-dev{version.dev}"
175+
f"{'+' + version.local if version.local else ''}"
176+
)
177+
if version.is_pre_release:
178+
return cls.from_string(
179+
f"{version.major}."
180+
f"{version.minor}."
181+
f"{version.patch}"
182+
f"-{version.pre[0]}{version.pre[1]}"
183+
f"{'+' + version.local if version.local else ''}"
184+
)
185+
186+
return cls.from_string(str(version))
187+
188+
189+
# pylint: disable=protected-access
190+
class SemanticVersionCalculator(VersionCalculator):
191+
version_cls = SemanticVersion
192+
193+
@classmethod
194+
def next_dev_version(cls, current_version: Version) -> Version:
195+
"""
196+
Get the next development version from a valid version
197+
"""
198+
if current_version.is_dev_release:
199+
return cls.version_from_string(
200+
f"{current_version.major}."
201+
f"{current_version.minor}."
202+
f"{current_version.patch}"
203+
f"-dev{current_version.pre[1] + 1}"
204+
)
205+
206+
if current_version.is_pre_release:
207+
return cls.version_from_string(
208+
f"{current_version.major}."
209+
f"{current_version.minor}."
210+
f"{current_version.patch}-"
211+
f"{current_version.pre[0]}{current_version.pre[1]}+dev1"
212+
)
213+
214+
return cls.version_from_string(
215+
f"{current_version.major}."
216+
f"{current_version.minor}."
217+
f"{current_version.patch + 1}"
218+
"-dev1"
219+
)
220+
221+
@classmethod
222+
def next_alpha_version(cls, current_version: Version) -> Version:
223+
"""
224+
Get the next alpha version from a valid version
225+
"""
226+
if current_version.is_dev_release:
227+
return cls.version_from_string(
228+
f"{current_version.major}."
229+
f"{current_version.minor}."
230+
f"{current_version.patch}"
231+
"-alpha1"
232+
)
233+
if current_version.is_alpha_release:
234+
return cls.version_from_string(
235+
f"{current_version.major}."
236+
f"{current_version.minor}."
237+
f"{current_version.patch}"
238+
f"-alpha{current_version.pre[1] + 1}"
239+
)
240+
return cls.version_from_string(
241+
f"{current_version.major}."
242+
f"{current_version.minor}."
243+
f"{current_version.patch + 1}"
244+
"-alpha1"
245+
)
246+
247+
@classmethod
248+
def next_beta_version(cls, current_version: Version) -> Version:
249+
"""
250+
Get the next alpha version from a valid version
251+
"""
252+
if current_version.is_dev_release or current_version.is_alpha_release:
253+
return cls.version_from_string(
254+
f"{current_version.major}."
255+
f"{current_version.minor}."
256+
f"{current_version.patch}"
257+
"-beta1"
258+
)
259+
if current_version.is_beta_release:
260+
return cls.version_from_string(
261+
f"{current_version.major}."
262+
f"{current_version.minor}."
263+
f"{current_version.patch}"
264+
f"-beta{current_version.pre[1] + 1}"
265+
)
266+
return cls.version_from_string(
267+
f"{current_version.major}."
268+
f"{current_version.minor}."
269+
f"{current_version.patch + 1}"
270+
"-beta1"
271+
)
272+
273+
@classmethod
274+
def next_release_candidate_version(
275+
cls, current_version: Version
276+
) -> Version:
277+
"""
278+
Get the next alpha version from a valid version
279+
"""
280+
if (
281+
current_version.is_dev_release
282+
or current_version.is_alpha_release
283+
or current_version.is_beta_release
284+
):
285+
return cls.version_from_string(
286+
f"{current_version.major}."
287+
f"{current_version.minor}."
288+
f"{current_version.patch}"
289+
"-rc1"
290+
)
291+
if current_version.is_release_candidate:
292+
return cls.version_from_string(
293+
f"{current_version.major}."
294+
f"{current_version.minor}."
295+
f"{current_version.patch}"
296+
f"-rc{current_version.pre[1] + 1}"
297+
)
298+
return cls.version_from_string(
299+
f"{current_version.major}."
300+
f"{current_version.minor}."
301+
f"{current_version.patch + 1}"
302+
"-rc1"
303+
)
304+
305+
306+
class SemanticVersioningScheme(VersioningScheme):
307+
version_cls = SemanticVersion
308+
version_calculator_cls = SemanticVersionCalculator

0 commit comments

Comments
 (0)