Coverage for pyTooling/Versioning/__init__.py: 88%
675 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-25 22:22 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2025 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Implementation of semantic and date versioning version-numbers.
34.. hint:: See :ref:`high-level help <VERSIONING>` for explanations and usage examples.
35"""
36from enum import Flag, Enum
37from re import compile as re_compile
38from sys import version_info # needed for versions before Python 3.11
39from typing import Optional as Nullable, Union, Callable, Any
41try:
42 from pyTooling.Decorators import export, readonly
43 from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
44 from pyTooling.Exceptions import ToolingException
45 from pyTooling.Common import getFullyQualifiedName
46except (ImportError, ModuleNotFoundError): # pragma: no cover
47 print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!")
49 try:
50 from Decorators import export, readonly
51 from MetaClasses import ExtendedType, abstractmethod, mustoverride
52 from Exceptions import ToolingException
53 from Common import getFullyQualifiedName
54 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
55 print("[pyTooling.Versioning] Could not import directly!")
56 raise ex
59@export
60class Parts(Flag):
61 """Enumeration describing parts of a version number that can be present."""
62 Unknown = 0 #: Undocumented
63 Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``).
64 Year = 1 #: Year is present. (e.g. X in ``XXXX.10``).
65 Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
66 Month = 2 #: Month is present. (e.g. X in ``2024.YY``).
67 Week = 2 #: Week is present. (e.g. X in ``2024.YY``).
68 Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
69 Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
70 Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``).
71 Level = 8 #: Release level is present.
72 Dev = 16 #: Development part is present.
73 Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
74 Post = 64 #: Post-release number is present.
75 Prefix = 128 #: Prefix is present.
76 Postfix = 256 #: Postfix is present.
77 Hash = 512 #: Hash is present.
78# AHead = 256
81@export
82class ReleaseLevel(Enum):
83 """Enumeration describing the version's maturity level."""
84 Final = 0 #:
85 ReleaseCandidate = -10 #:
86 Development = -20 #:
87 Gamma = -30 #:
88 Beta = -40 #:
89 Alpha = -50 #:
91 def __eq__(self, other: Any):
92 if isinstance(other, str): 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 other = ReleaseLevel(other)
94 if not isinstance(other, ReleaseLevel): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
96 if version_info >= (3, 11): # pragma: no cover
97 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}")
98 raise ex
100 return self is other
102 def __ne__(self, other: Any):
103 if isinstance(other, str):
104 other = ReleaseLevel(other)
105 if not isinstance(other, ReleaseLevel):
106 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
107 if version_info >= (3, 11): # pragma: no cover
108 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}")
109 raise ex
111 return self is not other
113 def __lt__(self, other: Any):
114 if isinstance(other, str): 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 other = ReleaseLevel(other)
116 if not isinstance(other, ReleaseLevel): 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true
117 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
118 if version_info >= (3, 11): # pragma: no cover
119 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}")
120 raise ex
122 return self.value < other.value
124 def __le__(self, other: Any):
125 if isinstance(other, str):
126 other = ReleaseLevel(other)
127 if not isinstance(other, ReleaseLevel):
128 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
129 if version_info >= (3, 11): # pragma: no cover
130 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}")
131 raise ex
133 return self.value <= other.value
135 def __gt__(self, other: Any):
136 if isinstance(other, str): 136 ↛ 137line 136 didn't jump to line 137 because the condition on line 136 was never true
137 other = ReleaseLevel(other)
138 if not isinstance(other, ReleaseLevel): 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
140 if version_info >= (3, 11): # pragma: no cover
141 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}")
142 raise ex
144 return self.value > other.value
146 def __ge__(self, other: Any):
147 if isinstance(other, str):
148 other = ReleaseLevel(other)
149 if not isinstance(other, ReleaseLevel):
150 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
151 if version_info >= (3, 11): # pragma: no cover
152 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}")
153 raise ex
155 return self.value >= other.value
157 def __hash__(self) -> int:
158 return hash(self.value)
160 def __str__(self) -> str:
161 if self is ReleaseLevel.Final:
162 return "final"
163 elif self is ReleaseLevel.ReleaseCandidate:
164 return "rc"
165 elif self is ReleaseLevel.Development: 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 return "dev"
167 elif self is ReleaseLevel.Beta: 167 ↛ 168line 167 didn't jump to line 168 because the condition on line 167 was never true
168 return "beta"
169 elif self is ReleaseLevel.Alpha: 169 ↛ 172line 169 didn't jump to line 172 because the condition on line 169 was always true
170 return "alpha"
172 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
175@export
176class Flags(Flag):
177 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
178 NoVCS = 0 #: No Version Control System VCS
179 Clean = 1 #: A versioned build was created from a *clean* working directory.
180 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
182 CVS = 16 #: Concurrent Versions System (CVS)
183 SVN = 32 #: Subversion (SVN)
184 Git = 64 #: Git
185 Hg = 128 #: Mercurial (Hg)
188@export
189def WordSizeValidator(
190 bits: Nullable[int] = None,
191 majorBits: Nullable[int] = None,
192 minorBits: Nullable[int] = None,
193 microBits: Nullable[int] = None,
194 buildBits: Nullable[int] = None
195):
196 """
197 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
199 :param bits: Number of bits to encode any positive version number part.
200 :param majorBits: Number of bits to encode a positive major number in a version.
201 :param minorBits: Number of bits to encode a positive minor number in a version.
202 :param microBits: Number of bits to encode a positive micro number in a version.
203 :param buildBits: Number of bits to encode a positive build number in a version.
204 :return: A validation function for Version instances.
205 """
206 majorMax = minorMax = microMax = buildMax = -1
207 if bits is not None:
208 majorMax = minorMax = microMax = buildMax = 2**bits - 1
210 if majorBits is not None:
211 majorMax = 2**majorBits - 1
212 if minorBits is not None:
213 minorMax = 2**minorBits - 1
214 if microBits is not None:
215 microMax = 2 ** microBits - 1
216 if buildBits is not None: 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true
217 buildMax = 2**buildBits - 1
219 def validator(version: SemanticVersion) -> bool:
220 if Parts.Major in version._parts and version._major > majorMax:
221 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
223 if Parts.Minor in version._parts and version._minor > minorMax:
224 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
226 if Parts.Micro in version._parts and version._micro > microMax:
227 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
229 if Parts.Build in version._parts and version._build > buildMax: 229 ↛ 230line 229 didn't jump to line 230 because the condition on line 229 was never true
230 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
232 return True
234 return validator
237@export
238def MaxValueValidator(
239 max: Nullable[int] = None,
240 majorMax: Nullable[int] = None,
241 minorMax: Nullable[int] = None,
242 microMax: Nullable[int] = None,
243 buildMax: Nullable[int] = None
244):
245 """
246 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
248 :param max: The upper bound for any positive version number part.
249 :param majorMax: The upper bound for the positive major number.
250 :param minorMax: The upper bound for the positive minor number.
251 :param microMax: The upper bound for the positive micro number.
252 :param buildMax: The upper bound for the positive build number.
253 :return: A validation function for Version instances.
254 """
255 if max is not None: 255 ↛ 258line 255 didn't jump to line 258 because the condition on line 255 was always true
256 majorMax = minorMax = microMax = buildMax = max
258 def validator(version: SemanticVersion) -> bool:
259 if Parts.Major in version._parts and version._major > majorMax:
260 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
262 if Parts.Minor in version._parts and version._minor > minorMax:
263 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
265 if Parts.Micro in version._parts and version._micro > microMax:
266 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
268 if Parts.Build in version._parts and version._build > buildMax: 268 ↛ 269line 268 didn't jump to line 269 because the condition on line 268 was never true
269 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
271 return True
273 return validator
276@export
277class Version(metaclass=ExtendedType, slots=True):
278 """Base-class for a version representation."""
280 __hash: Nullable[int] #: once computed hash of the object
282 _parts: Parts #: Integer flag enumeration of present parts in a version number.
283 _prefix: str #: Prefix string
284 _major: int #: Major number part of the version number.
285 _minor: int #: Minor number part of the version number.
286 _micro: int #: Micro number part of the version number.
287 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
288 _releaseNumber: int #: Release number (Python calls this a serial).
289 _post: int #: Post-release version number part.
290 _dev: int #: Development number
291 _build: int #: Build number part of the version number.
292 _postfix: str #: Postfix string
293 _hash: str #: Hash from version control system.
294 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
296 def __init__(
297 self,
298 major: int,
299 minor: Nullable[int] = None,
300 micro: Nullable[int] = None,
301 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
302 number: Nullable[int] = None,
303 post: Nullable[int] = None,
304 dev: Nullable[int] = None,
305 *,
306 build: Nullable[int] = None,
307 postfix: Nullable[str] = None,
308 prefix: Nullable[str] = None,
309 hash: Nullable[str] = None,
310 flags: Flags = Flags.NoVCS
311 ) -> None:
312 """
313 Initializes a version number representation.
315 :param major: Major number part of the version number.
316 :param minor: Minor number part of the version number.
317 :param micro: Micro (patch) number part of the version number.
318 :param level: Release level (alpha, beta, release candidate, final, ...) of the version number.
319 :param number: Release number part (in combination with release level) of the version number.
320 :param post: Post number part of the version number.
321 :param dev: Development number part of the version number.
322 :param build: Build number part of the version number.
323 :param postfix: The version number's postfix.
324 :param prefix: The version number's prefix.
325 :param hash: Postfix string.
326 :param flags: The version number's flags.
327 :raises TypeError: If parameter 'major' is not of type int.
328 :raises ValueError: If parameter 'major' is a negative number.
329 :raises TypeError: If parameter 'minor' is not of type int.
330 :raises ValueError: If parameter 'minor' is a negative number.
331 :raises TypeError: If parameter 'micro' is not of type int.
332 :raises ValueError: If parameter 'micro' is a negative number.
333 :raises TypeError: If parameter 'build' is not of type int.
334 :raises ValueError: If parameter 'build' is a negative number.
335 :raises TypeError: If parameter 'prefix' is not of type str.
336 :raises TypeError: If parameter 'postfix' is not of type str.
337 """
338 self.__hash = None
340 if not isinstance(major, int):
341 raise TypeError("Parameter 'major' is not of type 'int'.")
342 elif major < 0:
343 raise ValueError("Parameter 'major' is negative.")
345 self._parts = Parts.Major
346 self._major = major
348 if minor is not None:
349 if not isinstance(minor, int):
350 raise TypeError("Parameter 'minor' is not of type 'int'.")
351 elif minor < 0:
352 raise ValueError("Parameter 'minor' is negative.")
354 self._parts |= Parts.Minor
355 self._minor = minor
356 else:
357 self._minor = 0
359 if micro is not None:
360 if not isinstance(micro, int):
361 raise TypeError("Parameter 'micro' is not of type 'int'.")
362 elif micro < 0:
363 raise ValueError("Parameter 'micro' is negative.")
365 self._parts |= Parts.Micro
366 self._micro = micro
367 else:
368 self._micro = 0
370 if level is None:
371 raise ValueError("Parameter 'level' is None.")
372 elif not isinstance(level, ReleaseLevel):
373 raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
374 elif level is ReleaseLevel.Final:
375 if number is not None:
376 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
378 self._parts |= Parts.Level
379 self._releaseLevel = level
380 self._releaseNumber = 0
381 else:
382 self._parts |= Parts.Level
383 self._releaseLevel = level
385 if number is not None:
386 if not isinstance(number, int):
387 raise TypeError("Parameter 'number' is not of type 'int'.")
388 elif number < 0:
389 raise ValueError("Parameter 'number' is negative.")
391 self._releaseNumber = number
392 else:
393 self._releaseNumber = 0
395 if dev is not None:
396 if not isinstance(dev, int):
397 raise TypeError("Parameter 'dev' is not of type 'int'.")
398 elif dev < 0:
399 raise ValueError("Parameter 'dev' is negative.")
401 self._parts |= Parts.Dev
402 self._dev = dev
403 else:
404 self._dev = 0
406 if post is not None:
407 if not isinstance(post, int):
408 raise TypeError("Parameter 'post' is not of type 'int'.")
409 elif post < 0:
410 raise ValueError("Parameter 'post' is negative.")
412 self._parts |= Parts.Post
413 self._post = post
414 else:
415 self._post = 0
417 if build is not None:
418 if not isinstance(build, int):
419 raise TypeError("Parameter 'build' is not of type 'int'.")
420 elif build < 0:
421 raise ValueError("Parameter 'build' is negative.")
423 self._build = build
424 self._parts |= Parts.Build
425 else:
426 self._build = 0
428 if postfix is not None:
429 if not isinstance(postfix, str):
430 raise TypeError("Parameter 'postfix' is not of type 'str'.")
432 self._parts |= Parts.Postfix
433 self._postfix = postfix
434 else:
435 self._postfix = ""
437 if prefix is not None:
438 if not isinstance(prefix, str):
439 raise TypeError("Parameter 'prefix' is not of type 'str'.")
441 self._parts |= Parts.Prefix
442 self._prefix = prefix
443 else:
444 self._prefix = ""
446 if hash is not None:
447 if not isinstance(hash, str):
448 raise TypeError("Parameter 'hash' is not of type 'str'.")
450 self._parts |= Parts.Hash
451 self._hash = hash
452 else:
453 self._hash = ""
455 if flags is None:
456 raise ValueError("Parameter 'flags' is None.")
457 elif not isinstance(flags, Flags):
458 raise TypeError("Parameter 'flags' is not of type 'Flags'.")
460 self._flags = flags
462 @classmethod
463 @abstractmethod
464 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version":
465 """Parse a version string and return a Version instance."""
467 @readonly
468 def Parts(self) -> Parts:
469 """
470 Read-only property to access the used parts of this version number.
472 :return: A flag enumeration of used version number parts.
473 """
474 return self._parts
476 @readonly
477 def Prefix(self) -> str:
478 """
479 Read-only property to access the version number's prefix.
481 :return: The prefix of the version number.
482 """
483 return self._prefix
485 @readonly
486 def Major(self) -> int:
487 """
488 Read-only property to access the major number.
490 :return: The major number.
491 """
492 return self._major
494 @readonly
495 def Minor(self) -> int:
496 """
497 Read-only property to access the minor number.
499 :return: The minor number.
500 """
501 return self._minor
503 @readonly
504 def Micro(self) -> int:
505 """
506 Read-only property to access the micro number.
508 :return: The micro number.
509 """
510 return self._micro
512 @readonly
513 def ReleaseLevel(self) -> ReleaseLevel:
514 """
515 Read-only property to access the release level.
517 :return: The release level.
518 """
519 return self._releaseLevel
521 @readonly
522 def ReleaseNumber(self) -> int:
523 """
524 Read-only property to access the release number.
526 :return: The release number.
527 """
528 return self._releaseNumber
530 @readonly
531 def Post(self) -> int:
532 """
533 Read-only property to access the post number.
535 :return: The post number.
536 """
537 return self._post
539 @readonly
540 def Dev(self) -> int:
541 """
542 Read-only property to access the development number.
544 :return: The development number.
545 """
546 return self._dev
548 @readonly
549 def Build(self) -> int:
550 """
551 Read-only property to access the build number.
553 :return: The build number.
554 """
555 return self._build
557 @readonly
558 def Postfix(self) -> str:
559 """
560 Read-only property to access the version number's postfix.
562 :return: The postfix of the version number.
563 """
564 return self._postfix
566 @readonly
567 def Hash(self) -> str:
568 """
569 Read-only property to access the version number's hash.
571 :return: The hash.
572 """
573 return self._hash
575 @readonly
576 def Flags(self) -> Flags:
577 """
578 Read-only property to access the version number's flags.
580 :return: The flags of the version number.
581 """
582 return self._flags
584 def _equal(self, left: "Version", right: "Version") -> Nullable[bool]:
585 """
586 Private helper method to compute the equality of two :class:`Version` instances.
588 :param left: Left parameter.
589 :param right: Right parameter.
590 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
591 """
592 return (
593 (left._major == right._major) and
594 (left._minor == right._minor) and
595 (left._micro == right._micro) and
596 (left._releaseLevel == right._releaseLevel) and
597 (left._releaseNumber == right._releaseNumber) and
598 (left._post == right._post) and
599 (left._dev == right._dev) and
600 (left._build == right._build) and
601 (left._postfix == right._postfix)
602 )
604 def _compare(self, left: "Version", right: "Version") -> Nullable[bool]:
605 """
606 Private helper method to compute the comparison of two :class:`Version` instances.
608 :param left: Left parameter.
609 :param right: Right parameter.
610 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
611 False if ``left`` is greater than ``right``. |br|
612 Otherwise it's None (both parameters are equal).
613 """
614 if left._major < right._major:
615 return True
616 elif left._major > right._major:
617 return False
619 if left._minor < right._minor:
620 return True
621 elif left._minor > right._minor:
622 return False
624 if left._micro < right._micro:
625 return True
626 elif left._micro > right._micro:
627 return False
629 if left._releaseLevel < right._releaseLevel: 629 ↛ 630line 629 didn't jump to line 630 because the condition on line 629 was never true
630 return True
631 elif left._releaseLevel > right._releaseLevel: 631 ↛ 632line 631 didn't jump to line 632 because the condition on line 631 was never true
632 return False
634 if left._releaseNumber < right._releaseNumber: 634 ↛ 635line 634 didn't jump to line 635 because the condition on line 634 was never true
635 return True
636 elif left._releaseNumber > right._releaseNumber: 636 ↛ 637line 636 didn't jump to line 637 because the condition on line 636 was never true
637 return False
639 if left._post < right._post: 639 ↛ 640line 639 didn't jump to line 640 because the condition on line 639 was never true
640 return True
641 elif left._post > right._post: 641 ↛ 642line 641 didn't jump to line 642 because the condition on line 641 was never true
642 return False
644 if left._dev < right._dev: 644 ↛ 645line 644 didn't jump to line 645 because the condition on line 644 was never true
645 return True
646 elif left._dev > right._dev: 646 ↛ 647line 646 didn't jump to line 647 because the condition on line 646 was never true
647 return False
649 if left._build < right._build: 649 ↛ 650line 649 didn't jump to line 650 because the condition on line 649 was never true
650 return True
651 elif left._build > right._build: 651 ↛ 652line 651 didn't jump to line 652 because the condition on line 651 was never true
652 return False
654 return None
656 def _minimum(self, actual: "Version", expected: "Version") -> Nullable[bool]:
657 exactMajor = Parts.Minor in expected._parts
658 exactMinor = Parts.Micro in expected._parts
660 if exactMajor and actual._major != expected._major: 660 ↛ 661line 660 didn't jump to line 661 because the condition on line 660 was never true
661 return False
662 elif not exactMajor and actual._major < expected._major:
663 return False
665 if exactMinor and actual._minor != expected._minor: 665 ↛ 666line 665 didn't jump to line 666 because the condition on line 665 was never true
666 return False
667 elif not exactMinor and actual._minor < expected._minor:
668 return False
670 if Parts.Micro in expected._parts:
671 return actual._micro >= expected._micro
673 return True
675 def _format(self, formatSpec: str) -> str:
676 """
677 Return a string representation of this version number according to the format specification.
679 .. topic:: Format Specifiers
681 * ``%p`` - prefix
682 * ``%M`` - major number
683 * ``%m`` - minor number
684 * ``%u`` - micro number
685 * ``%b`` - build number
687 :param formatSpec: The format specification.
688 :return: Formatted version number.
689 """
690 if formatSpec == "":
691 return self.__str__()
693 result = formatSpec
694 result = result.replace("%p", str(self._prefix))
695 result = result.replace("%M", str(self._major))
696 result = result.replace("%m", str(self._minor))
697 result = result.replace("%u", str(self._micro))
698 result = result.replace("%b", str(self._build))
699 result = result.replace("%r", str(self._releaseLevel)[0])
700 result = result.replace("%R", str(self._releaseLevel))
701 result = result.replace("%n", str(self._releaseNumber))
702 result = result.replace("%d", str(self._dev))
703 result = result.replace("%P", str(self._postfix))
705 return result
707 @mustoverride
708 def __eq__(self, other: Union["Version", str, int, None]) -> bool:
709 """
710 Compare two version numbers for equality.
712 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
713 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
714 number is assumed (all other parts are zero).
716 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
717 number.
719 :param other: Parameter to compare against.
720 :returns: ``True``, if both version numbers are equal.
721 :raises ValueError: If parameter ``other`` is None.
722 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
723 """
724 if other is None:
725 raise ValueError(f"Second operand is None.")
726 elif isinstance(other, self.__class__):
727 pass
728 elif isinstance(other, str):
729 other = self.__class__.Parse(other)
730 elif isinstance(other, int):
731 other = self.__class__(major=other)
732 else:
733 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
734 if version_info >= (3, 11): # pragma: no cover
735 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
736 raise ex
738 return self._equal(self, other)
740 @mustoverride
741 def __ne__(self, other: Union["Version", str, int, None]) -> bool:
742 """
743 Compare two version numbers for inequality.
745 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
746 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
747 number is assumed (all other parts are zero).
749 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
750 number.
752 :param other: Parameter to compare against.
753 :returns: ``True``, if both version numbers are not equal.
754 :raises ValueError: If parameter ``other`` is None.
755 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
756 """
757 if other is None:
758 raise ValueError(f"Second operand is None.")
759 elif isinstance(other, self.__class__):
760 pass
761 elif isinstance(other, str):
762 other = self.__class__.Parse(other)
763 elif isinstance(other, int):
764 other = self.__class__(major=other)
765 else:
766 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
767 if version_info >= (3, 11): # pragma: no cover
768 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
769 raise ex
771 return not self._equal(self, other)
773 @mustoverride
774 def __lt__(self, other: Union["Version", str, int, None]) -> bool:
775 """
776 Compare two version numbers if the version is less than the second operand.
778 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
779 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
780 number is assumed (all other parts are zero).
782 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
783 number.
785 :param other: Parameter to compare against.
786 :returns: ``True``, if version is less than the second operand.
787 :raises ValueError: If parameter ``other`` is None.
788 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
789 """
790 if other is None:
791 raise ValueError(f"Second operand is None.")
792 elif isinstance(other, self.__class__):
793 pass
794 elif isinstance(other, str):
795 other = self.__class__.Parse(other)
796 elif isinstance(other, int):
797 other = self.__class__(major=other)
798 else:
799 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
800 if version_info >= (3, 11): # pragma: no cover
801 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
802 raise ex
804 result = self._compare(self, other)
805 return result if result is not None else False
807 @mustoverride
808 def __le__(self, other: Union["Version", str, int, None]) -> bool:
809 """
810 Compare two version numbers if the version is less than or equal the second operand.
812 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
813 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
814 number is assumed (all other parts are zero).
816 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
817 number.
819 :param other: Parameter to compare against.
820 :returns: ``True``, if version is less than or equal the second operand.
821 :raises ValueError: If parameter ``other`` is None.
822 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
823 """
824 if other is None:
825 raise ValueError(f"Second operand is None.")
826 elif isinstance(other, self.__class__):
827 pass
828 elif isinstance(other, str):
829 other = self.__class__.Parse(other)
830 elif isinstance(other, int):
831 other = self.__class__(major=other)
832 else:
833 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.")
834 if version_info >= (3, 11): # pragma: no cover
835 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
836 raise ex
838 result = self._compare(self, other)
839 return result if result is not None else True
841 @mustoverride
842 def __gt__(self, other: Union["Version", str, int, None]) -> bool:
843 """
844 Compare two version numbers if the version is greater than the second operand.
846 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
847 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
848 number is assumed (all other parts are zero).
850 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
851 number.
853 :param other: Parameter to compare against.
854 :returns: ``True``, if version is greater than the second operand.
855 :raises ValueError: If parameter ``other`` is None.
856 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
857 """
858 if other is None:
859 raise ValueError(f"Second operand is None.")
860 elif isinstance(other, self.__class__):
861 pass
862 elif isinstance(other, str):
863 other = self.__class__.Parse(other)
864 elif isinstance(other, int):
865 other = self.__class__(major=other)
866 else:
867 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
868 if version_info >= (3, 11): # pragma: no cover
869 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
870 raise ex
872 return not self.__le__(other)
874 @mustoverride
875 def __ge__(self, other: Union["Version", str, int, None]) -> bool:
876 """
877 Compare two version numbers if the version is greater than or equal the second operand.
879 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
880 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
881 number is assumed (all other parts are zero).
883 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
884 number.
886 :param other: Parameter to compare against.
887 :returns: ``True``, if version is greater than or equal the second operand.
888 :raises ValueError: If parameter ``other`` is None.
889 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
890 """
891 if other is None:
892 raise ValueError(f"Second operand is None.")
893 elif isinstance(other, self.__class__):
894 pass
895 elif isinstance(other, str):
896 other = self.__class__.Parse(other)
897 elif isinstance(other, int):
898 other = self.__class__(major=other)
899 else:
900 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
901 if version_info >= (3, 11): # pragma: no cover
902 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
903 raise ex
905 return not self.__lt__(other)
907 def __rshift__(self, other: Union["Version", str, int, None]) -> bool:
908 if other is None:
909 raise ValueError(f"Second operand is None.")
910 elif isinstance(other, self.__class__):
911 pass
912 elif isinstance(other, str):
913 other = self.__class__.Parse(other)
914 elif isinstance(other, int):
915 other = self.__class__(major=other)
916 else:
917 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by %= operator.")
918 if version_info >= (3, 11): # pragma: no cover
919 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
920 raise ex
922 return self._minimum(self, other)
924 def __hash__(self) -> int:
925 if self.__hash is None: 925 ↛ 940line 925 didn't jump to line 940 because the condition on line 925 was always true
926 self.__hash = hash((
927 self._prefix,
928 self._major,
929 self._minor,
930 self._micro,
931 self._releaseLevel,
932 self._releaseNumber,
933 self._post,
934 self._dev,
935 self._build,
936 self._postfix,
937 self._hash,
938 self._flags
939 ))
940 return self.__hash
943@export
944class SemanticVersion(Version):
945 """Representation of a semantic version number like ``3.7.12``."""
947 _PATTERN = re_compile(
948 r"^"
949 r"(?P<prefix>[a-zA-Z]*)"
950 r"(?P<major>\d+)"
951 r"(?:\.(?P<minor>\d+))?"
952 r"(?:\.(?P<micro>\d+))?"
953 r"(?:"
954 r"(?:\.(?P<build>\d+))"
955 r"|"
956 r"(?:[-](?P<release>dev|final))"
957 r"|"
958 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
959 r")?"
960 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
961 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
962 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
963 r"$"
964 )
965# QUESTION: was this how many commits a version is ahead of the last tagged version?
966# ahead: int = 0
968 def __init__(
969 self,
970 major: int,
971 minor: Nullable[int] = None,
972 micro: Nullable[int] = None,
973 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
974 number: Nullable[int] = None,
975 post: Nullable[int] = None,
976 dev: Nullable[int] = None,
977 *,
978 build: Nullable[int] = None,
979 postfix: Nullable[str] = None,
980 prefix: Nullable[str] = None,
981 hash: Nullable[str] = None,
982 flags: Flags = Flags.NoVCS
983 ) -> None:
984 """
985 Initializes a semantic version number representation.
987 :param major: Major number part of the version number.
988 :param minor: Minor number part of the version number.
989 :param micro: Micro (patch) number part of the version number.
990 :param build: Build number part of the version number.
991 :param level: tbd
992 :param number: tbd
993 :param post: Post number part of the version number.
994 :param dev: Development number part of the version number.
995 :param prefix: The version number's prefix.
996 :param postfix: The version number's postfix.
997 :param flags: The version number's flags.
998 :param hash: tbd
999 :raises TypeError: If parameter 'major' is not of type int.
1000 :raises ValueError: If parameter 'major' is a negative number.
1001 :raises TypeError: If parameter 'minor' is not of type int.
1002 :raises ValueError: If parameter 'minor' is a negative number.
1003 :raises TypeError: If parameter 'micro' is not of type int.
1004 :raises ValueError: If parameter 'micro' is a negative number.
1005 :raises TypeError: If parameter 'build' is not of type int.
1006 :raises ValueError: If parameter 'build' is a negative number.
1007 :raises TypeError: If parameter 'post' is not of type int.
1008 :raises ValueError: If parameter 'post' is a negative number.
1009 :raises TypeError: If parameter 'dev' is not of type int.
1010 :raises ValueError: If parameter 'dev' is a negative number.
1011 :raises TypeError: If parameter 'prefix' is not of type str.
1012 :raises TypeError: If parameter 'postfix' is not of type str.
1013 """
1014 super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags)
1016 @classmethod
1017 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "SemanticVersion":
1018 """
1019 Parse a version string and return a :class:`SemanticVersion` instance.
1021 Allowed prefix characters:
1023 * ``v|V`` - version, public version, public release
1024 * ``i|I`` - internal version, internal release
1025 * ``r|R`` - release, revision
1026 * ``rev|REV`` - revision
1028 :param versionString: The version string to parse.
1029 :returns: An object representing a semantic version.
1030 :raises TypeError: If parameter ``other`` is not a string.
1031 :raises ValueError: If parameter ``other`` is None.
1032 :raises ValueError: If parameter ``other`` is empty.
1033 """
1034 if versionString is None:
1035 raise ValueError("Parameter 'versionString' is None.")
1036 elif not isinstance(versionString, str):
1037 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1038 if version_info >= (3, 11): # pragma: no cover
1039 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1040 raise ex
1042 versionString = versionString.strip()
1043 if versionString == "":
1044 raise ValueError("Parameter 'versionString' is empty.")
1046 match = cls._PATTERN.match(versionString)
1047 if match is None:
1048 raise ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1050 def toInt(value: Nullable[str]) -> Nullable[int]:
1051 if value is None or value == "":
1052 return None
1053 try:
1054 return int(value)
1055 except ValueError as ex: # pragma: no cover
1056 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1058 release = match["release"]
1059 if release is not None:
1060 if release == "dev": 1060 ↛ 1062line 1060 didn't jump to line 1062 because the condition on line 1060 was always true
1061 releaseLevel = ReleaseLevel.Development
1062 elif release == "final":
1063 releaseLevel = ReleaseLevel.Final
1064 else: # pragma: no cover
1065 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1066 else:
1067 level = match["level"]
1068 if level is not None:
1069 level = level.lower()
1070 if level == "a" or level == "alpha":
1071 releaseLevel = ReleaseLevel.Alpha
1072 elif level == "b" or level == "beta":
1073 releaseLevel = ReleaseLevel.Beta
1074 elif level == "c" or level == "gamma":
1075 releaseLevel = ReleaseLevel.Gamma
1076 elif level == "rc":
1077 releaseLevel = ReleaseLevel.ReleaseCandidate
1078 else: # pragma: no cover
1079 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1080 else:
1081 releaseLevel = ReleaseLevel.Final
1083 version = cls(
1084 major=toInt(match["major"]),
1085 minor=toInt(match["minor"]),
1086 micro=toInt(match["micro"]),
1087 level=releaseLevel,
1088 number=toInt(match["number"]),
1089 post=toInt(match["post"]),
1090 dev=toInt(match["dev"]),
1091 build=toInt(match["build"]),
1092 postfix=match["postfix"],
1093 prefix=match["prefix"],
1094 # hash=match["hash"],
1095 flags=Flags.Clean
1096 )
1097 if validator is not None and not validator(version):
1098 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1100 return version
1102 @readonly
1103 def Patch(self) -> int:
1104 """
1105 Read-only property to access the patch number.
1107 The patch number is identical to the micro number.
1109 :return: The patch number.
1110 """
1111 return self._micro
1113 def _equal(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1114 """
1115 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1117 :param left: Left parameter.
1118 :param right: Right parameter.
1119 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1120 """
1121 return super()._equal(left, right)
1123 def _compare(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1124 """
1125 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1127 :param left: Left parameter.
1128 :param right: Right parameter.
1129 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1130 False if ``left`` is greater than ``right``. |br|
1131 Otherwise it's None (both parameters are equal).
1132 """
1133 return super()._compare(left, right)
1135 def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1136 """
1137 Compare two version numbers for equality.
1139 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1140 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1141 number is assumed (all other parts are zero).
1143 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1144 number.
1146 :param other: Parameter to compare against.
1147 :returns: ``True``, if both version numbers are equal.
1148 :raises ValueError: If parameter ``other`` is None.
1149 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1150 """
1151 return super().__eq__(other)
1153 def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1154 """
1155 Compare two version numbers for inequality.
1157 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1158 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1159 number is assumed (all other parts are zero).
1161 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1162 number.
1164 :param other: Parameter to compare against.
1165 :returns: ``True``, if both version numbers are not equal.
1166 :raises ValueError: If parameter ``other`` is None.
1167 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1168 """
1169 return super().__ne__(other)
1171 def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1172 """
1173 Compare two version numbers if the version is less than the second operand.
1175 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1176 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1177 number is assumed (all other parts are zero).
1179 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1180 number.
1182 :param other: Parameter to compare against.
1183 :returns: ``True``, if version is less than the second operand.
1184 :raises ValueError: If parameter ``other`` is None.
1185 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1186 """
1187 return super().__lt__(other)
1189 def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1190 """
1191 Compare two version numbers if the version is less than or equal the second operand.
1193 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1194 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1195 number is assumed (all other parts are zero).
1197 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1198 number.
1200 :param other: Parameter to compare against.
1201 :returns: ``True``, if version is less than or equal the second operand.
1202 :raises ValueError: If parameter ``other`` is None.
1203 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1204 """
1205 return super().__le__(other)
1207 def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1208 """
1209 Compare two version numbers if the version is greater than the second operand.
1211 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1212 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1213 number is assumed (all other parts are zero).
1215 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1216 number.
1218 :param other: Parameter to compare against.
1219 :returns: ``True``, if version is greater than the second operand.
1220 :raises ValueError: If parameter ``other`` is None.
1221 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1222 """
1223 return super().__gt__(other)
1225 def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1226 """
1227 Compare two version numbers if the version is greater than or equal the second operand.
1229 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1230 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1231 number is assumed (all other parts are zero).
1233 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1234 number.
1236 :param other: Parameter to compare against.
1237 :returns: ``True``, if version is greater than or equal the second operand.
1238 :raises ValueError: If parameter ``other`` is None.
1239 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1240 """
1241 return super().__ge__(other)
1243 def __rshift__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1244 return super().__rshift__(other)
1246 def __hash__(self) -> int:
1247 return super().__hash__()
1249 def __format__(self, formatSpec: str) -> str:
1250 result = self._format(formatSpec)
1252 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1253 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1255 return result.replace("%%", "%")
1257 def __repr__(self) -> str:
1258 """
1259 Return a string representation of this version number without prefix ``v``.
1261 :returns: Raw version number representation without a prefix.
1262 """
1263 return f"{self._prefix if Parts.Prefix in self._parts else ''}{self._major}.{self._minor}.{self._micro}"
1265 def __str__(self) -> str:
1266 """
1267 Return a string representation of this version number.
1269 :returns: Version number representation.
1270 """
1271 result = self._prefix if Parts.Prefix in self._parts else ""
1272 result += f"{self._major}" # major is always present
1273 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1274 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1275 result += f".{self._build}" if Parts.Build in self._parts else ""
1276 if self._releaseLevel is ReleaseLevel.Development:
1277 result += "-dev"
1278 elif self._releaseLevel is ReleaseLevel.Alpha:
1279 result += f".alpha{self._releaseNumber}"
1280 elif self._releaseLevel is ReleaseLevel.Beta:
1281 result += f".beta{self._releaseNumber}"
1282 elif self._releaseLevel is ReleaseLevel.Gamma: 1282 ↛ 1283line 1282 didn't jump to line 1283 because the condition on line 1282 was never true
1283 result += f".gamma{self._releaseNumber}"
1284 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1285 result += f".rc{self._releaseNumber}"
1286 result += f".post{self._post}" if Parts.Post in self._parts else ""
1287 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1288 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1290 return result
1293@export
1294class PythonVersion(SemanticVersion):
1295 @classmethod
1296 def FromSysVersionInfo(cls) -> "PythonVersion":
1297 from sys import version_info
1299 if version_info.releaselevel == "final":
1300 rl = ReleaseLevel.Final
1301 number = None
1302 else: # pragma: no cover
1303 number = version_info.serial
1305 if version_info.releaselevel == "alpha":
1306 rl = ReleaseLevel.Alpha
1307 elif version_info.releaselevel == "beta":
1308 rl = ReleaseLevel.Beta
1309 elif version_info.releaselevel == "candidate":
1310 rl = ReleaseLevel.ReleaseCandidate
1311 else: # pragma: no cover
1312 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1314 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1316 def __hash__(self) -> int:
1317 return super().__hash__()
1319 def __str__(self) -> str:
1320 """
1321 Return a string representation of this version number.
1323 :returns: Version number representation.
1324 """
1325 result = self._prefix if Parts.Prefix in self._parts else ""
1326 result += f"{self._major}" # major is always present
1327 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1328 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1329 if self._releaseLevel is ReleaseLevel.Alpha:
1330 result += f"a{self._releaseNumber}"
1331 elif self._releaseLevel is ReleaseLevel.Beta:
1332 result += f"b{self._releaseNumber}"
1333 elif self._releaseLevel is ReleaseLevel.Gamma:
1334 result += f"c{self._releaseNumber}"
1335 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1336 result += f"rc{self._releaseNumber}"
1337 result += f".post{self._post}" if Parts.Post in self._parts else ""
1338 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1339 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1341 return result
1343@export
1344class CalendarVersion(Version):
1345 """Representation of a calendar version number like ``2021.10``."""
1347 def __init__(
1348 self,
1349 major: int,
1350 minor: Nullable[int] = None,
1351 micro: Nullable[int] = None,
1352 build: Nullable[int] = None,
1353 flags: Flags = Flags.Clean,
1354 prefix: Nullable[str] = None,
1355 postfix: Nullable[str] = None
1356 ) -> None:
1357 """
1358 Initializes a calendar version number representation.
1360 :param major: Major number part of the version number.
1361 :param minor: Minor number part of the version number.
1362 :param micro: Micro (patch) number part of the version number.
1363 :param build: Build number part of the version number.
1364 :param flags: The version number's flags.
1365 :param prefix: The version number's prefix.
1366 :param postfix: The version number's postfix.
1367 :raises TypeError: If parameter 'major' is not of type int.
1368 :raises ValueError: If parameter 'major' is a negative number.
1369 :raises TypeError: If parameter 'minor' is not of type int.
1370 :raises ValueError: If parameter 'minor' is a negative number.
1371 :raises TypeError: If parameter 'micro' is not of type int.
1372 :raises ValueError: If parameter 'micro' is a negative number.
1373 :raises TypeError: If parameter 'build' is not of type int.
1374 :raises ValueError: If parameter 'build' is a negative number.
1375 :raises TypeError: If parameter 'prefix' is not of type str.
1376 :raises TypeError: If parameter 'postfix' is not of type str.
1377 """
1378 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1380 @classmethod
1381 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion":
1382 """
1383 Parse a version string and return a :class:`CalendarVersion` instance.
1385 :param versionString: The version string to parse.
1386 :returns: An object representing a calendar version.
1387 :raises TypeError: If parameter ``other`` is not a string.
1388 :raises ValueError: If parameter ``other`` is None.
1389 :raises ValueError: If parameter ``other`` is empty.
1390 """
1391 parts = Parts.Unknown
1393 if versionString is None:
1394 raise ValueError("Parameter 'versionString' is None.")
1395 elif not isinstance(versionString, str):
1396 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1397 if version_info >= (3, 11): # pragma: no cover
1398 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1399 raise ex
1400 elif versionString == "":
1401 raise ValueError("Parameter 'versionString' is empty.")
1403 split = versionString.split(".")
1404 length = len(split)
1405 major = int(split[0])
1406 minor = 0
1407 parts |= Parts.Major
1409 if length >= 2:
1410 minor = int(split[1])
1411 parts |= Parts.Minor
1413 flags = Flags.Clean
1415 version = cls(major, minor, 0, 0, flags)
1416 if validator is not None and not validator(version):
1417 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1419 return version
1421 @property
1422 def Year(self) -> int:
1423 """
1424 Read-only property to access the year part.
1426 :return: The year part.
1427 """
1428 return self._major
1430 def _equal(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1431 """
1432 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1434 :param left: Left parameter.
1435 :param right: Right parameter.
1436 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1437 """
1438 return (
1439 (left._major == right._major) and
1440 (left._minor == right._minor)
1441 )
1443 def _compare(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1444 """
1445 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1447 :param left: Left parameter.
1448 :param right: Right parameter.
1449 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1450 False if ``left`` is greater than ``right``. |br|
1451 Otherwise it's None (both parameters are equal).
1452 """
1453 if left._major < right._major:
1454 return True
1455 elif left._major > right._major:
1456 return False
1458 if left._minor < right._minor:
1459 return True
1460 elif left._minor > right._minor:
1461 return False
1463 return None
1465 def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1466 """
1467 Compare two version numbers for equality.
1469 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1470 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1471 number is assumed (all other parts are zero).
1473 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1474 number.
1476 :param other: Parameter to compare against.
1477 :returns: ``True``, if both version numbers are equal.
1478 :raises ValueError: If parameter ``other`` is None.
1479 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1480 """
1481 return super().__eq__(other)
1483 def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1484 """
1485 Compare two version numbers for inequality.
1487 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1488 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1489 number is assumed (all other parts are zero).
1491 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1492 number.
1494 :param other: Parameter to compare against.
1495 :returns: ``True``, if both version numbers are not equal.
1496 :raises ValueError: If parameter ``other`` is None.
1497 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1498 """
1499 return super().__ne__(other)
1501 def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1502 """
1503 Compare two version numbers if the version is less than the second operand.
1505 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1506 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1507 number is assumed (all other parts are zero).
1509 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1510 number.
1512 :param other: Parameter to compare against.
1513 :returns: ``True``, if version is less than the second operand.
1514 :raises ValueError: If parameter ``other`` is None.
1515 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1516 """
1517 return super().__lt__(other)
1519 def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1520 """
1521 Compare two version numbers if the version is less than or equal the second operand.
1523 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1524 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1525 number is assumed (all other parts are zero).
1527 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1528 number.
1530 :param other: Parameter to compare against.
1531 :returns: ``True``, if version is less than or equal the second operand.
1532 :raises ValueError: If parameter ``other`` is None.
1533 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1534 """
1535 return super().__le__(other)
1537 def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1538 """
1539 Compare two version numbers if the version is greater than the second operand.
1541 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1542 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1543 number is assumed (all other parts are zero).
1545 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1546 number.
1548 :param other: Parameter to compare against.
1549 :returns: ``True``, if version is greater than the second operand.
1550 :raises ValueError: If parameter ``other`` is None.
1551 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1552 """
1553 return super().__gt__(other)
1555 def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1556 """
1557 Compare two version numbers if the version is greater than or equal the second operand.
1559 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1560 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1561 number is assumed (all other parts are zero).
1563 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1564 number.
1566 :param other: Parameter to compare against.
1567 :returns: ``True``, if version is greater than or equal the second operand.
1568 :raises ValueError: If parameter ``other`` is None.
1569 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1570 """
1571 return super().__ge__(other)
1573 def __hash__(self) -> int:
1574 return super().__hash__()
1576 def __format__(self, formatSpec: str) -> str:
1577 """
1578 Return a string representation of this version number according to the format specification.
1580 .. topic:: Format Specifiers
1582 * ``%M`` - major number (year)
1583 * ``%m`` - minor number (month/week)
1585 :param formatSpec: The format specification.
1586 :return: Formatted version number.
1587 """
1588 if formatSpec == "":
1589 return self.__str__()
1591 result = formatSpec
1592 # result = result.replace("%P", str(self._prefix))
1593 result = result.replace("%M", str(self._major))
1594 result = result.replace("%m", str(self._minor))
1595 # result = result.replace("%p", str(self._pre))
1597 return result.replace("%%", "%")
1599 def __repr__(self) -> str:
1600 """
1601 Return a string representation of this version number without prefix ``v``.
1603 :returns: Raw version number representation without a prefix.
1604 """
1605 return f"{self._major}.{self._minor}"
1607 def __str__(self) -> str:
1608 """
1609 Return a string representation of this version number with prefix ``v``.
1611 :returns: Version number representation including a prefix.
1612 """
1613 result = f"{self._major}"
1614 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1616 return result
1619@export
1620class YearMonthVersion(CalendarVersion):
1621 """Representation of a calendar version number like ``2021.10``."""
1623 def __init__(
1624 self,
1625 year: int,
1626 month: Nullable[int] = None,
1627 build: Nullable[int] = None,
1628 flags: Flags = Flags.Clean,
1629 prefix: Nullable[str] = None,
1630 postfix: Nullable[str] = None
1631 ) -> None:
1632 """
1633 Initializes a year-month version number representation.
1635 :param year: Year part of the version number.
1636 :param month: Month part of the version number.
1637 :param build: Build number part of the version number.
1638 :param flags: The version number's flags.
1639 :param prefix: The version number's prefix.
1640 :param postfix: The version number's postfix.
1641 :raises TypeError: If parameter 'major' is not of type int.
1642 :raises ValueError: If parameter 'major' is a negative number.
1643 :raises TypeError: If parameter 'minor' is not of type int.
1644 :raises ValueError: If parameter 'minor' is a negative number.
1645 :raises TypeError: If parameter 'micro' is not of type int.
1646 :raises ValueError: If parameter 'micro' is a negative number.
1647 :raises TypeError: If parameter 'build' is not of type int.
1648 :raises ValueError: If parameter 'build' is a negative number.
1649 :raises TypeError: If parameter 'prefix' is not of type str.
1650 :raises TypeError: If parameter 'postfix' is not of type str.
1651 """
1652 super().__init__(year, month, 0, build, flags, prefix, postfix)
1654 @property
1655 def Month(self) -> int:
1656 """
1657 Read-only property to access the month part.
1659 :return: The month part.
1660 """
1661 return self._minor
1663 def __hash__(self) -> int:
1664 return super().__hash__()
1667@export
1668class YearWeekVersion(CalendarVersion):
1669 """Representation of a calendar version number like ``2021.47``."""
1671 def __init__(
1672 self,
1673 year: int,
1674 week: Nullable[int] = None,
1675 build: Nullable[int] = None,
1676 flags: Flags = Flags.Clean,
1677 prefix: Nullable[str] = None,
1678 postfix: Nullable[str] = None
1679 ) -> None:
1680 """
1681 Initializes a year-week version number representation.
1683 :param year: Year part of the version number.
1684 :param week: Week part of the version number.
1685 :param build: Build number part of the version number.
1686 :param flags: The version number's flags.
1687 :param prefix: The version number's prefix.
1688 :param postfix: The version number's postfix.
1689 :raises TypeError: If parameter 'major' is not of type int.
1690 :raises ValueError: If parameter 'major' is a negative number.
1691 :raises TypeError: If parameter 'minor' is not of type int.
1692 :raises ValueError: If parameter 'minor' is a negative number.
1693 :raises TypeError: If parameter 'micro' is not of type int.
1694 :raises ValueError: If parameter 'micro' is a negative number.
1695 :raises TypeError: If parameter 'build' is not of type int.
1696 :raises ValueError: If parameter 'build' is a negative number.
1697 :raises TypeError: If parameter 'prefix' is not of type str.
1698 :raises TypeError: If parameter 'postfix' is not of type str.
1699 """
1700 super().__init__(year, week, 0, build, flags, prefix, postfix)
1702 @property
1703 def Week(self) -> int:
1704 """
1705 Read-only property to access the week part.
1707 :return: The week part.
1708 """
1709 return self._minor
1711 def __hash__(self) -> int:
1712 return super().__hash__()
1715@export
1716class YearReleaseVersion(CalendarVersion):
1717 """Representation of a calendar version number like ``2021.2``."""
1719 def __init__(
1720 self,
1721 year: int,
1722 release: Nullable[int] = None,
1723 build: Nullable[int] = None,
1724 flags: Flags = Flags.Clean,
1725 prefix: Nullable[str] = None,
1726 postfix: Nullable[str] = None
1727 ) -> None:
1728 """
1729 Initializes a year-release version number representation.
1731 :param year: Year part of the version number.
1732 :param release: Release number of the version number.
1733 :param build: Build number part of the version number.
1734 :param flags: The version number's flags.
1735 :param prefix: The version number's prefix.
1736 :param postfix: The version number's postfix.
1737 :raises TypeError: If parameter 'major' is not of type int.
1738 :raises ValueError: If parameter 'major' is a negative number.
1739 :raises TypeError: If parameter 'minor' is not of type int.
1740 :raises ValueError: If parameter 'minor' is a negative number.
1741 :raises TypeError: If parameter 'micro' is not of type int.
1742 :raises ValueError: If parameter 'micro' is a negative number.
1743 :raises TypeError: If parameter 'build' is not of type int.
1744 :raises ValueError: If parameter 'build' is a negative number.
1745 :raises TypeError: If parameter 'prefix' is not of type str.
1746 :raises TypeError: If parameter 'postfix' is not of type str.
1747 """
1748 super().__init__(year, release, 0, build, flags, prefix, postfix)
1750 @property
1751 def Release(self) -> int:
1752 """
1753 Read-only property to access the release number.
1755 :return: The release number.
1756 """
1757 return self._minor
1759 def __hash__(self) -> int:
1760 return super().__hash__()
1763@export
1764class YearMonthDayVersion(CalendarVersion):
1765 """Representation of a calendar version number like ``2021.10.15``."""
1767 def __init__(
1768 self,
1769 year: int,
1770 month: Nullable[int] = None,
1771 day: Nullable[int] = None,
1772 build: Nullable[int] = None,
1773 flags: Flags = Flags.Clean,
1774 prefix: Nullable[str] = None,
1775 postfix: Nullable[str] = None
1776 ) -> None:
1777 """
1778 Initializes a year-month-day version number representation.
1780 :param year: Year part of the version number.
1781 :param month: Month part of the version number.
1782 :param day: Day part of the version number.
1783 :param build: Build number part of the version number.
1784 :param flags: The version number's flags.
1785 :param prefix: The version number's prefix.
1786 :param postfix: The version number's postfix.
1787 :raises TypeError: If parameter 'major' is not of type int.
1788 :raises ValueError: If parameter 'major' is a negative number.
1789 :raises TypeError: If parameter 'minor' is not of type int.
1790 :raises ValueError: If parameter 'minor' is a negative number.
1791 :raises TypeError: If parameter 'micro' is not of type int.
1792 :raises ValueError: If parameter 'micro' is a negative number.
1793 :raises TypeError: If parameter 'build' is not of type int.
1794 :raises ValueError: If parameter 'build' is a negative number.
1795 :raises TypeError: If parameter 'prefix' is not of type str.
1796 :raises TypeError: If parameter 'postfix' is not of type str.
1797 """
1798 super().__init__(year, month, day, build, flags, prefix, postfix)
1800 @property
1801 def Month(self) -> int:
1802 """
1803 Read-only property to access the month part.
1805 :return: The month part.
1806 """
1807 return self._minor
1809 @property
1810 def Day(self) -> int:
1811 """
1812 Read-only property to access the day part.
1814 :return: The day part.
1815 """
1816 return self._micro
1818 def __hash__(self) -> int:
1819 return super().__hash__()