Coverage for pyTooling/Versioning/__init__.py: 82%
975 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 22:23 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 22:23 +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 collections.abc import Iterable as abc_Iterable
37from enum import Flag, Enum
38from re import compile as re_compile
39from sys import version_info # needed for versions before Python 3.11
40from typing import Optional as Nullable, Union, Callable, Any, Generic, TypeVar, Tuple, Iterable, Iterator, List
42try:
43 from pyTooling.Decorators import export, readonly
44 from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
45 from pyTooling.Exceptions import ToolingException
46 from pyTooling.Common import getFullyQualifiedName
47except (ImportError, ModuleNotFoundError): # pragma: no cover
48 print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!")
50 try:
51 from Decorators import export, readonly
52 from MetaClasses import ExtendedType, abstractmethod, mustoverride
53 from Exceptions import ToolingException
54 from Common import getFullyQualifiedName
55 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
56 print("[pyTooling.Versioning] Could not import directly!")
57 raise ex
60@export
61class Parts(Flag):
62 """Enumeration describing parts of a version number that can be present."""
63 Unknown = 0 #: Undocumented
64 Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``).
65 Year = 1 #: Year is present. (e.g. X in ``XXXX.10``).
66 Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
67 Month = 2 #: Month is present. (e.g. X in ``2024.YY``).
68 Week = 2 #: Week is present. (e.g. X in ``2024.YY``).
69 Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
70 Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
71 Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``).
72 Level = 8 #: Release level is present.
73 Dev = 16 #: Development part is present.
74 Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
75 Post = 64 #: Post-release number is present.
76 Prefix = 128 #: Prefix is present.
77 Postfix = 256 #: Postfix is present.
78 Hash = 512 #: Hash is present.
79# AHead = 256
82@export
83class ReleaseLevel(Enum):
84 """Enumeration describing the version's maturity level."""
85 Final = 0 #:
86 ReleaseCandidate = -10 #:
87 Development = -20 #:
88 Gamma = -30 #:
89 Beta = -40 #:
90 Alpha = -50 #:
92 def __eq__(self, other: Any):
93 """
94 Compare two release levels if the level is equal to the second operand.
96 :param other: Operand to compare against.
97 :returns: ``True``, if release level is equal the second operand's release level.
98 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
99 """
100 if isinstance(other, str): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 other = ReleaseLevel(other)
103 if not isinstance(other, ReleaseLevel): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
105 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
106 raise ex
108 return self is other
110 def __ne__(self, other: Any):
111 """
112 Compare two release levels if the level is unequal to the second operand.
114 :param other: Operand to compare against.
115 :returns: ``True``, if release level is unequal the second operand's release level.
116 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
117 """
118 if isinstance(other, str):
119 other = ReleaseLevel(other)
121 if not isinstance(other, ReleaseLevel):
122 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.")
123 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
124 raise ex
126 return self is not other
128 def __lt__(self, other: Any):
129 """
130 Compare two release levels if the level is less than the second operand.
132 :param other: Operand to compare against.
133 :returns: ``True``, if release level is less than the second operand.
134 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
135 """
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)
139 if not isinstance(other, ReleaseLevel): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true
140 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
141 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
142 raise ex
144 return self.value < other.value
146 def __le__(self, other: Any):
147 """
148 Compare two release levels if the level is less than or equal the second operand.
150 :param other: Operand to compare against.
151 :returns: ``True``, if release level is less than or equal the second operand.
152 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
153 """
154 if isinstance(other, str):
155 other = ReleaseLevel(other)
157 if not isinstance(other, ReleaseLevel):
158 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <=>= operator.")
159 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
160 raise ex
162 return self.value <= other.value
164 def __gt__(self, other: Any):
165 """
166 Compare two release levels if the level is greater than the second operand.
168 :param other: Operand to compare against.
169 :returns: ``True``, if release level is greater than the second operand.
170 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
171 """
172 if isinstance(other, str): 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true
173 other = ReleaseLevel(other)
175 if not isinstance(other, ReleaseLevel): 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
177 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
178 raise ex
180 return self.value > other.value
182 def __ge__(self, other: Any):
183 """
184 Compare two release levels if the level is greater than or equal the second operand.
186 :param other: Operand to compare against.
187 :returns: ``True``, if release level is greater than or equal the second operand.
188 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
189 """
190 if isinstance(other, str):
191 other = ReleaseLevel(other)
193 if not isinstance(other, ReleaseLevel):
194 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
195 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
196 raise ex
198 return self.value >= other.value
200 def __hash__(self) -> int:
201 return hash(self.value)
203 def __str__(self) -> str:
204 """
205 Returns the release level's string equivalent.
207 :returns: The string equivalent of the release level.
208 """
209 if self is ReleaseLevel.Final:
210 return "final"
211 elif self is ReleaseLevel.ReleaseCandidate:
212 return "rc"
213 elif self is ReleaseLevel.Development: 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true
214 return "dev"
215 elif self is ReleaseLevel.Beta: 215 ↛ 216line 215 didn't jump to line 216 because the condition on line 215 was never true
216 return "beta"
217 elif self is ReleaseLevel.Alpha: 217 ↛ 220line 217 didn't jump to line 220 because the condition on line 217 was always true
218 return "alpha"
220 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
223@export
224class Flags(Flag):
225 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
226 NoVCS = 0 #: No Version Control System VCS
227 Clean = 1 #: A versioned build was created from a *clean* working directory.
228 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
230 CVS = 16 #: Concurrent Versions System (CVS)
231 SVN = 32 #: Subversion (SVN)
232 Git = 64 #: Git
233 Hg = 128 #: Mercurial (Hg)
236@export
237def WordSizeValidator(
238 bits: Nullable[int] = None,
239 majorBits: Nullable[int] = None,
240 minorBits: Nullable[int] = None,
241 microBits: Nullable[int] = None,
242 buildBits: Nullable[int] = None
243):
244 """
245 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
247 :param bits: Number of bits to encode any positive version number part.
248 :param majorBits: Number of bits to encode a positive major number in a version.
249 :param minorBits: Number of bits to encode a positive minor number in a version.
250 :param microBits: Number of bits to encode a positive micro number in a version.
251 :param buildBits: Number of bits to encode a positive build number in a version.
252 :return: A validation function for Version instances.
253 """
254 majorMax = minorMax = microMax = buildMax = -1
255 if bits is not None:
256 majorMax = minorMax = microMax = buildMax = 2**bits - 1
258 if majorBits is not None:
259 majorMax = 2**majorBits - 1
260 if minorBits is not None:
261 minorMax = 2**minorBits - 1
262 if microBits is not None:
263 microMax = 2 ** microBits - 1
264 if buildBits is not None: 264 ↛ 265line 264 didn't jump to line 265 because the condition on line 264 was never true
265 buildMax = 2**buildBits - 1
267 def validator(version: SemanticVersion) -> bool:
268 if Parts.Major in version._parts and version._major > majorMax:
269 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
271 if Parts.Minor in version._parts and version._minor > minorMax:
272 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
274 if Parts.Micro in version._parts and version._micro > microMax:
275 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
277 if Parts.Build in version._parts and version._build > buildMax: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
280 return True
282 return validator
285@export
286def MaxValueValidator(
287 max: Nullable[int] = None,
288 majorMax: Nullable[int] = None,
289 minorMax: Nullable[int] = None,
290 microMax: Nullable[int] = None,
291 buildMax: Nullable[int] = None
292):
293 """
294 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
296 :param max: The upper bound for any positive version number part.
297 :param majorMax: The upper bound for the positive major number.
298 :param minorMax: The upper bound for the positive minor number.
299 :param microMax: The upper bound for the positive micro number.
300 :param buildMax: The upper bound for the positive build number.
301 :return: A validation function for Version instances.
302 """
303 if max is not None: 303 ↛ 306line 303 didn't jump to line 306 because the condition on line 303 was always true
304 majorMax = minorMax = microMax = buildMax = max
306 def validator(version: SemanticVersion) -> bool:
307 if Parts.Major in version._parts and version._major > majorMax:
308 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
310 if Parts.Minor in version._parts and version._minor > minorMax:
311 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
313 if Parts.Micro in version._parts and version._micro > microMax:
314 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
316 if Parts.Build in version._parts and version._build > buildMax: 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true
317 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
319 return True
321 return validator
324@export
325class Version(metaclass=ExtendedType, slots=True):
326 """Base-class for a version representation."""
328 __hash: Nullable[int] #: once computed hash of the object
330 _parts: Parts #: Integer flag enumeration of present parts in a version number.
331 _prefix: str #: Prefix string
332 _major: int #: Major number part of the version number.
333 _minor: int #: Minor number part of the version number.
334 _micro: int #: Micro number part of the version number.
335 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
336 _releaseNumber: int #: Release number (Python calls this a serial).
337 _post: int #: Post-release version number part.
338 _dev: int #: Development number
339 _build: int #: Build number part of the version number.
340 _postfix: str #: Postfix string
341 _hash: str #: Hash from version control system.
342 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
344 def __init__(
345 self,
346 major: int,
347 minor: Nullable[int] = None,
348 micro: Nullable[int] = None,
349 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
350 number: Nullable[int] = None,
351 post: Nullable[int] = None,
352 dev: Nullable[int] = None,
353 *,
354 build: Nullable[int] = None,
355 postfix: Nullable[str] = None,
356 prefix: Nullable[str] = None,
357 hash: Nullable[str] = None,
358 flags: Flags = Flags.NoVCS
359 ) -> None:
360 """
361 Initializes a version number representation.
363 :param major: Major number part of the version number.
364 :param minor: Minor number part of the version number.
365 :param micro: Micro (patch) number part of the version number.
366 :param level: Release level (alpha, beta, release candidate, final, ...) of the version number.
367 :param number: Release number part (in combination with release level) of the version number.
368 :param post: Post number part of the version number.
369 :param dev: Development number part of the version number.
370 :param build: Build number part of the version number.
371 :param postfix: The version number's postfix.
372 :param prefix: The version number's prefix.
373 :param hash: Postfix string.
374 :param flags: The version number's flags.
375 :raises TypeError: If parameter 'major' is not of type int.
376 :raises ValueError: If parameter 'major' is a negative number.
377 :raises TypeError: If parameter 'minor' is not of type int.
378 :raises ValueError: If parameter 'minor' is a negative number.
379 :raises TypeError: If parameter 'micro' is not of type int.
380 :raises ValueError: If parameter 'micro' is a negative number.
381 :raises TypeError: If parameter 'build' is not of type int.
382 :raises ValueError: If parameter 'build' is a negative number.
383 :raises TypeError: If parameter 'prefix' is not of type str.
384 :raises TypeError: If parameter 'postfix' is not of type str.
385 """
386 self.__hash = None
388 if not isinstance(major, int):
389 raise TypeError("Parameter 'major' is not of type 'int'.")
390 elif major < 0:
391 raise ValueError("Parameter 'major' is negative.")
393 self._parts = Parts.Major
394 self._major = major
396 if minor is not None:
397 if not isinstance(minor, int):
398 raise TypeError("Parameter 'minor' is not of type 'int'.")
399 elif minor < 0:
400 raise ValueError("Parameter 'minor' is negative.")
402 self._parts |= Parts.Minor
403 self._minor = minor
404 else:
405 self._minor = 0
407 if micro is not None:
408 if not isinstance(micro, int):
409 raise TypeError("Parameter 'micro' is not of type 'int'.")
410 elif micro < 0:
411 raise ValueError("Parameter 'micro' is negative.")
413 self._parts |= Parts.Micro
414 self._micro = micro
415 else:
416 self._micro = 0
418 if level is None:
419 raise ValueError("Parameter 'level' is None.")
420 elif not isinstance(level, ReleaseLevel):
421 raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
422 elif level is ReleaseLevel.Final:
423 if number is not None:
424 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
426 self._parts |= Parts.Level
427 self._releaseLevel = level
428 self._releaseNumber = 0
429 else:
430 self._parts |= Parts.Level
431 self._releaseLevel = level
433 if number is not None:
434 if not isinstance(number, int):
435 raise TypeError("Parameter 'number' is not of type 'int'.")
436 elif number < 0:
437 raise ValueError("Parameter 'number' is negative.")
439 self._releaseNumber = number
440 else:
441 self._releaseNumber = 0
443 if dev is not None:
444 if not isinstance(dev, int):
445 raise TypeError("Parameter 'dev' is not of type 'int'.")
446 elif dev < 0:
447 raise ValueError("Parameter 'dev' is negative.")
449 self._parts |= Parts.Dev
450 self._dev = dev
451 else:
452 self._dev = 0
454 if post is not None:
455 if not isinstance(post, int):
456 raise TypeError("Parameter 'post' is not of type 'int'.")
457 elif post < 0:
458 raise ValueError("Parameter 'post' is negative.")
460 self._parts |= Parts.Post
461 self._post = post
462 else:
463 self._post = 0
465 if build is not None:
466 if not isinstance(build, int):
467 raise TypeError("Parameter 'build' is not of type 'int'.")
468 elif build < 0:
469 raise ValueError("Parameter 'build' is negative.")
471 self._build = build
472 self._parts |= Parts.Build
473 else:
474 self._build = 0
476 if postfix is not None:
477 if not isinstance(postfix, str):
478 raise TypeError("Parameter 'postfix' is not of type 'str'.")
480 self._parts |= Parts.Postfix
481 self._postfix = postfix
482 else:
483 self._postfix = ""
485 if prefix is not None:
486 if not isinstance(prefix, str):
487 raise TypeError("Parameter 'prefix' is not of type 'str'.")
489 self._parts |= Parts.Prefix
490 self._prefix = prefix
491 else:
492 self._prefix = ""
494 if hash is not None:
495 if not isinstance(hash, str):
496 raise TypeError("Parameter 'hash' is not of type 'str'.")
498 self._parts |= Parts.Hash
499 self._hash = hash
500 else:
501 self._hash = ""
503 if flags is None:
504 raise ValueError("Parameter 'flags' is None.")
505 elif not isinstance(flags, Flags):
506 raise TypeError("Parameter 'flags' is not of type 'Flags'.")
508 self._flags = flags
510 @classmethod
511 @abstractmethod
512 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version":
513 """Parse a version string and return a Version instance."""
515 @readonly
516 def Parts(self) -> Parts:
517 """
518 Read-only property to access the used parts of this version number.
520 :return: A flag enumeration of used version number parts.
521 """
522 return self._parts
524 @readonly
525 def Prefix(self) -> str:
526 """
527 Read-only property to access the version number's prefix.
529 :return: The prefix of the version number.
530 """
531 return self._prefix
533 @readonly
534 def Major(self) -> int:
535 """
536 Read-only property to access the major number.
538 :return: The major number.
539 """
540 return self._major
542 @readonly
543 def Minor(self) -> int:
544 """
545 Read-only property to access the minor number.
547 :return: The minor number.
548 """
549 return self._minor
551 @readonly
552 def Micro(self) -> int:
553 """
554 Read-only property to access the micro number.
556 :return: The micro number.
557 """
558 return self._micro
560 @readonly
561 def ReleaseLevel(self) -> ReleaseLevel:
562 """
563 Read-only property to access the release level.
565 :return: The release level.
566 """
567 return self._releaseLevel
569 @readonly
570 def ReleaseNumber(self) -> int:
571 """
572 Read-only property to access the release number.
574 :return: The release number.
575 """
576 return self._releaseNumber
578 @readonly
579 def Post(self) -> int:
580 """
581 Read-only property to access the post number.
583 :return: The post number.
584 """
585 return self._post
587 @readonly
588 def Dev(self) -> int:
589 """
590 Read-only property to access the development number.
592 :return: The development number.
593 """
594 return self._dev
596 @readonly
597 def Build(self) -> int:
598 """
599 Read-only property to access the build number.
601 :return: The build number.
602 """
603 return self._build
605 @readonly
606 def Postfix(self) -> str:
607 """
608 Read-only property to access the version number's postfix.
610 :return: The postfix of the version number.
611 """
612 return self._postfix
614 @readonly
615 def Hash(self) -> str:
616 """
617 Read-only property to access the version number's hash.
619 :return: The hash.
620 """
621 return self._hash
623 @readonly
624 def Flags(self) -> Flags:
625 """
626 Read-only property to access the version number's flags.
628 :return: The flags of the version number.
629 """
630 return self._flags
632 def _equal(self, left: "Version", right: "Version") -> Nullable[bool]:
633 """
634 Private helper method to compute the equality of two :class:`Version` instances.
636 :param left: Left operand.
637 :param right: Right operand.
638 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
639 """
640 return (
641 (left._major == right._major) and
642 (left._minor == right._minor) and
643 (left._micro == right._micro) and
644 (left._releaseLevel == right._releaseLevel) and
645 (left._releaseNumber == right._releaseNumber) and
646 (left._post == right._post) and
647 (left._dev == right._dev) and
648 (left._build == right._build) and
649 (left._postfix == right._postfix)
650 )
652 def _compare(self, left: "Version", right: "Version") -> Nullable[bool]:
653 """
654 Private helper method to compute the comparison of two :class:`Version` instances.
656 :param left: Left operand.
657 :param right: Right operand.
658 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
659 False if ``left`` is greater than ``right``. |br|
660 Otherwise it's None (both operands are equal).
661 """
662 if left._major < right._major:
663 return True
664 elif left._major > right._major:
665 return False
667 if left._minor < right._minor:
668 return True
669 elif left._minor > right._minor:
670 return False
672 if left._micro < right._micro:
673 return True
674 elif left._micro > right._micro:
675 return False
677 if left._releaseLevel < right._releaseLevel: 677 ↛ 678line 677 didn't jump to line 678 because the condition on line 677 was never true
678 return True
679 elif left._releaseLevel > right._releaseLevel: 679 ↛ 680line 679 didn't jump to line 680 because the condition on line 679 was never true
680 return False
682 if left._releaseNumber < right._releaseNumber: 682 ↛ 683line 682 didn't jump to line 683 because the condition on line 682 was never true
683 return True
684 elif left._releaseNumber > right._releaseNumber: 684 ↛ 685line 684 didn't jump to line 685 because the condition on line 684 was never true
685 return False
687 if left._post < right._post: 687 ↛ 688line 687 didn't jump to line 688 because the condition on line 687 was never true
688 return True
689 elif left._post > right._post: 689 ↛ 690line 689 didn't jump to line 690 because the condition on line 689 was never true
690 return False
692 if left._dev < right._dev: 692 ↛ 693line 692 didn't jump to line 693 because the condition on line 692 was never true
693 return True
694 elif left._dev > right._dev: 694 ↛ 695line 694 didn't jump to line 695 because the condition on line 694 was never true
695 return False
697 if left._build < right._build: 697 ↛ 698line 697 didn't jump to line 698 because the condition on line 697 was never true
698 return True
699 elif left._build > right._build: 699 ↛ 700line 699 didn't jump to line 700 because the condition on line 699 was never true
700 return False
702 return None
704 def _minimum(self, actual: "Version", expected: "Version") -> Nullable[bool]:
705 exactMajor = Parts.Minor in expected._parts
706 exactMinor = Parts.Micro in expected._parts
708 if exactMajor and actual._major != expected._major: 708 ↛ 709line 708 didn't jump to line 709 because the condition on line 708 was never true
709 return False
710 elif not exactMajor and actual._major < expected._major:
711 return False
713 if exactMinor and actual._minor != expected._minor: 713 ↛ 714line 713 didn't jump to line 714 because the condition on line 713 was never true
714 return False
715 elif not exactMinor and actual._minor < expected._minor:
716 return False
718 if Parts.Micro in expected._parts:
719 return actual._micro >= expected._micro
721 return True
723 def _format(self, formatSpec: str) -> str:
724 """
725 Return a string representation of this version number according to the format specification.
727 .. topic:: Format Specifiers
729 * ``%p`` - prefix
730 * ``%M`` - major number
731 * ``%m`` - minor number
732 * ``%u`` - micro number
733 * ``%b`` - build number
735 :param formatSpec: The format specification.
736 :return: Formatted version number.
737 """
738 if formatSpec == "":
739 return self.__str__()
741 result = formatSpec
742 result = result.replace("%p", str(self._prefix))
743 result = result.replace("%M", str(self._major))
744 result = result.replace("%m", str(self._minor))
745 result = result.replace("%u", str(self._micro))
746 result = result.replace("%b", str(self._build))
747 result = result.replace("%r", str(self._releaseLevel)[0])
748 result = result.replace("%R", str(self._releaseLevel))
749 result = result.replace("%n", str(self._releaseNumber))
750 result = result.replace("%d", str(self._dev))
751 result = result.replace("%P", str(self._postfix))
753 return result
755 @mustoverride
756 def __eq__(self, other: Union["Version", str, int, None]) -> bool:
757 """
758 Compare two version numbers for equality.
760 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
761 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
762 number is assumed (all other parts are zero).
764 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
765 number.
767 :param other: Operand to compare against.
768 :returns: ``True``, if both version numbers are equal.
769 :raises ValueError: If parameter ``other`` is None.
770 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
771 """
772 if other is None:
773 raise ValueError(f"Second operand is None.")
774 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
775 pass
776 elif isinstance(other, str):
777 other = self.__class__.Parse(other)
778 elif isinstance(other, int):
779 other = self.__class__(major=other)
780 else:
781 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
782 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
783 raise ex
785 return self._equal(self, other)
787 @mustoverride
788 def __ne__(self, other: Union["Version", str, int, None]) -> bool:
789 """
790 Compare two version numbers for inequality.
792 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
793 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
794 number is assumed (all other parts are zero).
796 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
797 number.
799 :param other: Operand to compare against.
800 :returns: ``True``, if both version numbers are not equal.
801 :raises ValueError: If parameter ``other`` is None.
802 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
803 """
804 if other is None:
805 raise ValueError(f"Second operand is None.")
806 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
807 pass
808 elif isinstance(other, str):
809 other = self.__class__.Parse(other)
810 elif isinstance(other, int):
811 other = self.__class__(major=other)
812 else:
813 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
814 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
815 raise ex
817 return not self._equal(self, other)
819 @mustoverride
820 def __lt__(self, other: Union["Version", str, int, None]) -> bool:
821 """
822 Compare two version numbers if the version is less than the second operand.
824 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
825 ``str`` and ``int`` are accepted, too. |br|
826 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
827 number is assumed (all other parts are zero).
829 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
830 number.
832 :param other: Operand to compare against.
833 :returns: ``True``, if version is less than the second operand.
834 :raises ValueError: If parameter ``other`` is None.
835 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
836 """
837 if other is None:
838 raise ValueError(f"Second operand is None.")
839 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
840 pass
841 elif isinstance(other, VersionRange):
842 other = other._lowerBound
843 elif isinstance(other, VersionSet):
844 other = other._items[0]
845 elif isinstance(other, str):
846 other = self.__class__.Parse(other)
847 elif isinstance(other, int):
848 other = self.__class__(major=other)
849 else:
850 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
851 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
852 raise ex
854 return self._compare(self, other) is True
856 @mustoverride
857 def __le__(self, other: Union["Version", str, int, None]) -> bool:
858 """
859 Compare two version numbers if the version is less than or equal the second operand.
861 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
862 ``str`` and ``int`` are accepted, too. |br|
863 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
864 number is assumed (all other parts are zero).
866 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
867 number.
869 :param other: Operand to compare against.
870 :returns: ``True``, if version is less than or equal the second operand.
871 :raises ValueError: If parameter ``other`` is None.
872 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
873 """
874 equalValue = True
875 if other is None:
876 raise ValueError(f"Second operand is None.")
877 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
878 pass
879 elif isinstance(other, VersionRange):
880 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
881 other = other._lowerBound
882 elif isinstance(other, VersionSet):
883 other = other._items[0]
884 elif isinstance(other, str):
885 other = self.__class__.Parse(other)
886 elif isinstance(other, int):
887 other = self.__class__(major=other)
888 else:
889 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.")
890 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
891 raise ex
893 result = self._compare(self, other)
894 return result if result is not None else equalValue
896 @mustoverride
897 def __gt__(self, other: Union["Version", str, int, None]) -> bool:
898 """
899 Compare two version numbers if the version is greater than the second operand.
901 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
902 ``str`` and ``int`` are accepted, too. |br|
903 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
904 number is assumed (all other parts are zero).
906 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
907 number.
909 :param other: Operand to compare against.
910 :returns: ``True``, if version is greater than the second operand.
911 :raises ValueError: If parameter ``other`` is None.
912 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
913 """
914 if other is None:
915 raise ValueError(f"Second operand is None.")
916 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
917 pass
918 elif isinstance(other, VersionRange):
919 other = other._upperBound
920 elif isinstance(other, VersionSet):
921 other = other._items[-1]
922 elif isinstance(other, str):
923 other = self.__class__.Parse(other)
924 elif isinstance(other, int):
925 other = self.__class__(major=other)
926 else:
927 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
928 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
929 raise ex
931 return self._compare(self, other) is False
933 @mustoverride
934 def __ge__(self, other: Union["Version", str, int, None]) -> bool:
935 """
936 Compare two version numbers if the version is greater than or equal the second operand.
938 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
939 ``str`` and ``int`` are accepted, too. |br|
940 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
941 number is assumed (all other parts are zero).
943 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
944 number.
946 :param other: Operand to compare against.
947 :returns: ``True``, if version is greater than or equal the second operand.
948 :raises ValueError: If parameter ``other`` is None.
949 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
950 """
951 equalValue = True
952 if other is None:
953 raise ValueError(f"Second operand is None.")
954 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
955 pass
956 elif isinstance(other, VersionRange):
957 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
958 other = other._upperBound
959 elif isinstance(other, VersionSet):
960 other = other._items[-1]
961 elif isinstance(other, str):
962 other = self.__class__.Parse(other)
963 elif isinstance(other, int):
964 other = self.__class__(major=other)
965 else:
966 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
967 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
968 raise ex
970 result = self._compare(self, other)
971 return not result if result is not None else equalValue
973 def __rshift__(self, other: Union["Version", str, int, None]) -> bool:
974 if other is None:
975 raise ValueError(f"Second operand is None.")
976 elif isinstance(other, self.__class__):
977 pass
978 elif isinstance(other, str):
979 other = self.__class__.Parse(other)
980 elif isinstance(other, int):
981 other = self.__class__(major=other)
982 else:
983 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by %= operator.")
984 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
985 raise ex
987 return self._minimum(self, other)
989 def __hash__(self) -> int:
990 if self.__hash is None: 990 ↛ 1005line 990 didn't jump to line 1005 because the condition on line 990 was always true
991 self.__hash = hash((
992 self._prefix,
993 self._major,
994 self._minor,
995 self._micro,
996 self._releaseLevel,
997 self._releaseNumber,
998 self._post,
999 self._dev,
1000 self._build,
1001 self._postfix,
1002 self._hash,
1003 self._flags
1004 ))
1005 return self.__hash
1008@export
1009class SemanticVersion(Version):
1010 """Representation of a semantic version number like ``3.7.12``."""
1012 _PATTERN = re_compile(
1013 r"^"
1014 r"(?P<prefix>[a-zA-Z]*)"
1015 r"(?P<major>\d+)"
1016 r"(?:\.(?P<minor>\d+))?"
1017 r"(?:\.(?P<micro>\d+))?"
1018 r"(?:"
1019 r"(?:\.(?P<build>\d+))"
1020 r"|"
1021 r"(?:[-](?P<release>dev|final))"
1022 r"|"
1023 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
1024 r")?"
1025 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1026 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1027 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1028 r"$"
1029 )
1030# QUESTION: was this how many commits a version is ahead of the last tagged version?
1031# ahead: int = 0
1033 def __init__(
1034 self,
1035 major: int,
1036 minor: Nullable[int] = None,
1037 micro: Nullable[int] = None,
1038 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1039 number: Nullable[int] = None,
1040 post: Nullable[int] = None,
1041 dev: Nullable[int] = None,
1042 *,
1043 build: Nullable[int] = None,
1044 postfix: Nullable[str] = None,
1045 prefix: Nullable[str] = None,
1046 hash: Nullable[str] = None,
1047 flags: Flags = Flags.NoVCS
1048 ) -> None:
1049 """
1050 Initializes a semantic version number representation.
1052 :param major: Major number part of the version number.
1053 :param minor: Minor number part of the version number.
1054 :param micro: Micro (patch) number part of the version number.
1055 :param build: Build number part of the version number.
1056 :param level: tbd
1057 :param number: tbd
1058 :param post: Post number part of the version number.
1059 :param dev: Development number part of the version number.
1060 :param prefix: The version number's prefix.
1061 :param postfix: The version number's postfix.
1062 :param flags: The version number's flags.
1063 :param hash: tbd
1064 :raises TypeError: If parameter 'major' is not of type int.
1065 :raises ValueError: If parameter 'major' is a negative number.
1066 :raises TypeError: If parameter 'minor' is not of type int.
1067 :raises ValueError: If parameter 'minor' is a negative number.
1068 :raises TypeError: If parameter 'micro' is not of type int.
1069 :raises ValueError: If parameter 'micro' is a negative number.
1070 :raises TypeError: If parameter 'build' is not of type int.
1071 :raises ValueError: If parameter 'build' is a negative number.
1072 :raises TypeError: If parameter 'post' is not of type int.
1073 :raises ValueError: If parameter 'post' is a negative number.
1074 :raises TypeError: If parameter 'dev' is not of type int.
1075 :raises ValueError: If parameter 'dev' is a negative number.
1076 :raises TypeError: If parameter 'prefix' is not of type str.
1077 :raises TypeError: If parameter 'postfix' is not of type str.
1078 """
1079 super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags)
1081 @classmethod
1082 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "SemanticVersion":
1083 """
1084 Parse a version string and return a :class:`SemanticVersion` instance.
1086 Allowed prefix characters:
1088 * ``v|V`` - version, public version, public release
1089 * ``i|I`` - internal version, internal release
1090 * ``r|R`` - release, revision
1091 * ``rev|REV`` - revision
1093 :param versionString: The version string to parse.
1094 :returns: An object representing a semantic version.
1095 :raises TypeError: If parameter ``other`` is not a string.
1096 :raises ValueError: If parameter ``other`` is None.
1097 :raises ValueError: If parameter ``other`` is empty.
1098 """
1099 if versionString is None:
1100 raise ValueError("Parameter 'versionString' is None.")
1101 elif not isinstance(versionString, str):
1102 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1103 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1104 raise ex
1106 versionString = versionString.strip()
1107 if versionString == "":
1108 raise ValueError("Parameter 'versionString' is empty.")
1110 match = cls._PATTERN.match(versionString)
1111 if match is None:
1112 raise ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1114 def toInt(value: Nullable[str]) -> Nullable[int]:
1115 if value is None or value == "":
1116 return None
1117 try:
1118 return int(value)
1119 except ValueError as ex: # pragma: no cover
1120 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1122 release = match["release"]
1123 if release is not None:
1124 if release == "dev": 1124 ↛ 1126line 1124 didn't jump to line 1126 because the condition on line 1124 was always true
1125 releaseLevel = ReleaseLevel.Development
1126 elif release == "final":
1127 releaseLevel = ReleaseLevel.Final
1128 else: # pragma: no cover
1129 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1130 else:
1131 level = match["level"]
1132 if level is not None:
1133 level = level.lower()
1134 if level == "a" or level == "alpha":
1135 releaseLevel = ReleaseLevel.Alpha
1136 elif level == "b" or level == "beta":
1137 releaseLevel = ReleaseLevel.Beta
1138 elif level == "c" or level == "gamma":
1139 releaseLevel = ReleaseLevel.Gamma
1140 elif level == "rc":
1141 releaseLevel = ReleaseLevel.ReleaseCandidate
1142 else: # pragma: no cover
1143 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1144 else:
1145 releaseLevel = ReleaseLevel.Final
1147 version = cls(
1148 major=toInt(match["major"]),
1149 minor=toInt(match["minor"]),
1150 micro=toInt(match["micro"]),
1151 level=releaseLevel,
1152 number=toInt(match["number"]),
1153 post=toInt(match["post"]),
1154 dev=toInt(match["dev"]),
1155 build=toInt(match["build"]),
1156 postfix=match["postfix"],
1157 prefix=match["prefix"],
1158 # hash=match["hash"],
1159 flags=Flags.Clean
1160 )
1161 if validator is not None and not validator(version):
1162 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1164 return version
1166 @readonly
1167 def Patch(self) -> int:
1168 """
1169 Read-only property to access the patch number.
1171 The patch number is identical to the micro number.
1173 :return: The patch number.
1174 """
1175 return self._micro
1177 def _equal(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1178 """
1179 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1181 :param left: Left operand.
1182 :param right: Right operand.
1183 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1184 """
1185 return super()._equal(left, right)
1187 def _compare(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1188 """
1189 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1191 :param left: Left operand.
1192 :param right: Right operand.
1193 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1194 False if ``left`` is greater than ``right``. |br|
1195 Otherwise it's None (both operands are equal).
1196 """
1197 return super()._compare(left, right)
1199 def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1200 """
1201 Compare two version numbers for equality.
1203 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1204 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1205 number is assumed (all other parts are zero).
1207 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1208 number.
1210 :param other: Operand to compare against.
1211 :returns: ``True``, if both version numbers are equal.
1212 :raises ValueError: If parameter ``other`` is None.
1213 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1214 """
1215 return super().__eq__(other)
1217 def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1218 """
1219 Compare two version numbers for inequality.
1221 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1222 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1223 number is assumed (all other parts are zero).
1225 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1226 number.
1228 :param other: Operand to compare against.
1229 :returns: ``True``, if both version numbers are not equal.
1230 :raises ValueError: If parameter ``other`` is None.
1231 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1232 """
1233 return super().__ne__(other)
1235 def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1236 """
1237 Compare two version numbers if the version is less than the second operand.
1239 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1240 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1241 number is assumed (all other parts are zero).
1243 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1244 number.
1246 :param other: Operand to compare against.
1247 :returns: ``True``, if version is less than the second operand.
1248 :raises ValueError: If parameter ``other`` is None.
1249 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1250 """
1251 return super().__lt__(other)
1253 def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1254 """
1255 Compare two version numbers if the version is less than or equal the second operand.
1257 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1258 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1259 number is assumed (all other parts are zero).
1261 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1262 number.
1264 :param other: Operand to compare against.
1265 :returns: ``True``, if version is less than or equal the second operand.
1266 :raises ValueError: If parameter ``other`` is None.
1267 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1268 """
1269 return super().__le__(other)
1271 def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1272 """
1273 Compare two version numbers if the version is greater than the second operand.
1275 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1276 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1277 number is assumed (all other parts are zero).
1279 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1280 number.
1282 :param other: Operand to compare against.
1283 :returns: ``True``, if version is greater than the second operand.
1284 :raises ValueError: If parameter ``other`` is None.
1285 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1286 """
1287 return super().__gt__(other)
1289 def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1290 """
1291 Compare two version numbers if the version is greater than or equal the second operand.
1293 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1294 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1295 number is assumed (all other parts are zero).
1297 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1298 number.
1300 :param other: Operand to compare against.
1301 :returns: ``True``, if version is greater than or equal the second operand.
1302 :raises ValueError: If parameter ``other`` is None.
1303 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1304 """
1305 return super().__ge__(other)
1307 def __rshift__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1308 return super().__rshift__(other)
1310 def __hash__(self) -> int:
1311 return super().__hash__()
1313 def __format__(self, formatSpec: str) -> str:
1314 result = self._format(formatSpec)
1316 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1317 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1319 return result.replace("%%", "%")
1321 def __repr__(self) -> str:
1322 """
1323 Return a string representation of this version number without prefix ``v``.
1325 :returns: Raw version number representation without a prefix.
1326 """
1327 return f"{self._prefix if Parts.Prefix in self._parts else ''}{self._major}.{self._minor}.{self._micro}"
1329 def __str__(self) -> str:
1330 """
1331 Return a string representation of this version number.
1333 :returns: Version number representation.
1334 """
1335 result = self._prefix if Parts.Prefix in self._parts else ""
1336 result += f"{self._major}" # major is always present
1337 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1338 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1339 result += f".{self._build}" if Parts.Build in self._parts else ""
1340 if self._releaseLevel is ReleaseLevel.Development:
1341 result += "-dev"
1342 elif self._releaseLevel is ReleaseLevel.Alpha:
1343 result += f".alpha{self._releaseNumber}"
1344 elif self._releaseLevel is ReleaseLevel.Beta:
1345 result += f".beta{self._releaseNumber}"
1346 elif self._releaseLevel is ReleaseLevel.Gamma: 1346 ↛ 1347line 1346 didn't jump to line 1347 because the condition on line 1346 was never true
1347 result += f".gamma{self._releaseNumber}"
1348 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1349 result += f".rc{self._releaseNumber}"
1350 result += f".post{self._post}" if Parts.Post in self._parts else ""
1351 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1352 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1354 return result
1357@export
1358class PythonVersion(SemanticVersion):
1359 """
1360 Represents a Python version.
1361 """
1363 @classmethod
1364 def FromSysVersionInfo(cls) -> "PythonVersion":
1365 """
1366 Create a Python version from :data:`sys.version_info`.
1368 :returns: A PythonVersion instance of the current Python interpreter's version.
1369 """
1370 from sys import version_info
1372 if version_info.releaselevel == "final":
1373 rl = ReleaseLevel.Final
1374 number = None
1375 else: # pragma: no cover
1376 number = version_info.serial
1378 if version_info.releaselevel == "alpha":
1379 rl = ReleaseLevel.Alpha
1380 elif version_info.releaselevel == "beta":
1381 rl = ReleaseLevel.Beta
1382 elif version_info.releaselevel == "candidate":
1383 rl = ReleaseLevel.ReleaseCandidate
1384 else: # pragma: no cover
1385 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1387 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1389 def __hash__(self) -> int:
1390 return super().__hash__()
1392 def __str__(self) -> str:
1393 """
1394 Return a string representation of this version number.
1396 :returns: Version number representation.
1397 """
1398 result = self._prefix if Parts.Prefix in self._parts else ""
1399 result += f"{self._major}" # major is always present
1400 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1401 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1402 if self._releaseLevel is ReleaseLevel.Alpha:
1403 result += f"a{self._releaseNumber}"
1404 elif self._releaseLevel is ReleaseLevel.Beta:
1405 result += f"b{self._releaseNumber}"
1406 elif self._releaseLevel is ReleaseLevel.Gamma:
1407 result += f"c{self._releaseNumber}"
1408 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1409 result += f"rc{self._releaseNumber}"
1410 result += f".post{self._post}" if Parts.Post in self._parts else ""
1411 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1412 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1414 return result
1417@export
1418class CalendarVersion(Version):
1419 """Representation of a calendar version number like ``2021.10``."""
1421 def __init__(
1422 self,
1423 major: int,
1424 minor: Nullable[int] = None,
1425 micro: Nullable[int] = None,
1426 build: Nullable[int] = None,
1427 flags: Flags = Flags.Clean,
1428 prefix: Nullable[str] = None,
1429 postfix: Nullable[str] = None
1430 ) -> None:
1431 """
1432 Initializes a calendar version number representation.
1434 :param major: Major number part of the version number.
1435 :param minor: Minor number part of the version number.
1436 :param micro: Micro (patch) number part of the version number.
1437 :param build: Build number part of the version number.
1438 :param flags: The version number's flags.
1439 :param prefix: The version number's prefix.
1440 :param postfix: The version number's postfix.
1441 :raises TypeError: If parameter 'major' is not of type int.
1442 :raises ValueError: If parameter 'major' is a negative number.
1443 :raises TypeError: If parameter 'minor' is not of type int.
1444 :raises ValueError: If parameter 'minor' is a negative number.
1445 :raises TypeError: If parameter 'micro' is not of type int.
1446 :raises ValueError: If parameter 'micro' is a negative number.
1447 :raises TypeError: If parameter 'build' is not of type int.
1448 :raises ValueError: If parameter 'build' is a negative number.
1449 :raises TypeError: If parameter 'prefix' is not of type str.
1450 :raises TypeError: If parameter 'postfix' is not of type str.
1451 """
1452 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1454 @classmethod
1455 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion":
1456 """
1457 Parse a version string and return a :class:`CalendarVersion` instance.
1459 :param versionString: The version string to parse.
1460 :returns: An object representing a calendar version.
1461 :raises TypeError: If parameter ``other`` is not a string.
1462 :raises ValueError: If parameter ``other`` is None.
1463 :raises ValueError: If parameter ``other`` is empty.
1464 """
1465 parts = Parts.Unknown
1467 if versionString is None:
1468 raise ValueError("Parameter 'versionString' is None.")
1469 elif not isinstance(versionString, str):
1470 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1471 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1472 raise ex
1473 elif versionString == "":
1474 raise ValueError("Parameter 'versionString' is empty.")
1476 split = versionString.split(".")
1477 length = len(split)
1478 major = int(split[0])
1479 minor = 0
1480 parts |= Parts.Major
1482 if length >= 2:
1483 minor = int(split[1])
1484 parts |= Parts.Minor
1486 flags = Flags.Clean
1488 version = cls(major, minor, flags=flags)
1489 if validator is not None and not validator(version):
1490 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1492 return version
1494 @property
1495 def Year(self) -> int:
1496 """
1497 Read-only property to access the year part.
1499 :return: The year part.
1500 """
1501 return self._major
1503 def _equal(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1504 """
1505 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1507 :param left: Left parameter.
1508 :param right: Right parameter.
1509 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1510 """
1511 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1513 def _compare(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1514 """
1515 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1517 :param left: Left parameter.
1518 :param right: Right parameter.
1519 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1520 False if ``left`` is greater than ``right``. |br|
1521 Otherwise it's None (both parameters are equal).
1522 """
1523 if left._major < right._major:
1524 return True
1525 elif left._major > right._major:
1526 return False
1528 if left._minor < right._minor:
1529 return True
1530 elif left._minor > right._minor:
1531 return False
1533 if left._micro < right._micro: 1533 ↛ 1534line 1533 didn't jump to line 1534 because the condition on line 1533 was never true
1534 return True
1535 elif left._micro > right._micro: 1535 ↛ 1536line 1535 didn't jump to line 1536 because the condition on line 1535 was never true
1536 return False
1538 return None
1540 def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1541 """
1542 Compare two version numbers for equality.
1544 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1545 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1546 number is assumed (all other parts are zero).
1548 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1549 number.
1551 :param other: Parameter to compare against.
1552 :returns: ``True``, if both version numbers are equal.
1553 :raises ValueError: If parameter ``other`` is None.
1554 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1555 """
1556 return super().__eq__(other)
1558 def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1559 """
1560 Compare two version numbers for inequality.
1562 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1563 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1564 number is assumed (all other parts are zero).
1566 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1567 number.
1569 :param other: Parameter to compare against.
1570 :returns: ``True``, if both version numbers are not equal.
1571 :raises ValueError: If parameter ``other`` is None.
1572 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1573 """
1574 return super().__ne__(other)
1576 def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1577 """
1578 Compare two version numbers if the version is less than the second operand.
1580 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1581 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1582 number is assumed (all other parts are zero).
1584 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1585 number.
1587 :param other: Parameter to compare against.
1588 :returns: ``True``, if version is less than the second operand.
1589 :raises ValueError: If parameter ``other`` is None.
1590 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1591 """
1592 return super().__lt__(other)
1594 def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1595 """
1596 Compare two version numbers if the version is less than or equal the second operand.
1598 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1599 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1600 number is assumed (all other parts are zero).
1602 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1603 number.
1605 :param other: Parameter to compare against.
1606 :returns: ``True``, if version is less than or equal the second operand.
1607 :raises ValueError: If parameter ``other`` is None.
1608 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1609 """
1610 return super().__le__(other)
1612 def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1613 """
1614 Compare two version numbers if the version is greater than the second operand.
1616 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1617 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1618 number is assumed (all other parts are zero).
1620 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1621 number.
1623 :param other: Parameter to compare against.
1624 :returns: ``True``, if version is greater than the second operand.
1625 :raises ValueError: If parameter ``other`` is None.
1626 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1627 """
1628 return super().__gt__(other)
1630 def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1631 """
1632 Compare two version numbers if the version is greater than or equal the second operand.
1634 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1635 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1636 number is assumed (all other parts are zero).
1638 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1639 number.
1641 :param other: Parameter to compare against.
1642 :returns: ``True``, if version is greater than or equal the second operand.
1643 :raises ValueError: If parameter ``other`` is None.
1644 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1645 """
1646 return super().__ge__(other)
1648 def __hash__(self) -> int:
1649 return super().__hash__()
1651 def __format__(self, formatSpec: str) -> str:
1652 """
1653 Return a string representation of this version number according to the format specification.
1655 .. topic:: Format Specifiers
1657 * ``%M`` - major number (year)
1658 * ``%m`` - minor number (month/week)
1660 :param formatSpec: The format specification.
1661 :return: Formatted version number.
1662 """
1663 if formatSpec == "":
1664 return self.__str__()
1666 result = formatSpec
1667 # result = result.replace("%P", str(self._prefix))
1668 result = result.replace("%M", str(self._major))
1669 result = result.replace("%m", str(self._minor))
1670 # result = result.replace("%p", str(self._pre))
1672 return result.replace("%%", "%")
1674 def __repr__(self) -> str:
1675 """
1676 Return a string representation of this version number without prefix ``v``.
1678 :returns: Raw version number representation without a prefix.
1679 """
1680 return f"{self._major}.{self._minor}"
1682 def __str__(self) -> str:
1683 """
1684 Return a string representation of this version number with prefix ``v``.
1686 :returns: Version number representation including a prefix.
1687 """
1688 result = f"{self._major}"
1689 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1691 return result
1694@export
1695class YearMonthVersion(CalendarVersion):
1696 """Representation of a calendar version number made of year and month like ``2021.10``."""
1698 def __init__(
1699 self,
1700 year: int,
1701 month: Nullable[int] = None,
1702 build: Nullable[int] = None,
1703 flags: Flags = Flags.Clean,
1704 prefix: Nullable[str] = None,
1705 postfix: Nullable[str] = None
1706 ) -> None:
1707 """
1708 Initializes a year-month version number representation.
1710 :param year: Year part of the version number.
1711 :param month: Month part of the version number.
1712 :param build: Build number part of the version number.
1713 :param flags: The version number's flags.
1714 :param prefix: The version number's prefix.
1715 :param postfix: The version number's postfix.
1716 :raises TypeError: If parameter 'major' is not of type int.
1717 :raises ValueError: If parameter 'major' is a negative number.
1718 :raises TypeError: If parameter 'minor' is not of type int.
1719 :raises ValueError: If parameter 'minor' is a negative number.
1720 :raises TypeError: If parameter 'micro' is not of type int.
1721 :raises ValueError: If parameter 'micro' is a negative number.
1722 :raises TypeError: If parameter 'build' is not of type int.
1723 :raises ValueError: If parameter 'build' is a negative number.
1724 :raises TypeError: If parameter 'prefix' is not of type str.
1725 :raises TypeError: If parameter 'postfix' is not of type str.
1726 """
1727 super().__init__(year, month, 0, build, flags, prefix, postfix)
1729 @property
1730 def Month(self) -> int:
1731 """
1732 Read-only property to access the month part.
1734 :return: The month part.
1735 """
1736 return self._minor
1738 def __hash__(self) -> int:
1739 return super().__hash__()
1742@export
1743class YearWeekVersion(CalendarVersion):
1744 """Representation of a calendar version number made of year and week like ``2021.47``."""
1746 def __init__(
1747 self,
1748 year: int,
1749 week: Nullable[int] = None,
1750 build: Nullable[int] = None,
1751 flags: Flags = Flags.Clean,
1752 prefix: Nullable[str] = None,
1753 postfix: Nullable[str] = None
1754 ) -> None:
1755 """
1756 Initializes a year-week version number representation.
1758 :param year: Year part of the version number.
1759 :param week: Week part of the version number.
1760 :param build: Build number part of the version number.
1761 :param flags: The version number's flags.
1762 :param prefix: The version number's prefix.
1763 :param postfix: The version number's postfix.
1764 :raises TypeError: If parameter 'major' is not of type int.
1765 :raises ValueError: If parameter 'major' is a negative number.
1766 :raises TypeError: If parameter 'minor' is not of type int.
1767 :raises ValueError: If parameter 'minor' is a negative number.
1768 :raises TypeError: If parameter 'micro' is not of type int.
1769 :raises ValueError: If parameter 'micro' is a negative number.
1770 :raises TypeError: If parameter 'build' is not of type int.
1771 :raises ValueError: If parameter 'build' is a negative number.
1772 :raises TypeError: If parameter 'prefix' is not of type str.
1773 :raises TypeError: If parameter 'postfix' is not of type str.
1774 """
1775 super().__init__(year, week, 0, build, flags, prefix, postfix)
1777 @property
1778 def Week(self) -> int:
1779 """
1780 Read-only property to access the week part.
1782 :return: The week part.
1783 """
1784 return self._minor
1786 def __hash__(self) -> int:
1787 return super().__hash__()
1790@export
1791class YearReleaseVersion(CalendarVersion):
1792 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
1794 def __init__(
1795 self,
1796 year: int,
1797 release: Nullable[int] = None,
1798 build: Nullable[int] = None,
1799 flags: Flags = Flags.Clean,
1800 prefix: Nullable[str] = None,
1801 postfix: Nullable[str] = None
1802 ) -> None:
1803 """
1804 Initializes a year-release version number representation.
1806 :param year: Year part of the version number.
1807 :param release: Release number of the version number.
1808 :param build: Build number part of the version number.
1809 :param flags: The version number's flags.
1810 :param prefix: The version number's prefix.
1811 :param postfix: The version number's postfix.
1812 :raises TypeError: If parameter 'major' is not of type int.
1813 :raises ValueError: If parameter 'major' is a negative number.
1814 :raises TypeError: If parameter 'minor' is not of type int.
1815 :raises ValueError: If parameter 'minor' is a negative number.
1816 :raises TypeError: If parameter 'micro' is not of type int.
1817 :raises ValueError: If parameter 'micro' is a negative number.
1818 :raises TypeError: If parameter 'build' is not of type int.
1819 :raises ValueError: If parameter 'build' is a negative number.
1820 :raises TypeError: If parameter 'prefix' is not of type str.
1821 :raises TypeError: If parameter 'postfix' is not of type str.
1822 """
1823 super().__init__(year, release, 0, build, flags, prefix, postfix)
1825 @property
1826 def Release(self) -> int:
1827 """
1828 Read-only property to access the release number.
1830 :return: The release number.
1831 """
1832 return self._minor
1834 def __hash__(self) -> int:
1835 return super().__hash__()
1838@export
1839class YearMonthDayVersion(CalendarVersion):
1840 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
1842 def __init__(
1843 self,
1844 year: int,
1845 month: Nullable[int] = None,
1846 day: Nullable[int] = None,
1847 build: Nullable[int] = None,
1848 flags: Flags = Flags.Clean,
1849 prefix: Nullable[str] = None,
1850 postfix: Nullable[str] = None
1851 ) -> None:
1852 """
1853 Initializes a year-month-day version number representation.
1855 :param year: Year part of the version number.
1856 :param month: Month part of the version number.
1857 :param day: Day part of the version number.
1858 :param build: Build number part of the version number.
1859 :param flags: The version number's flags.
1860 :param prefix: The version number's prefix.
1861 :param postfix: The version number's postfix.
1862 :raises TypeError: If parameter 'major' is not of type int.
1863 :raises ValueError: If parameter 'major' is a negative number.
1864 :raises TypeError: If parameter 'minor' is not of type int.
1865 :raises ValueError: If parameter 'minor' is a negative number.
1866 :raises TypeError: If parameter 'micro' is not of type int.
1867 :raises ValueError: If parameter 'micro' is a negative number.
1868 :raises TypeError: If parameter 'build' is not of type int.
1869 :raises ValueError: If parameter 'build' is a negative number.
1870 :raises TypeError: If parameter 'prefix' is not of type str.
1871 :raises TypeError: If parameter 'postfix' is not of type str.
1872 """
1873 super().__init__(year, month, day, build, flags, prefix, postfix)
1875 @property
1876 def Month(self) -> int:
1877 """
1878 Read-only property to access the month part.
1880 :return: The month part.
1881 """
1882 return self._minor
1884 @property
1885 def Day(self) -> int:
1886 """
1887 Read-only property to access the day part.
1889 :return: The day part.
1890 """
1891 return self._micro
1893 def __hash__(self) -> int:
1894 return super().__hash__()
1897V = TypeVar("V", bound=Version)
1899@export
1900class RangeBoundHandling(Flag):
1901 """
1902 A flag defining how to handle bounds in a range.
1904 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
1905 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
1906 """
1907 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
1908 LowerBoundInclusive = 0 #: Lower bound is inclusive.
1909 UpperBoundInclusive = 0 #: Upper bound is inclusive.
1910 LowerBoundExclusive = 1 #: Lower bound is exclusive.
1911 UpperBoundExclusive = 2 #: Upper bound is exclusive.
1912 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
1915@export
1916class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
1917 """
1918 Representation of a version range described by a lower bound and upper bound version.
1920 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
1921 """
1922 _lowerBound: V
1923 _upperBound: V
1924 _boundHandling: RangeBoundHandling
1926 def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None:
1927 """
1928 Initializes a version range described by a lower and upper bound.
1930 :param lowerBound: lowest version (inclusive).
1931 :param upperBound: hightest version (inclusive).
1932 :raises TypeError: If parameter ``lowerBound`` is not of type :class:`Version`.
1933 :raises TypeError: If parameter ``upperBound`` is not of type :class:`Version`.
1934 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
1935 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
1936 """
1937 if not isinstance(lowerBound, Version):
1938 ex = TypeError(f"Parameter 'lowerBound' is not of type 'Version'.")
1939 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
1940 raise ex
1942 if not isinstance(upperBound, Version):
1943 ex = TypeError(f"Parameter 'upperBound' is not of type 'Version'.")
1944 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
1945 raise ex
1947 if not ((lBC := lowerBound.__class__) is (uBC := upperBound.__class__) or issubclass(lBC, uBC) or issubclass(uBC, lBC)):
1948 ex = TypeError(f"Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
1949 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and type '{getFullyQualifiedName(upperBound)}' for upperBound.")
1950 raise ex
1952 if not (lowerBound <= upperBound):
1953 ex = ValueError(f"Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
1954 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
1955 raise ex
1957 self._lowerBound = lowerBound
1958 self._upperBound = upperBound
1959 self._boundHandling = boundHandling
1961 @property
1962 def LowerBound(self) -> V:
1963 """
1964 Property to access the range's lower bound.
1966 :return: Lower bound of the version range.
1967 """
1968 return self._lowerBound
1970 @LowerBound.setter
1971 def LowerBound(self, value: V) -> None:
1972 if not isinstance(value, Version):
1973 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
1974 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
1975 raise ex
1977 self._lowerBound = value
1979 @readonly
1980 def UpperBound(self) -> V:
1981 """
1982 Property to access the range's upper bound.
1984 :return: Upper bound of the version range.
1985 """
1986 return self._upperBound
1988 @UpperBound.setter
1989 def UpperBound(self, value: V) -> None:
1990 if not isinstance(value, Version):
1991 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
1992 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
1993 raise ex
1995 self._upperBound = value
1997 @readonly
1998 def BoundHandling(self) -> RangeBoundHandling:
1999 """
2000 Property to access the range's bound handling strategy.
2002 :return: The range's bound handling strategy.
2003 """
2004 return self._boundHandling
2006 @BoundHandling.setter
2007 def BoundHandling(self, value: RangeBoundHandling) -> None:
2008 if not isinstance(value, RangeBoundHandling):
2009 ex = TypeError(f"Parameter 'value' is not of type 'RangeBoundHandling'.")
2010 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2011 raise ex
2013 self._boundHandling = value
2015 def __and__(self, other: Any) -> "VersionRange[T]":
2016 """
2017 Compute the intersection of two version ranges.
2019 :param other: Second version range to intersect with.
2020 :returns: Intersected version range.
2021 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2022 :raises ValueError: If intersection is empty.
2023 """
2024 if not isinstance(other, VersionRange): 2024 ↛ 2025line 2024 didn't jump to line 2025 because the condition on line 2024 was never true
2025 ex = TypeError(f"Parameter 'other' is not of type 'VersionRange'.")
2026 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2027 raise ex
2029 if not (isinstance(other._lowerBound, self._lowerBound.__class__) and isinstance(self._lowerBound, other._lowerBound.__class__)): 2029 ↛ 2030line 2029 didn't jump to line 2030 because the condition on line 2029 was never true
2030 ex = TypeError(f"Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible with each other.")
2031 ex.add_note(
2032 f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2033 raise ex
2035 if other._lowerBound < self._lowerBound:
2036 lBound = self._lowerBound
2037 elif other._lowerBound in self: 2037 ↛ 2040line 2037 didn't jump to line 2040 because the condition on line 2037 was always true
2038 lBound = other._lowerBound
2039 else:
2040 raise ValueError()
2042 if other._upperBound > self._upperBound:
2043 uBound = self._upperBound
2044 elif other._upperBound in self: 2044 ↛ 2047line 2044 didn't jump to line 2047 because the condition on line 2044 was always true
2045 uBound = other._upperBound
2046 else:
2047 raise ValueError()
2049 return self.__class__(lBound, uBound)
2051 def __lt__(self, other: Any) -> bool:
2052 """
2053 Compare a version range and a version numbers if the version range is less than the second operand (version).
2055 :param other: Operand to compare against.
2056 :returns: ``True``, if version range is less than the second operand (version).
2057 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2058 """
2059 # TODO: support VersionRange < VersionRange too
2060 # TODO: support str, int, ... like Version ?
2061 if not isinstance(other, Version):
2062 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2063 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2064 raise ex
2066 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2066 ↛ 2067line 2066 didn't jump to line 2067 because the condition on line 2066 was never true
2067 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2068 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2069 raise ex
2071 return self._upperBound < other
2073 def __le__(self, other: Any) -> bool:
2074 """
2075 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2077 :param other: Operand to compare against.
2078 :returns: ``True``, if version range is less than or equal the second operand (version).
2079 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2080 """
2081 # TODO: support VersionRange < VersionRange too
2082 # TODO: support str, int, ... like Version ?
2083 if not isinstance(other, Version): 2083 ↛ 2084line 2083 didn't jump to line 2084 because the condition on line 2083 was never true
2084 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2085 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2086 raise ex
2088 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2088 ↛ 2089line 2088 didn't jump to line 2089 because the condition on line 2088 was never true
2089 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2090 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2091 raise ex
2093 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2094 return self._upperBound < other
2095 else:
2096 return self._upperBound <= other
2098 def __gt__(self, other: Any) -> bool:
2099 """
2100 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2102 :param other: Operand to compare against.
2103 :returns: ``True``, if version range is greater than the second operand (version).
2104 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2105 """
2106 # TODO: support VersionRange < VersionRange too
2107 # TODO: support str, int, ... like Version ?
2108 if not isinstance(other, Version): 2108 ↛ 2109line 2108 didn't jump to line 2109 because the condition on line 2108 was never true
2109 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2110 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2111 raise ex
2113 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2113 ↛ 2114line 2113 didn't jump to line 2114 because the condition on line 2113 was never true
2114 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2115 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2116 raise ex
2118 return self._lowerBound > other
2120 def __ge__(self, other: Any) -> bool:
2121 """
2122 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2124 :param other: Operand to compare against.
2125 :returns: ``True``, if version range is greater than or equal the second operand (version).
2126 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2127 """
2128 # TODO: support VersionRange < VersionRange too
2129 # TODO: support str, int, ... like Version ?
2130 if not isinstance(other, Version): 2130 ↛ 2131line 2130 didn't jump to line 2131 because the condition on line 2130 was never true
2131 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2132 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2133 raise ex
2135 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2135 ↛ 2136line 2135 didn't jump to line 2136 because the condition on line 2135 was never true
2136 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2137 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2138 raise ex
2140 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2140 ↛ 2141line 2140 didn't jump to line 2141 because the condition on line 2140 was never true
2141 return self._lowerBound > other
2142 else:
2143 return self._lowerBound >= other
2145 def __contains__(self, version: Version) -> bool:
2146 """
2147 Check if the version is in the version range.
2149 :param version: Version to check.
2150 :returns: ``True``, if version is in range.
2151 :raises TypeError: If parameter ``version`` is not of type :class:`Version`.
2152 """
2153 if not isinstance(version, Version): 2153 ↛ 2154line 2153 didn't jump to line 2154 because the condition on line 2153 was never true
2154 ex = TypeError(f"Parameter 'item' is not of type 'Version'.")
2155 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2156 raise ex
2158 if self._boundHandling is RangeBoundHandling.BothBoundsInclusive: 2158 ↛ 2160line 2158 didn't jump to line 2160 because the condition on line 2158 was always true
2159 return self._lowerBound <= version <= self._upperBound
2160 elif self._boundHandling is (RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive):
2161 return self._lowerBound <= version < self._upperBound
2162 elif self._boundHandling is (RangeBoundHandling.LowerBoundExclusive | RangeBoundHandling.UpperBoundInclusive):
2163 return self._lowerBound < version <= self._upperBound
2164 else:
2165 return self._lowerBound < version < self._upperBound
2168@export
2169class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2170 """
2171 Representation of an ordered set of versions.
2173 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2174 """
2175 _items: List[V] #: An ordered list of set members.
2177 def __init__(self, versions: Union[Version, Iterable[V]]):
2178 """
2179 Initializes a version set either by a single version or an iterable of versions.
2181 :param versions: A single version or an iterable of versions.
2182 :raises ValueError: If parameter ``versions`` is None`.
2183 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2184 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2185 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2186 """
2187 if versions is None:
2188 raise ValueError(f"Parameter 'versions' is None.")
2190 if isinstance(versions, Version):
2191 self._items = [versions]
2192 elif isinstance(versions, abc_Iterable): 2192 ↛ 2210line 2192 didn't jump to line 2210 because the condition on line 2192 was always true
2193 iterator = iter(versions)
2194 try:
2195 firstVersion = next(iterator)
2196 except StopIteration:
2197 self._items = []
2198 return
2200 if not isinstance(firstVersion, Version): 2200 ↛ 2201line 2200 didn't jump to line 2201 because the condition on line 2200 was never true
2201 raise TypeError(f"First element in parameter 'versions' is not of type Version.")
2203 baseType = firstVersion.__class__
2204 for version in iterator:
2205 if not isinstance(version, baseType):
2206 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2208 self._items = list(sorted(versions))
2209 else:
2210 raise TypeError(f"Parameter 'versions' is not an Iterable.")
2212 def __and__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2213 """
2214 Compute intersection of two version sets.
2216 :param other: Second set of versions.
2217 :returns: Intersection of two version sets.
2218 """
2219 selfIterator = self.__iter__()
2220 otherIterator = other.__iter__()
2222 result = []
2223 try:
2224 selfValue = next(selfIterator)
2225 otherValue = next(otherIterator)
2227 while True:
2228 if selfValue < otherValue:
2229 selfValue = next(selfIterator)
2230 elif otherValue < selfValue:
2231 otherValue = next(otherIterator)
2232 else:
2233 result.append(selfValue)
2234 selfValue = next(selfIterator)
2235 otherValue = next(otherIterator)
2237 except StopIteration:
2238 pass
2240 return VersionSet(result)
2242 def __or__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2243 """
2244 Compute union of two version sets.
2246 :param other: Second set of versions.
2247 :returns: Union of two version sets.
2248 """
2249 selfIterator = self.__iter__()
2250 otherIterator = other.__iter__()
2252 result = []
2253 try:
2254 selfValue = next(selfIterator)
2255 except StopIteration:
2256 for otherValue in otherIterator:
2257 result.append(otherValue)
2259 try:
2260 otherValue = next(otherIterator)
2261 except StopIteration:
2262 for selfValue in selfIterator:
2263 result.append(selfValue)
2265 while True:
2266 if selfValue < otherValue:
2267 result.append(selfValue)
2268 try:
2269 selfValue = next(selfIterator)
2270 except StopIteration:
2271 result.append(otherValue)
2272 for otherValue in otherIterator: 2272 ↛ 2273line 2272 didn't jump to line 2273 because the loop on line 2272 never started
2273 result.append(otherValue)
2275 break
2276 elif otherValue < selfValue:
2277 result.append(otherValue)
2278 try:
2279 otherValue = next(otherIterator)
2280 except StopIteration:
2281 result.append(selfValue)
2282 for selfValue in selfIterator:
2283 result.append(selfValue)
2285 break
2286 else:
2287 result.append(selfValue)
2288 try:
2289 selfValue = next(selfIterator)
2290 except StopIteration:
2291 for otherValue in otherIterator: 2291 ↛ 2292line 2291 didn't jump to line 2292 because the loop on line 2291 never started
2292 result.append(otherValue)
2294 break
2296 try:
2297 otherValue = next(otherIterator)
2298 except StopIteration:
2299 for selfValue in selfIterator:
2300 result.append(selfValue)
2302 break
2304 return VersionSet(result)
2306 def __lt__(self, other: Any) -> bool:
2307 """
2308 Compare a version set and a version numbers if the version set is less than the second operand (version).
2310 :param other: Operand to compare against.
2311 :returns: ``True``, if version set is less than the second operand (version).
2312 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2313 """
2314 # TODO: support VersionRange < VersionRange too
2315 # TODO: support str, int, ... like Version ?
2316 if not isinstance(other, Version):
2317 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2318 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2319 raise ex
2321 return self._items[-1] < other
2323 def __le__(self, other: Any) -> bool:
2324 """
2325 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
2327 :param other: Operand to compare against.
2328 :returns: ``True``, if version set is less than or equal the second operand (version).
2329 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2330 """
2331 # TODO: support VersionRange < VersionRange too
2332 # TODO: support str, int, ... like Version ?
2333 if not isinstance(other, Version): 2333 ↛ 2334line 2333 didn't jump to line 2334 because the condition on line 2333 was never true
2334 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2335 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2336 raise ex
2338 return self._items[-1] <= other
2340 def __gt__(self, other: Any) -> bool:
2341 """
2342 Compare a version set and a version numbers if the version set is greater than the second operand (version).
2344 :param other: Operand to compare against.
2345 :returns: ``True``, if version set is greater than the second operand (version).
2346 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2347 """
2348 # TODO: support VersionRange < VersionRange too
2349 # TODO: support str, int, ... like Version ?
2350 if not isinstance(other, Version): 2350 ↛ 2351line 2350 didn't jump to line 2351 because the condition on line 2350 was never true
2351 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2352 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2353 raise ex
2355 return self._items[0] > other
2357 def __ge__(self, other: Any) -> bool:
2358 """
2359 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
2361 :param other: Operand to compare against.
2362 :returns: ``True``, if version set is greater than or equal the second operand (version).
2363 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2364 """
2365 # TODO: support VersionRange < VersionRange too
2366 # TODO: support str, int, ... like Version ?
2367 if not isinstance(other, Version): 2367 ↛ 2368line 2367 didn't jump to line 2368 because the condition on line 2367 was never true
2368 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2369 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2370 raise ex
2372 return self._items[0] >= other
2374 def __contains__(self, version: V) -> bool:
2375 """
2376 Checks if the version a member of the set.
2378 :param version: The version to check.
2379 :returns: ``True``, if the version is a member of the set.
2380 """
2381 return version in self._items
2383 def __len__(self) -> int:
2384 """
2385 Returns the number of members in the set.
2387 :returns: Number of set members.
2388 """
2389 return len(self._items)
2391 def __iter__(self) -> Iterator[V]:
2392 """
2393 Returns an iterator to iterate all versions of this set from lowest to highest.
2395 :returns: Iterator to iterate versions.
2396 """
2397 return self._items.__iter__()
2399 def __getitem__(self, index: int) -> V:
2400 """
2401 Access to a version of a set by index.
2403 :param index: The index of the version to access.
2404 :returns: The indexed version.
2406 .. hint:: Versions are ordered from lowest to highest version number.
2407 """
2408 return self._items[index]