Coverage for pyTooling / Versioning / __init__.py: 83%
973 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-08 23:46 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-08 23:46 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2026 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::
36 See :ref:`high-level help <VERSIONING>` for explanations and usage examples.
37"""
38from collections.abc import Iterable as abc_Iterable
39from enum import Flag, Enum
40from re import compile as re_compile
41from typing import Optional as Nullable, Union, Callable, Any, Generic, TypeVar, Iterable, Iterator, List
43try:
44 from pyTooling.Decorators import export, readonly
45 from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
46 from pyTooling.Exceptions import ToolingException
47 from pyTooling.Common import getFullyQualifiedName
48except (ImportError, ModuleNotFoundError): # pragma: no cover
49 print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!")
51 try:
52 from Decorators import export, readonly
53 from MetaClasses import ExtendedType, abstractmethod, mustoverride
54 from Exceptions import ToolingException
55 from Common import getFullyQualifiedName
56 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
57 print("[pyTooling.Versioning] Could not import directly!")
58 raise ex
61@export
62class Parts(Flag):
63 """Enumeration describing parts of a version number that can be present."""
64 Unknown = 0 #: Undocumented
65 Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``).
66 Year = 1 #: Year is present. (e.g. X in ``XXXX.10``).
67 Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
68 Month = 2 #: Month is present. (e.g. X in ``2024.YY``).
69 Week = 2 #: Week is present. (e.g. X in ``2024.YY``).
70 Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
71 Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
72 Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``).
73 Level = 8 #: Release level is present.
74 Dev = 16 #: Development part is present.
75 Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
76 Post = 64 #: Post-release number is present.
77 Prefix = 128 #: Prefix is present.
78 Postfix = 256 #: Postfix is present.
79 Hash = 512 #: Hash is present.
80# AHead = 256
83@export
84class ReleaseLevel(Enum):
85 """Enumeration describing the version's maturity level."""
86 Final = 0 #:
87 ReleaseCandidate = -10 #:
88 Development = -20 #:
89 Gamma = -30 #:
90 Beta = -40 #:
91 Alpha = -50 #:
93 def __eq__(self, other: Any) -> bool:
94 """
95 Compare two release levels if the level is equal to the second operand.
97 :param other: Operand to compare against.
98 :returns: ``True``, if release level is equal the second operand's release level.
99 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
100 """
101 if isinstance(other, str): 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 other = ReleaseLevel(other)
104 if not isinstance(other, ReleaseLevel): 104 ↛ 105line 104 didn't jump to line 105 because the condition on line 104 was never true
105 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
106 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
107 raise ex
109 return self is other
111 def __ne__(self, other: Any) -> bool:
112 """
113 Compare two release levels if the level is unequal to the second operand.
115 :param other: Operand to compare against.
116 :returns: ``True``, if release level is unequal the second operand's release level.
117 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
118 """
119 if isinstance(other, str):
120 other = ReleaseLevel(other)
122 if not isinstance(other, ReleaseLevel):
123 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.")
124 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
125 raise ex
127 return self is not other
129 def __lt__(self, other: Any) -> bool:
130 """
131 Compare two release levels if the level is less than the second operand.
133 :param other: Operand to compare against.
134 :returns: ``True``, if release level is less than the second operand.
135 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
136 """
137 if isinstance(other, str): 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true
138 other = ReleaseLevel(other)
140 if not isinstance(other, ReleaseLevel): 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
142 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
143 raise ex
145 return self.value < other.value
147 def __le__(self, other: Any) -> bool:
148 """
149 Compare two release levels if the level is less than or equal the second operand.
151 :param other: Operand to compare against.
152 :returns: ``True``, if release level is less than or equal the second operand.
153 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
154 """
155 if isinstance(other, str):
156 other = ReleaseLevel(other)
158 if not isinstance(other, ReleaseLevel):
159 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <=>= operator.")
160 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
161 raise ex
163 return self.value <= other.value
165 def __gt__(self, other: Any) -> bool:
166 """
167 Compare two release levels if the level is greater than the second operand.
169 :param other: Operand to compare against.
170 :returns: ``True``, if release level is greater than the second operand.
171 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
172 """
173 if isinstance(other, str): 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true
174 other = ReleaseLevel(other)
176 if not isinstance(other, ReleaseLevel): 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
178 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
179 raise ex
181 return self.value > other.value
183 def __ge__(self, other: Any) -> bool:
184 """
185 Compare two release levels if the level is greater than or equal the second operand.
187 :param other: Operand to compare against.
188 :returns: ``True``, if release level is greater than or equal the second operand.
189 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
190 """
191 if isinstance(other, str):
192 other = ReleaseLevel(other)
194 if not isinstance(other, ReleaseLevel):
195 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
196 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
197 raise ex
199 return self.value >= other.value
201 def __hash__(self) -> int:
202 return hash(self.value)
204 def __str__(self) -> str:
205 """
206 Returns the release level's string equivalent.
208 :returns: The string equivalent of the release level.
209 """
210 if self is ReleaseLevel.Final:
211 return "final"
212 elif self is ReleaseLevel.ReleaseCandidate:
213 return "rc"
214 elif self is ReleaseLevel.Development: 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true
215 return "dev"
216 elif self is ReleaseLevel.Beta: 216 ↛ 217line 216 didn't jump to line 217 because the condition on line 216 was never true
217 return "beta"
218 elif self is ReleaseLevel.Alpha: 218 ↛ 221line 218 didn't jump to line 221 because the condition on line 218 was always true
219 return "alpha"
221 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
224@export
225class Flags(Flag):
226 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
227 NoVCS = 0 #: No Version Control System VCS
228 Clean = 1 #: A versioned build was created from a *clean* working directory.
229 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
231 CVS = 16 #: Concurrent Versions System (CVS)
232 SVN = 32 #: Subversion (SVN)
233 Git = 64 #: Git
234 Hg = 128 #: Mercurial (Hg)
237@export
238def WordSizeValidator(
239 bits: Nullable[int] = None,
240 majorBits: Nullable[int] = None,
241 minorBits: Nullable[int] = None,
242 microBits: Nullable[int] = None,
243 buildBits: Nullable[int] = None
244):
245 """
246 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
248 :param bits: Number of bits to encode any positive version number part.
249 :param majorBits: Number of bits to encode a positive major number in a version.
250 :param minorBits: Number of bits to encode a positive minor number in a version.
251 :param microBits: Number of bits to encode a positive micro number in a version.
252 :param buildBits: Number of bits to encode a positive build number in a version.
253 :return: A validation function for Version instances.
254 """
255 majorMax = minorMax = microMax = buildMax = -1
256 if bits is not None:
257 majorMax = minorMax = microMax = buildMax = 2**bits - 1
259 if majorBits is not None:
260 majorMax = 2**majorBits - 1
261 if minorBits is not None:
262 minorMax = 2**minorBits - 1
263 if microBits is not None:
264 microMax = 2 ** microBits - 1
265 if buildBits is not None: 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true
266 buildMax = 2**buildBits - 1
268 def validator(version: SemanticVersion) -> bool:
269 if Parts.Major in version._parts and version._major > majorMax:
270 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
272 if Parts.Minor in version._parts and version._minor > minorMax:
273 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
275 if Parts.Micro in version._parts and version._micro > microMax:
276 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
278 if Parts.Build in version._parts and version._build > buildMax: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
281 return True
283 return validator
286@export
287def MaxValueValidator(
288 max: Nullable[int] = None,
289 majorMax: Nullable[int] = None,
290 minorMax: Nullable[int] = None,
291 microMax: Nullable[int] = None,
292 buildMax: Nullable[int] = None
293):
294 """
295 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
297 :param max: The upper bound for any positive version number part.
298 :param majorMax: The upper bound for the positive major number.
299 :param minorMax: The upper bound for the positive minor number.
300 :param microMax: The upper bound for the positive micro number.
301 :param buildMax: The upper bound for the positive build number.
302 :return: A validation function for Version instances.
303 """
304 if max is not None: 304 ↛ 307line 304 didn't jump to line 307 because the condition on line 304 was always true
305 majorMax = minorMax = microMax = buildMax = max
307 def validator(version: SemanticVersion) -> bool:
308 if Parts.Major in version._parts and version._major > majorMax:
309 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
311 if Parts.Minor in version._parts and version._minor > minorMax:
312 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
314 if Parts.Micro in version._parts and version._micro > microMax:
315 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
317 if Parts.Build in version._parts and version._build > buildMax: 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true
318 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
320 return True
322 return validator
325@export
326class Version(metaclass=ExtendedType, slots=True):
327 """Base-class for a version representation."""
329 __hash: Nullable[int] #: once computed hash of the object
331 _parts: Parts #: Integer flag enumeration of present parts in a version number.
332 _prefix: str #: Prefix string
333 _major: int #: Major number part of the version number.
334 _minor: int #: Minor number part of the version number.
335 _micro: int #: Micro number part of the version number.
336 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
337 _releaseNumber: int #: Release number (Python calls this a serial).
338 _post: int #: Post-release version number part.
339 _dev: int #: Development number
340 _build: int #: Build number part of the version number.
341 _postfix: str #: Postfix string
342 _hash: str #: Hash from version control system.
343 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
345 def __init__(
346 self,
347 major: int,
348 minor: Nullable[int] = None,
349 micro: Nullable[int] = None,
350 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
351 number: Nullable[int] = None,
352 post: Nullable[int] = None,
353 dev: Nullable[int] = None,
354 *,
355 build: Nullable[int] = None,
356 postfix: Nullable[str] = None,
357 prefix: Nullable[str] = None,
358 hash: Nullable[str] = None,
359 flags: Flags = Flags.NoVCS
360 ) -> None:
361 """
362 Initializes a version number representation.
364 :param major: Major number part of the version number.
365 :param minor: Minor number part of the version number.
366 :param micro: Micro (patch) number part of the version number.
367 :param level: Release level (alpha, beta, release candidate, final, ...) of the version number.
368 :param number: Release number part (in combination with release level) of the version number.
369 :param post: Post number part of the version number.
370 :param dev: Development number part of the version number.
371 :param build: Build number part of the version number.
372 :param postfix: The version number's postfix.
373 :param prefix: The version number's prefix.
374 :param hash: Postfix string.
375 :param flags: The version number's flags.
376 :raises TypeError: If parameter 'major' is not of type int.
377 :raises ValueError: If parameter 'major' is a negative number.
378 :raises TypeError: If parameter 'minor' is not of type int.
379 :raises ValueError: If parameter 'minor' is a negative number.
380 :raises TypeError: If parameter 'micro' is not of type int.
381 :raises ValueError: If parameter 'micro' is a negative number.
382 :raises TypeError: If parameter 'build' is not of type int.
383 :raises ValueError: If parameter 'build' is a negative number.
384 :raises TypeError: If parameter 'prefix' is not of type str.
385 :raises TypeError: If parameter 'postfix' is not of type str.
386 """
387 self.__hash = None
389 if not isinstance(major, int):
390 raise TypeError("Parameter 'major' is not of type 'int'.")
391 elif major < 0:
392 raise ValueError("Parameter 'major' is negative.")
394 self._parts = Parts.Major
395 self._major = major
397 if minor is not None:
398 if not isinstance(minor, int):
399 raise TypeError("Parameter 'minor' is not of type 'int'.")
400 elif minor < 0:
401 raise ValueError("Parameter 'minor' is negative.")
403 self._parts |= Parts.Minor
404 self._minor = minor
405 else:
406 self._minor = 0
408 if micro is not None:
409 if not isinstance(micro, int):
410 raise TypeError("Parameter 'micro' is not of type 'int'.")
411 elif micro < 0:
412 raise ValueError("Parameter 'micro' is negative.")
414 self._parts |= Parts.Micro
415 self._micro = micro
416 else:
417 self._micro = 0
419 if level is None:
420 raise ValueError("Parameter 'level' is None.")
421 elif not isinstance(level, ReleaseLevel):
422 raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
423 elif level is ReleaseLevel.Final:
424 if number is not None:
425 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
427 self._parts |= Parts.Level
428 self._releaseLevel = level
429 self._releaseNumber = 0
430 else:
431 self._parts |= Parts.Level
432 self._releaseLevel = level
434 if number is not None:
435 if not isinstance(number, int):
436 raise TypeError("Parameter 'number' is not of type 'int'.")
437 elif number < 0:
438 raise ValueError("Parameter 'number' is negative.")
440 self._releaseNumber = number
441 else:
442 self._releaseNumber = 0
444 if dev is not None:
445 if not isinstance(dev, int):
446 raise TypeError("Parameter 'dev' is not of type 'int'.")
447 elif dev < 0:
448 raise ValueError("Parameter 'dev' is negative.")
450 self._parts |= Parts.Dev
451 self._dev = dev
452 else:
453 self._dev = 0
455 if post is not None:
456 if not isinstance(post, int):
457 raise TypeError("Parameter 'post' is not of type 'int'.")
458 elif post < 0:
459 raise ValueError("Parameter 'post' is negative.")
461 self._parts |= Parts.Post
462 self._post = post
463 else:
464 self._post = 0
466 if build is not None:
467 if not isinstance(build, int):
468 raise TypeError("Parameter 'build' is not of type 'int'.")
469 elif build < 0:
470 raise ValueError("Parameter 'build' is negative.")
472 self._build = build
473 self._parts |= Parts.Build
474 else:
475 self._build = 0
477 if postfix is not None:
478 if not isinstance(postfix, str):
479 raise TypeError("Parameter 'postfix' is not of type 'str'.")
481 self._parts |= Parts.Postfix
482 self._postfix = postfix
483 else:
484 self._postfix = ""
486 if prefix is not None:
487 if not isinstance(prefix, str):
488 raise TypeError("Parameter 'prefix' is not of type 'str'.")
490 self._parts |= Parts.Prefix
491 self._prefix = prefix
492 else:
493 self._prefix = ""
495 if hash is not None:
496 if not isinstance(hash, str):
497 raise TypeError("Parameter 'hash' is not of type 'str'.")
499 self._parts |= Parts.Hash
500 self._hash = hash
501 else:
502 self._hash = ""
504 if flags is None:
505 raise ValueError("Parameter 'flags' is None.")
506 elif not isinstance(flags, Flags):
507 raise TypeError("Parameter 'flags' is not of type 'Flags'.")
509 self._flags = flags
511 @classmethod
512 @abstractmethod
513 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version":
514 """Parse a version string and return a Version instance."""
516 @readonly
517 def Parts(self) -> Parts:
518 """
519 Read-only property to access the used parts of this version number.
521 :return: A flag enumeration of used version number parts.
522 """
523 return self._parts
525 @readonly
526 def Prefix(self) -> str:
527 """
528 Read-only property to access the version number's prefix.
530 :return: The prefix of the version number.
531 """
532 return self._prefix
534 @readonly
535 def Major(self) -> int:
536 """
537 Read-only property to access the major number.
539 :return: The major number.
540 """
541 return self._major
543 @readonly
544 def Minor(self) -> int:
545 """
546 Read-only property to access the minor number.
548 :return: The minor number.
549 """
550 return self._minor
552 @readonly
553 def Micro(self) -> int:
554 """
555 Read-only property to access the micro number.
557 :return: The micro number.
558 """
559 return self._micro
561 @readonly
562 def ReleaseLevel(self) -> ReleaseLevel:
563 """
564 Read-only property to access the release level.
566 :return: The release level.
567 """
568 return self._releaseLevel
570 @readonly
571 def ReleaseNumber(self) -> int:
572 """
573 Read-only property to access the release number.
575 :return: The release number.
576 """
577 return self._releaseNumber
579 @readonly
580 def Post(self) -> int:
581 """
582 Read-only property to access the post number.
584 :return: The post number.
585 """
586 return self._post
588 @readonly
589 def Dev(self) -> int:
590 """
591 Read-only property to access the development number.
593 :return: The development number.
594 """
595 return self._dev
597 @readonly
598 def Build(self) -> int:
599 """
600 Read-only property to access the build number.
602 :return: The build number.
603 """
604 return self._build
606 @readonly
607 def Postfix(self) -> str:
608 """
609 Read-only property to access the version number's postfix.
611 :return: The postfix of the version number.
612 """
613 return self._postfix
615 @readonly
616 def Hash(self) -> str:
617 """
618 Read-only property to access the version number's hash.
620 :return: The hash.
621 """
622 return self._hash
624 @readonly
625 def Flags(self) -> Flags:
626 """
627 Read-only property to access the version number's flags.
629 :return: The flags of the version number.
630 """
631 return self._flags
633 def _equal(self, left: "Version", right: "Version") -> Nullable[bool]:
634 """
635 Private helper method to compute the equality of two :class:`Version` instances.
637 :param left: Left operand.
638 :param right: Right operand.
639 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
640 """
641 return (
642 (left._major == right._major) and
643 (left._minor == right._minor) and
644 (left._micro == right._micro) and
645 (left._releaseLevel == right._releaseLevel) and
646 (left._releaseNumber == right._releaseNumber) and
647 (left._post == right._post) and
648 (left._dev == right._dev) and
649 (left._build == right._build) and
650 (left._postfix == right._postfix)
651 )
653 def _compare(self, left: "Version", right: "Version") -> Nullable[bool]:
654 """
655 Private helper method to compute the comparison of two :class:`Version` instances.
657 :param left: Left operand.
658 :param right: Right operand.
659 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
660 False if ``left`` is greater than ``right``. |br|
661 Otherwise it's None (both operands are equal).
662 """
663 if left._major < right._major:
664 return True
665 elif left._major > right._major:
666 return False
668 if left._minor < right._minor:
669 return True
670 elif left._minor > right._minor:
671 return False
673 if left._micro < right._micro:
674 return True
675 elif left._micro > right._micro:
676 return False
678 if left._releaseLevel < right._releaseLevel: 678 ↛ 679line 678 didn't jump to line 679 because the condition on line 678 was never true
679 return True
680 elif left._releaseLevel > right._releaseLevel: 680 ↛ 681line 680 didn't jump to line 681 because the condition on line 680 was never true
681 return False
683 if left._releaseNumber < right._releaseNumber: 683 ↛ 684line 683 didn't jump to line 684 because the condition on line 683 was never true
684 return True
685 elif left._releaseNumber > right._releaseNumber: 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true
686 return False
688 if left._post < right._post: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true
689 return True
690 elif left._post > right._post: 690 ↛ 691line 690 didn't jump to line 691 because the condition on line 690 was never true
691 return False
693 if left._dev < right._dev: 693 ↛ 694line 693 didn't jump to line 694 because the condition on line 693 was never true
694 return True
695 elif left._dev > right._dev: 695 ↛ 696line 695 didn't jump to line 696 because the condition on line 695 was never true
696 return False
698 if left._build < right._build: 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true
699 return True
700 elif left._build > right._build: 700 ↛ 701line 700 didn't jump to line 701 because the condition on line 700 was never true
701 return False
703 return None
705 def _minimum(self, actual: "Version", expected: "Version") -> Nullable[bool]:
706 exactMajor = Parts.Minor in expected._parts
707 exactMinor = Parts.Micro in expected._parts
709 if exactMajor and actual._major != expected._major: 709 ↛ 710line 709 didn't jump to line 710 because the condition on line 709 was never true
710 return False
711 elif not exactMajor and actual._major < expected._major:
712 return False
714 if exactMinor and actual._minor != expected._minor: 714 ↛ 715line 714 didn't jump to line 715 because the condition on line 714 was never true
715 return False
716 elif not exactMinor and actual._minor < expected._minor:
717 return False
719 if Parts.Micro in expected._parts:
720 return actual._micro >= expected._micro
722 return True
724 def _format(self, formatSpec: str) -> str:
725 """
726 Return a string representation of this version number according to the format specification.
728 .. topic:: Format Specifiers
730 * ``%p`` - prefix
731 * ``%M`` - major number
732 * ``%m`` - minor number
733 * ``%u`` - micro number
734 * ``%b`` - build number
736 :param formatSpec: The format specification.
737 :return: Formatted version number.
738 """
739 if formatSpec == "":
740 return self.__str__()
742 result = formatSpec
743 result = result.replace("%p", str(self._prefix))
744 result = result.replace("%M", str(self._major))
745 result = result.replace("%m", str(self._minor))
746 result = result.replace("%u", str(self._micro))
747 result = result.replace("%b", str(self._build))
748 result = result.replace("%r", str(self._releaseLevel)[0])
749 result = result.replace("%R", str(self._releaseLevel))
750 result = result.replace("%n", str(self._releaseNumber))
751 result = result.replace("%d", str(self._dev))
752 result = result.replace("%P", str(self._postfix))
754 return result
756 @mustoverride
757 def __eq__(self, other: Union["Version", str, int, None]) -> bool:
758 """
759 Compare two version numbers for equality.
761 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
762 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
763 number is assumed (all other parts are zero).
765 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
766 number.
768 :param other: Operand to compare against.
769 :returns: ``True``, if both version numbers are equal.
770 :raises ValueError: If parameter ``other`` is None.
771 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
772 """
773 if other is None:
774 raise ValueError(f"Second operand is None.")
775 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
776 pass
777 elif isinstance(other, str):
778 other = self.__class__.Parse(other)
779 elif isinstance(other, int):
780 other = self.__class__(major=other)
781 else:
782 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
783 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
784 raise ex
786 return self._equal(self, other)
788 @mustoverride
789 def __ne__(self, other: Union["Version", str, int, None]) -> bool:
790 """
791 Compare two version numbers for inequality.
793 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
794 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
795 number is assumed (all other parts are zero).
797 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
798 number.
800 :param other: Operand to compare against.
801 :returns: ``True``, if both version numbers are not equal.
802 :raises ValueError: If parameter ``other`` is None.
803 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
804 """
805 if other is None:
806 raise ValueError(f"Second operand is None.")
807 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
808 pass
809 elif isinstance(other, str):
810 other = self.__class__.Parse(other)
811 elif isinstance(other, int):
812 other = self.__class__(major=other)
813 else:
814 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
815 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
816 raise ex
818 return not self._equal(self, other)
820 @mustoverride
821 def __lt__(self, other: Union["Version", str, int, None]) -> bool:
822 """
823 Compare two version numbers if the version is less than the second operand.
825 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
826 ``str`` and ``int`` are accepted, too. |br|
827 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
828 number is assumed (all other parts are zero).
830 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
831 number.
833 :param other: Operand to compare against.
834 :returns: ``True``, if version is less than the second operand.
835 :raises ValueError: If parameter ``other`` is None.
836 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
837 """
838 if other is None:
839 raise ValueError(f"Second operand is None.")
840 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
841 pass
842 elif isinstance(other, VersionRange):
843 other = other._lowerBound
844 elif isinstance(other, VersionSet):
845 other = other._items[0]
846 elif isinstance(other, str):
847 other = self.__class__.Parse(other)
848 elif isinstance(other, int):
849 other = self.__class__(major=other)
850 else:
851 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
852 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
853 raise ex
855 return self._compare(self, other) is True
857 @mustoverride
858 def __le__(self, other: Union["Version", str, int, None]) -> bool:
859 """
860 Compare two version numbers if the version is less than or equal the second operand.
862 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
863 ``str`` and ``int`` are accepted, too. |br|
864 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
865 number is assumed (all other parts are zero).
867 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
868 number.
870 :param other: Operand to compare against.
871 :returns: ``True``, if version is less than or equal the second operand.
872 :raises ValueError: If parameter ``other`` is None.
873 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
874 """
875 equalValue = True
876 if other is None:
877 raise ValueError(f"Second operand is None.")
878 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
879 pass
880 elif isinstance(other, VersionRange):
881 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
882 other = other._lowerBound
883 elif isinstance(other, VersionSet):
884 other = other._items[0]
885 elif isinstance(other, str):
886 other = self.__class__.Parse(other)
887 elif isinstance(other, int):
888 other = self.__class__(major=other)
889 else:
890 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.")
891 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
892 raise ex
894 result = self._compare(self, other)
895 return result if result is not None else equalValue
897 @mustoverride
898 def __gt__(self, other: Union["Version", str, int, None]) -> bool:
899 """
900 Compare two version numbers if the version is greater than the second operand.
902 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
903 ``str`` and ``int`` are accepted, too. |br|
904 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
905 number is assumed (all other parts are zero).
907 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
908 number.
910 :param other: Operand to compare against.
911 :returns: ``True``, if version is greater than the second operand.
912 :raises ValueError: If parameter ``other`` is None.
913 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
914 """
915 if other is None:
916 raise ValueError(f"Second operand is None.")
917 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
918 pass
919 elif isinstance(other, VersionRange):
920 other = other._upperBound
921 elif isinstance(other, VersionSet):
922 other = other._items[-1]
923 elif isinstance(other, str):
924 other = self.__class__.Parse(other)
925 elif isinstance(other, int):
926 other = self.__class__(major=other)
927 else:
928 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
929 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
930 raise ex
932 return self._compare(self, other) is False
934 @mustoverride
935 def __ge__(self, other: Union["Version", str, int, None]) -> bool:
936 """
937 Compare two version numbers if the version is greater than or equal the second operand.
939 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
940 ``str`` and ``int`` are accepted, too. |br|
941 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
942 number is assumed (all other parts are zero).
944 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
945 number.
947 :param other: Operand to compare against.
948 :returns: ``True``, if version is greater than or equal the second operand.
949 :raises ValueError: If parameter ``other`` is None.
950 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
951 """
952 equalValue = True
953 if other is None:
954 raise ValueError(f"Second operand is None.")
955 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
956 pass
957 elif isinstance(other, VersionRange):
958 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
959 other = other._upperBound
960 elif isinstance(other, VersionSet):
961 other = other._items[-1]
962 elif isinstance(other, str):
963 other = self.__class__.Parse(other)
964 elif isinstance(other, int):
965 other = self.__class__(major=other)
966 else:
967 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
968 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
969 raise ex
971 result = self._compare(self, other)
972 return not result if result is not None else equalValue
974 def __rshift__(self, other: Union["Version", str, int, None]) -> bool:
975 if other is None:
976 raise ValueError(f"Second operand is None.")
977 elif isinstance(other, self.__class__):
978 pass
979 elif isinstance(other, str):
980 other = self.__class__.Parse(other)
981 elif isinstance(other, int):
982 other = self.__class__(major=other)
983 else:
984 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >> operator.")
985 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
986 raise ex
988 return self._minimum(self, other)
990 def __hash__(self) -> int:
991 if self.__hash is None:
992 self.__hash = hash((
993 self._prefix,
994 self._major,
995 self._minor,
996 self._micro,
997 self._releaseLevel,
998 self._releaseNumber,
999 self._post,
1000 self._dev,
1001 self._build,
1002 self._postfix,
1003 self._hash,
1004 self._flags
1005 ))
1006 return self.__hash
1009@export
1010class SemanticVersion(Version):
1011 """Representation of a semantic version number like ``3.7.12``."""
1013 _PATTERN = re_compile(
1014 r"^"
1015 r"(?P<prefix>[a-zA-Z]*)"
1016 r"(?P<major>\d+)"
1017 r"(?:\.(?P<minor>\d+))?"
1018 r"(?:\.(?P<micro>\d+))?"
1019 r"(?:"
1020 r"(?:\.(?P<build>\d+))"
1021 r"|"
1022 r"(?:[-](?P<release>dev|final))"
1023 r"|"
1024 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
1025 r")?"
1026 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1027 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1028 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1029 r"$"
1030 )
1031# QUESTION: was this how many commits a version is ahead of the last tagged version?
1032# ahead: int = 0
1034 def __init__(
1035 self,
1036 major: int,
1037 minor: Nullable[int] = None,
1038 micro: Nullable[int] = None,
1039 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1040 number: Nullable[int] = None,
1041 post: Nullable[int] = None,
1042 dev: Nullable[int] = None,
1043 *,
1044 build: Nullable[int] = None,
1045 postfix: Nullable[str] = None,
1046 prefix: Nullable[str] = None,
1047 hash: Nullable[str] = None,
1048 flags: Flags = Flags.NoVCS
1049 ) -> None:
1050 """
1051 Initializes a semantic version number representation.
1053 :param major: Major number part of the version number.
1054 :param minor: Minor number part of the version number.
1055 :param micro: Micro (patch) number part of the version number.
1056 :param build: Build number part of the version number.
1057 :param level: tbd
1058 :param number: tbd
1059 :param post: Post number part of the version number.
1060 :param dev: Development number part of the version number.
1061 :param prefix: The version number's prefix.
1062 :param postfix: The version number's postfix.
1063 :param flags: The version number's flags.
1064 :param hash: tbd
1065 :raises TypeError: If parameter 'major' is not of type int.
1066 :raises ValueError: If parameter 'major' is a negative number.
1067 :raises TypeError: If parameter 'minor' is not of type int.
1068 :raises ValueError: If parameter 'minor' is a negative number.
1069 :raises TypeError: If parameter 'micro' is not of type int.
1070 :raises ValueError: If parameter 'micro' is a negative number.
1071 :raises TypeError: If parameter 'build' is not of type int.
1072 :raises ValueError: If parameter 'build' is a negative number.
1073 :raises TypeError: If parameter 'post' is not of type int.
1074 :raises ValueError: If parameter 'post' is a negative number.
1075 :raises TypeError: If parameter 'dev' is not of type int.
1076 :raises ValueError: If parameter 'dev' is a negative number.
1077 :raises TypeError: If parameter 'prefix' is not of type str.
1078 :raises TypeError: If parameter 'postfix' is not of type str.
1079 """
1080 super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags)
1082 @classmethod
1083 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "SemanticVersion":
1084 """
1085 Parse a version string and return a :class:`SemanticVersion` instance.
1087 Allowed prefix characters:
1089 * ``v|V`` - version, public version, public release
1090 * ``i|I`` - internal version, internal release
1091 * ``r|R`` - release, revision
1092 * ``rev|REV`` - revision
1094 :param versionString: The version string to parse.
1095 :param validator: Optional, a validation function.
1096 :returns: An object representing a semantic version.
1097 :raises TypeError: When parameter ``versionString`` is not a string.
1098 :raises ValueError: When parameter ``versionString`` is None.
1099 :raises ValueError: When parameter ``versionString`` is empty.
1100 """
1101 if versionString is None:
1102 raise ValueError("Parameter 'versionString' is None.")
1103 elif not isinstance(versionString, str):
1104 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1105 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1106 raise ex
1107 elif (versionString := versionString.strip()) == "":
1108 raise ValueError("Parameter 'versionString' is empty.")
1110 if (match := cls._PATTERN.match(versionString)) is None:
1111 raise ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1113 def toInt(value: Nullable[str]) -> Nullable[int]:
1114 if value is None or value == "":
1115 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 )
1162 if validator is not None and not validator(version): 1162 ↛ 1164line 1162 didn't jump to line 1164 because the condition on line 1162 was never true
1163 # TODO: VersionValidatorException
1164 raise ValueError(f"Failed to validate version string '{versionString}'.")
1166 return version
1168 @readonly
1169 def Patch(self) -> int:
1170 """
1171 Read-only property to access the patch number.
1173 The patch number is identical to the micro number.
1175 :return: The patch number.
1176 """
1177 return self._micro
1179 def _equal(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1180 """
1181 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1183 :param left: Left operand.
1184 :param right: Right operand.
1185 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1186 """
1187 return super()._equal(left, right)
1189 def _compare(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1190 """
1191 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1193 :param left: Left operand.
1194 :param right: Right operand.
1195 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1196 False if ``left`` is greater than ``right``. |br|
1197 Otherwise it's None (both operands are equal).
1198 """
1199 return super()._compare(left, right)
1201 def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1202 """
1203 Compare two version numbers for equality.
1205 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1206 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1207 number is assumed (all other parts are zero).
1209 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1210 number.
1212 :param other: Operand to compare against.
1213 :returns: ``True``, if both version numbers are equal.
1214 :raises ValueError: If parameter ``other`` is None.
1215 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1216 """
1217 return super().__eq__(other)
1219 def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1220 """
1221 Compare two version numbers for inequality.
1223 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1224 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1225 number is assumed (all other parts are zero).
1227 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1228 number.
1230 :param other: Operand to compare against.
1231 :returns: ``True``, if both version numbers are not equal.
1232 :raises ValueError: If parameter ``other`` is None.
1233 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1234 """
1235 return super().__ne__(other)
1237 def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1238 """
1239 Compare two version numbers if the version is less than the second operand.
1241 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1242 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1243 number is assumed (all other parts are zero).
1245 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1246 number.
1248 :param other: Operand to compare against.
1249 :returns: ``True``, if version is less than the second operand.
1250 :raises ValueError: If parameter ``other`` is None.
1251 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1252 """
1253 return super().__lt__(other)
1255 def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1256 """
1257 Compare two version numbers if the version is less than or equal the second operand.
1259 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1260 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1261 number is assumed (all other parts are zero).
1263 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1264 number.
1266 :param other: Operand to compare against.
1267 :returns: ``True``, if version is less than or equal the second operand.
1268 :raises ValueError: If parameter ``other`` is None.
1269 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1270 """
1271 return super().__le__(other)
1273 def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1274 """
1275 Compare two version numbers if the version is greater than the second operand.
1277 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1278 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1279 number is assumed (all other parts are zero).
1281 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1282 number.
1284 :param other: Operand to compare against.
1285 :returns: ``True``, if version is greater than the second operand.
1286 :raises ValueError: If parameter ``other`` is None.
1287 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1288 """
1289 return super().__gt__(other)
1291 def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1292 """
1293 Compare two version numbers if the version is greater than or equal the second operand.
1295 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1296 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1297 number is assumed (all other parts are zero).
1299 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1300 number.
1302 :param other: Operand to compare against.
1303 :returns: ``True``, if version is greater than or equal the second operand.
1304 :raises ValueError: If parameter ``other`` is None.
1305 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1306 """
1307 return super().__ge__(other)
1309 def __rshift__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1310 return super().__rshift__(other)
1312 def __hash__(self) -> int:
1313 return super().__hash__()
1315 def __format__(self, formatSpec: str) -> str:
1316 result = self._format(formatSpec)
1318 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1319 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1321 return result.replace("%%", "%")
1323 def __repr__(self) -> str:
1324 """
1325 Return a string representation of this version number without prefix ``v``.
1327 :returns: Raw version number representation without a prefix.
1328 """
1329 return f"{self._prefix if Parts.Prefix in self._parts else ''}{self._major}.{self._minor}.{self._micro}"
1331 def __str__(self) -> str:
1332 """
1333 Return a string representation of this version number.
1335 :returns: Version number representation.
1336 """
1337 result = self._prefix if Parts.Prefix in self._parts else ""
1338 result += f"{self._major}" # major is always present
1339 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1340 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1341 result += f".{self._build}" if Parts.Build in self._parts else ""
1342 if self._releaseLevel is ReleaseLevel.Development:
1343 result += "-dev"
1344 elif self._releaseLevel is ReleaseLevel.Alpha:
1345 result += f".alpha{self._releaseNumber}"
1346 elif self._releaseLevel is ReleaseLevel.Beta:
1347 result += f".beta{self._releaseNumber}"
1348 elif self._releaseLevel is ReleaseLevel.Gamma: 1348 ↛ 1349line 1348 didn't jump to line 1349 because the condition on line 1348 was never true
1349 result += f".gamma{self._releaseNumber}"
1350 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1351 result += f".rc{self._releaseNumber}"
1352 result += f".post{self._post}" if Parts.Post in self._parts else ""
1353 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1354 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1356 return result
1359@export
1360class PythonVersion(SemanticVersion):
1361 """
1362 Represents a Python version.
1363 """
1365 @classmethod
1366 def FromSysVersionInfo(cls) -> "PythonVersion":
1367 """
1368 Create a Python version from :data:`sys.version_info`.
1370 :returns: A PythonVersion instance of the current Python interpreter's version.
1371 """
1372 from sys import version_info
1374 if version_info.releaselevel == "final":
1375 rl = ReleaseLevel.Final
1376 number = None
1377 else: # pragma: no cover
1378 number = version_info.serial
1380 if version_info.releaselevel == "alpha":
1381 rl = ReleaseLevel.Alpha
1382 elif version_info.releaselevel == "beta":
1383 rl = ReleaseLevel.Beta
1384 elif version_info.releaselevel == "candidate":
1385 rl = ReleaseLevel.ReleaseCandidate
1386 else: # pragma: no cover
1387 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1389 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1391 def __hash__(self) -> int:
1392 return super().__hash__()
1394 def __str__(self) -> str:
1395 """
1396 Return a string representation of this version number.
1398 :returns: Version number representation.
1399 """
1400 result = self._prefix if Parts.Prefix in self._parts else ""
1401 result += f"{self._major}" # major is always present
1402 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1403 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1404 if self._releaseLevel is ReleaseLevel.Alpha: 1404 ↛ 1405line 1404 didn't jump to line 1405 because the condition on line 1404 was never true
1405 result += f"a{self._releaseNumber}"
1406 elif self._releaseLevel is ReleaseLevel.Beta: 1406 ↛ 1407line 1406 didn't jump to line 1407 because the condition on line 1406 was never true
1407 result += f"b{self._releaseNumber}"
1408 elif self._releaseLevel is ReleaseLevel.Gamma: 1408 ↛ 1409line 1408 didn't jump to line 1409 because the condition on line 1408 was never true
1409 result += f"c{self._releaseNumber}"
1410 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: 1410 ↛ 1411line 1410 didn't jump to line 1411 because the condition on line 1410 was never true
1411 result += f"rc{self._releaseNumber}"
1412 result += f".post{self._post}" if Parts.Post in self._parts else ""
1413 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1414 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1416 return result
1419@export
1420class CalendarVersion(Version):
1421 """Representation of a calendar version number like ``2021.10``."""
1423 def __init__(
1424 self,
1425 major: int,
1426 minor: Nullable[int] = None,
1427 micro: Nullable[int] = None,
1428 build: Nullable[int] = None,
1429 flags: Flags = Flags.Clean,
1430 prefix: Nullable[str] = None,
1431 postfix: Nullable[str] = None
1432 ) -> None:
1433 """
1434 Initializes a calendar version number representation.
1436 :param major: Major number part of the version number.
1437 :param minor: Minor number part of the version number.
1438 :param micro: Micro (patch) number part of the version number.
1439 :param build: Build number part of the version number.
1440 :param flags: The version number's flags.
1441 :param prefix: The version number's prefix.
1442 :param postfix: The version number's postfix.
1443 :raises TypeError: If parameter 'major' is not of type int.
1444 :raises ValueError: If parameter 'major' is a negative number.
1445 :raises TypeError: If parameter 'minor' is not of type int.
1446 :raises ValueError: If parameter 'minor' is a negative number.
1447 :raises TypeError: If parameter 'micro' is not of type int.
1448 :raises ValueError: If parameter 'micro' is a negative number.
1449 :raises TypeError: If parameter 'build' is not of type int.
1450 :raises ValueError: If parameter 'build' is a negative number.
1451 :raises TypeError: If parameter 'prefix' is not of type str.
1452 :raises TypeError: If parameter 'postfix' is not of type str.
1453 """
1454 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1456 @classmethod
1457 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion":
1458 """
1459 Parse a version string and return a :class:`CalendarVersion` instance.
1461 :param versionString: The version string to parse.
1462 :returns: An object representing a calendar version.
1463 :raises TypeError: If parameter ``other`` is not a string.
1464 :raises ValueError: If parameter ``other`` is None.
1465 :raises ValueError: If parameter ``other`` is empty.
1466 """
1467 parts = Parts.Unknown
1469 if versionString is None:
1470 raise ValueError("Parameter 'versionString' is None.")
1471 elif not isinstance(versionString, str):
1472 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1473 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1474 raise ex
1475 elif versionString == "":
1476 raise ValueError("Parameter 'versionString' is empty.")
1478 split = versionString.split(".")
1479 length = len(split)
1480 major = int(split[0])
1481 minor = 0
1482 parts |= Parts.Major
1484 if length >= 2:
1485 minor = int(split[1])
1486 parts |= Parts.Minor
1488 flags = Flags.Clean
1490 version = cls(major, minor, flags=flags)
1491 if validator is not None and not validator(version):
1492 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1494 return version
1496 @property
1497 def Year(self) -> int:
1498 """
1499 Read-only property to access the year part.
1501 :return: The year part.
1502 """
1503 return self._major
1505 def _equal(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1506 """
1507 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1509 :param left: Left parameter.
1510 :param right: Right parameter.
1511 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1512 """
1513 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1515 def _compare(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1516 """
1517 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1519 :param left: Left parameter.
1520 :param right: Right parameter.
1521 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1522 False if ``left`` is greater than ``right``. |br|
1523 Otherwise it's None (both parameters are equal).
1524 """
1525 if left._major < right._major:
1526 return True
1527 elif left._major > right._major:
1528 return False
1530 if left._minor < right._minor:
1531 return True
1532 elif left._minor > right._minor:
1533 return False
1535 if left._micro < right._micro: 1535 ↛ 1536line 1535 didn't jump to line 1536 because the condition on line 1535 was never true
1536 return True
1537 elif left._micro > right._micro: 1537 ↛ 1538line 1537 didn't jump to line 1538 because the condition on line 1537 was never true
1538 return False
1540 return None
1542 def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1543 """
1544 Compare two version numbers for equality.
1546 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1547 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1548 number is assumed (all other parts are zero).
1550 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1551 number.
1553 :param other: Parameter to compare against.
1554 :returns: ``True``, if both version numbers are equal.
1555 :raises ValueError: If parameter ``other`` is None.
1556 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1557 """
1558 return super().__eq__(other)
1560 def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1561 """
1562 Compare two version numbers for inequality.
1564 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1565 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1566 number is assumed (all other parts are zero).
1568 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1569 number.
1571 :param other: Parameter to compare against.
1572 :returns: ``True``, if both version numbers are not equal.
1573 :raises ValueError: If parameter ``other`` is None.
1574 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1575 """
1576 return super().__ne__(other)
1578 def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1579 """
1580 Compare two version numbers if the version is less than the second operand.
1582 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1583 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1584 number is assumed (all other parts are zero).
1586 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1587 number.
1589 :param other: Parameter to compare against.
1590 :returns: ``True``, if version is less than the second operand.
1591 :raises ValueError: If parameter ``other`` is None.
1592 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1593 """
1594 return super().__lt__(other)
1596 def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1597 """
1598 Compare two version numbers if the version is less than or equal the second operand.
1600 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1601 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1602 number is assumed (all other parts are zero).
1604 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1605 number.
1607 :param other: Parameter to compare against.
1608 :returns: ``True``, if version is less than or equal the second operand.
1609 :raises ValueError: If parameter ``other`` is None.
1610 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1611 """
1612 return super().__le__(other)
1614 def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1615 """
1616 Compare two version numbers if the version is greater than the second operand.
1618 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1619 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1620 number is assumed (all other parts are zero).
1622 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1623 number.
1625 :param other: Parameter to compare against.
1626 :returns: ``True``, if version is greater than the second operand.
1627 :raises ValueError: If parameter ``other`` is None.
1628 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1629 """
1630 return super().__gt__(other)
1632 def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1633 """
1634 Compare two version numbers if the version is greater than or equal the second operand.
1636 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1637 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1638 number is assumed (all other parts are zero).
1640 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1641 number.
1643 :param other: Parameter to compare against.
1644 :returns: ``True``, if version is greater than or equal the second operand.
1645 :raises ValueError: If parameter ``other`` is None.
1646 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1647 """
1648 return super().__ge__(other)
1650 def __hash__(self) -> int:
1651 return super().__hash__()
1653 def __format__(self, formatSpec: str) -> str:
1654 """
1655 Return a string representation of this version number according to the format specification.
1657 .. topic:: Format Specifiers
1659 * ``%M`` - major number (year)
1660 * ``%m`` - minor number (month/week)
1662 :param formatSpec: The format specification.
1663 :return: Formatted version number.
1664 """
1665 if formatSpec == "":
1666 return self.__str__()
1668 result = formatSpec
1669 # result = result.replace("%P", str(self._prefix))
1670 result = result.replace("%M", str(self._major))
1671 result = result.replace("%m", str(self._minor))
1672 # result = result.replace("%p", str(self._pre))
1674 return result.replace("%%", "%")
1676 def __repr__(self) -> str:
1677 """
1678 Return a string representation of this version number without prefix ``v``.
1680 :returns: Raw version number representation without a prefix.
1681 """
1682 return f"{self._major}.{self._minor}"
1684 def __str__(self) -> str:
1685 """
1686 Return a string representation of this version number with prefix ``v``.
1688 :returns: Version number representation including a prefix.
1689 """
1690 result = f"{self._major}"
1691 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1693 return result
1696@export
1697class YearMonthVersion(CalendarVersion):
1698 """Representation of a calendar version number made of year and month like ``2021.10``."""
1700 def __init__(
1701 self,
1702 year: int,
1703 month: Nullable[int] = None,
1704 build: Nullable[int] = None,
1705 flags: Flags = Flags.Clean,
1706 prefix: Nullable[str] = None,
1707 postfix: Nullable[str] = None
1708 ) -> None:
1709 """
1710 Initializes a year-month version number representation.
1712 :param year: Year part of the version number.
1713 :param month: Month part of the version number.
1714 :param build: Build number part of the version number.
1715 :param flags: The version number's flags.
1716 :param prefix: The version number's prefix.
1717 :param postfix: The version number's postfix.
1718 :raises TypeError: If parameter 'major' is not of type int.
1719 :raises ValueError: If parameter 'major' is a negative number.
1720 :raises TypeError: If parameter 'minor' is not of type int.
1721 :raises ValueError: If parameter 'minor' is a negative number.
1722 :raises TypeError: If parameter 'micro' is not of type int.
1723 :raises ValueError: If parameter 'micro' is a negative number.
1724 :raises TypeError: If parameter 'build' is not of type int.
1725 :raises ValueError: If parameter 'build' is a negative number.
1726 :raises TypeError: If parameter 'prefix' is not of type str.
1727 :raises TypeError: If parameter 'postfix' is not of type str.
1728 """
1729 super().__init__(year, month, 0, build, flags, prefix, postfix)
1731 @property
1732 def Month(self) -> int:
1733 """
1734 Read-only property to access the month part.
1736 :return: The month part.
1737 """
1738 return self._minor
1740 def __hash__(self) -> int:
1741 return super().__hash__()
1744@export
1745class YearWeekVersion(CalendarVersion):
1746 """Representation of a calendar version number made of year and week like ``2021.47``."""
1748 def __init__(
1749 self,
1750 year: int,
1751 week: Nullable[int] = None,
1752 build: Nullable[int] = None,
1753 flags: Flags = Flags.Clean,
1754 prefix: Nullable[str] = None,
1755 postfix: Nullable[str] = None
1756 ) -> None:
1757 """
1758 Initializes a year-week version number representation.
1760 :param year: Year part of the version number.
1761 :param week: Week part of the version number.
1762 :param build: Build number part of the version number.
1763 :param flags: The version number's flags.
1764 :param prefix: The version number's prefix.
1765 :param postfix: The version number's postfix.
1766 :raises TypeError: If parameter 'major' is not of type int.
1767 :raises ValueError: If parameter 'major' is a negative number.
1768 :raises TypeError: If parameter 'minor' is not of type int.
1769 :raises ValueError: If parameter 'minor' is a negative number.
1770 :raises TypeError: If parameter 'micro' is not of type int.
1771 :raises ValueError: If parameter 'micro' is a negative number.
1772 :raises TypeError: If parameter 'build' is not of type int.
1773 :raises ValueError: If parameter 'build' is a negative number.
1774 :raises TypeError: If parameter 'prefix' is not of type str.
1775 :raises TypeError: If parameter 'postfix' is not of type str.
1776 """
1777 super().__init__(year, week, 0, build, flags, prefix, postfix)
1779 @property
1780 def Week(self) -> int:
1781 """
1782 Read-only property to access the week part.
1784 :return: The week part.
1785 """
1786 return self._minor
1788 def __hash__(self) -> int:
1789 return super().__hash__()
1792@export
1793class YearReleaseVersion(CalendarVersion):
1794 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
1796 def __init__(
1797 self,
1798 year: int,
1799 release: Nullable[int] = None,
1800 build: Nullable[int] = None,
1801 flags: Flags = Flags.Clean,
1802 prefix: Nullable[str] = None,
1803 postfix: Nullable[str] = None
1804 ) -> None:
1805 """
1806 Initializes a year-release version number representation.
1808 :param year: Year part of the version number.
1809 :param release: Release number of the version number.
1810 :param build: Build number part of the version number.
1811 :param flags: The version number's flags.
1812 :param prefix: The version number's prefix.
1813 :param postfix: The version number's postfix.
1814 :raises TypeError: If parameter 'major' is not of type int.
1815 :raises ValueError: If parameter 'major' is a negative number.
1816 :raises TypeError: If parameter 'minor' is not of type int.
1817 :raises ValueError: If parameter 'minor' is a negative number.
1818 :raises TypeError: If parameter 'micro' is not of type int.
1819 :raises ValueError: If parameter 'micro' is a negative number.
1820 :raises TypeError: If parameter 'build' is not of type int.
1821 :raises ValueError: If parameter 'build' is a negative number.
1822 :raises TypeError: If parameter 'prefix' is not of type str.
1823 :raises TypeError: If parameter 'postfix' is not of type str.
1824 """
1825 super().__init__(year, release, 0, build, flags, prefix, postfix)
1827 @property
1828 def Release(self) -> int:
1829 """
1830 Read-only property to access the release number.
1832 :return: The release number.
1833 """
1834 return self._minor
1836 def __hash__(self) -> int:
1837 return super().__hash__()
1840@export
1841class YearMonthDayVersion(CalendarVersion):
1842 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
1844 def __init__(
1845 self,
1846 year: int,
1847 month: Nullable[int] = None,
1848 day: Nullable[int] = None,
1849 build: Nullable[int] = None,
1850 flags: Flags = Flags.Clean,
1851 prefix: Nullable[str] = None,
1852 postfix: Nullable[str] = None
1853 ) -> None:
1854 """
1855 Initializes a year-month-day version number representation.
1857 :param year: Year part of the version number.
1858 :param month: Month part of the version number.
1859 :param day: Day part of the version number.
1860 :param build: Build number part of the version number.
1861 :param flags: The version number's flags.
1862 :param prefix: The version number's prefix.
1863 :param postfix: The version number's postfix.
1864 :raises TypeError: If parameter 'major' is not of type int.
1865 :raises ValueError: If parameter 'major' is a negative number.
1866 :raises TypeError: If parameter 'minor' is not of type int.
1867 :raises ValueError: If parameter 'minor' is a negative number.
1868 :raises TypeError: If parameter 'micro' is not of type int.
1869 :raises ValueError: If parameter 'micro' is a negative number.
1870 :raises TypeError: If parameter 'build' is not of type int.
1871 :raises ValueError: If parameter 'build' is a negative number.
1872 :raises TypeError: If parameter 'prefix' is not of type str.
1873 :raises TypeError: If parameter 'postfix' is not of type str.
1874 """
1875 super().__init__(year, month, day, build, flags, prefix, postfix)
1877 @property
1878 def Month(self) -> int:
1879 """
1880 Read-only property to access the month part.
1882 :return: The month part.
1883 """
1884 return self._minor
1886 @property
1887 def Day(self) -> int:
1888 """
1889 Read-only property to access the day part.
1891 :return: The day part.
1892 """
1893 return self._micro
1895 def __hash__(self) -> int:
1896 return super().__hash__()
1899V = TypeVar("V", bound=Version)
1901@export
1902class RangeBoundHandling(Flag):
1903 """
1904 A flag defining how to handle bounds in a range.
1906 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
1907 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
1908 """
1909 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
1910 LowerBoundInclusive = 0 #: Lower bound is inclusive.
1911 UpperBoundInclusive = 0 #: Upper bound is inclusive.
1912 LowerBoundExclusive = 1 #: Lower bound is exclusive.
1913 UpperBoundExclusive = 2 #: Upper bound is exclusive.
1914 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
1917@export
1918class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
1919 """
1920 Representation of a version range described by a lower bound and upper bound version.
1922 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
1923 """
1924 _lowerBound: V
1925 _upperBound: V
1926 _boundHandling: RangeBoundHandling
1928 def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None:
1929 """
1930 Initializes a version range described by a lower and upper bound.
1932 :param lowerBound: lowest version (inclusive).
1933 :param upperBound: hightest version (inclusive).
1934 :raises TypeError: If parameter ``lowerBound`` is not of type :class:`Version`.
1935 :raises TypeError: If parameter ``upperBound`` is not of type :class:`Version`.
1936 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
1937 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
1938 """
1939 if not isinstance(lowerBound, Version):
1940 ex = TypeError(f"Parameter 'lowerBound' is not of type 'Version'.")
1941 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
1942 raise ex
1944 if not isinstance(upperBound, Version):
1945 ex = TypeError(f"Parameter 'upperBound' is not of type 'Version'.")
1946 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
1947 raise ex
1949 if not ((lBC := lowerBound.__class__) is (uBC := upperBound.__class__) or issubclass(lBC, uBC) or issubclass(uBC, lBC)):
1950 ex = TypeError(f"Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
1951 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and type '{getFullyQualifiedName(upperBound)}' for upperBound.")
1952 raise ex
1954 if not (lowerBound <= upperBound):
1955 ex = ValueError(f"Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
1956 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
1957 raise ex
1959 self._lowerBound = lowerBound
1960 self._upperBound = upperBound
1961 self._boundHandling = boundHandling
1963 @property
1964 def LowerBound(self) -> V:
1965 """
1966 Property to access the range's lower bound.
1968 :return: Lower bound of the version range.
1969 """
1970 return self._lowerBound
1972 @LowerBound.setter
1973 def LowerBound(self, value: V) -> None:
1974 if not isinstance(value, Version):
1975 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
1976 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
1977 raise ex
1979 self._lowerBound = value
1981 @readonly
1982 def UpperBound(self) -> V:
1983 """
1984 Property to access the range's upper bound.
1986 :return: Upper bound of the version range.
1987 """
1988 return self._upperBound
1990 @UpperBound.setter
1991 def UpperBound(self, value: V) -> None:
1992 if not isinstance(value, Version):
1993 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
1994 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
1995 raise ex
1997 self._upperBound = value
1999 @readonly
2000 def BoundHandling(self) -> RangeBoundHandling:
2001 """
2002 Property to access the range's bound handling strategy.
2004 :return: The range's bound handling strategy.
2005 """
2006 return self._boundHandling
2008 @BoundHandling.setter
2009 def BoundHandling(self, value: RangeBoundHandling) -> None:
2010 if not isinstance(value, RangeBoundHandling):
2011 ex = TypeError(f"Parameter 'value' is not of type 'RangeBoundHandling'.")
2012 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2013 raise ex
2015 self._boundHandling = value
2017 def __and__(self, other: Any) -> "VersionRange[T]":
2018 """
2019 Compute the intersection of two version ranges.
2021 :param other: Second version range to intersect with.
2022 :returns: Intersected version range.
2023 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2024 :raises ValueError: If intersection is empty.
2025 """
2026 if not isinstance(other, VersionRange): 2026 ↛ 2027line 2026 didn't jump to line 2027 because the condition on line 2026 was never true
2027 ex = TypeError(f"Parameter 'other' is not of type 'VersionRange'.")
2028 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2029 raise ex
2031 if not (isinstance(other._lowerBound, self._lowerBound.__class__) and isinstance(self._lowerBound, other._lowerBound.__class__)): 2031 ↛ 2032line 2031 didn't jump to line 2032 because the condition on line 2031 was never true
2032 ex = TypeError(f"Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible with each other.")
2033 ex.add_note(
2034 f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2035 raise ex
2037 if other._lowerBound < self._lowerBound:
2038 lBound = self._lowerBound
2039 elif other._lowerBound in self: 2039 ↛ 2042line 2039 didn't jump to line 2042 because the condition on line 2039 was always true
2040 lBound = other._lowerBound
2041 else:
2042 raise ValueError()
2044 if other._upperBound > self._upperBound:
2045 uBound = self._upperBound
2046 elif other._upperBound in self: 2046 ↛ 2049line 2046 didn't jump to line 2049 because the condition on line 2046 was always true
2047 uBound = other._upperBound
2048 else:
2049 raise ValueError()
2051 return self.__class__(lBound, uBound)
2053 def __lt__(self, other: Any) -> bool:
2054 """
2055 Compare a version range and a version numbers if the version range is less than the second operand (version).
2057 :param other: Operand to compare against.
2058 :returns: ``True``, if version range is less than the second operand (version).
2059 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2060 """
2061 # TODO: support VersionRange < VersionRange too
2062 # TODO: support str, int, ... like Version ?
2063 if not isinstance(other, Version):
2064 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2065 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2066 raise ex
2068 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2068 ↛ 2069line 2068 didn't jump to line 2069 because the condition on line 2068 was never true
2069 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2070 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2071 raise ex
2073 return self._upperBound < other
2075 def __le__(self, other: Any) -> bool:
2076 """
2077 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2079 :param other: Operand to compare against.
2080 :returns: ``True``, if version range is less than or equal the second operand (version).
2081 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2082 """
2083 # TODO: support VersionRange < VersionRange too
2084 # TODO: support str, int, ... like Version ?
2085 if not isinstance(other, Version): 2085 ↛ 2086line 2085 didn't jump to line 2086 because the condition on line 2085 was never true
2086 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2087 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2088 raise ex
2090 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2090 ↛ 2091line 2090 didn't jump to line 2091 because the condition on line 2090 was never true
2091 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2092 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2093 raise ex
2095 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2096 return self._upperBound < other
2097 else:
2098 return self._upperBound <= other
2100 def __gt__(self, other: Any) -> bool:
2101 """
2102 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2104 :param other: Operand to compare against.
2105 :returns: ``True``, if version range is greater than the second operand (version).
2106 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2107 """
2108 # TODO: support VersionRange < VersionRange too
2109 # TODO: support str, int, ... like Version ?
2110 if not isinstance(other, Version): 2110 ↛ 2111line 2110 didn't jump to line 2111 because the condition on line 2110 was never true
2111 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2112 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2113 raise ex
2115 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2115 ↛ 2116line 2115 didn't jump to line 2116 because the condition on line 2115 was never true
2116 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2117 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2118 raise ex
2120 return self._lowerBound > other
2122 def __ge__(self, other: Any) -> bool:
2123 """
2124 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2126 :param other: Operand to compare against.
2127 :returns: ``True``, if version range is greater than or equal the second operand (version).
2128 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2129 """
2130 # TODO: support VersionRange < VersionRange too
2131 # TODO: support str, int, ... like Version ?
2132 if not isinstance(other, Version): 2132 ↛ 2133line 2132 didn't jump to line 2133 because the condition on line 2132 was never true
2133 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2134 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2135 raise ex
2137 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2137 ↛ 2138line 2137 didn't jump to line 2138 because the condition on line 2137 was never true
2138 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2139 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2140 raise ex
2142 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2142 ↛ 2143line 2142 didn't jump to line 2143 because the condition on line 2142 was never true
2143 return self._lowerBound > other
2144 else:
2145 return self._lowerBound >= other
2147 def __contains__(self, version: Version) -> bool:
2148 """
2149 Check if the version is in the version range.
2151 :param version: Version to check.
2152 :returns: ``True``, if version is in range.
2153 :raises TypeError: If parameter ``version`` is not of type :class:`Version`.
2154 """
2155 if not isinstance(version, Version): 2155 ↛ 2156line 2155 didn't jump to line 2156 because the condition on line 2155 was never true
2156 ex = TypeError(f"Parameter 'item' is not of type 'Version'.")
2157 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2158 raise ex
2160 if self._boundHandling is RangeBoundHandling.BothBoundsInclusive: 2160 ↛ 2162line 2160 didn't jump to line 2162 because the condition on line 2160 was always true
2161 return self._lowerBound <= version <= self._upperBound
2162 elif self._boundHandling is (RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive):
2163 return self._lowerBound <= version < self._upperBound
2164 elif self._boundHandling is (RangeBoundHandling.LowerBoundExclusive | RangeBoundHandling.UpperBoundInclusive):
2165 return self._lowerBound < version <= self._upperBound
2166 else:
2167 return self._lowerBound < version < self._upperBound
2170@export
2171class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2172 """
2173 Representation of an ordered set of versions.
2175 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2176 """
2177 _items: List[V] #: An ordered list of set members.
2179 def __init__(self, versions: Union[Version, Iterable[V]]) -> None:
2180 """
2181 Initializes a version set either by a single version or an iterable of versions.
2183 :param versions: A single version or an iterable of versions.
2184 :raises ValueError: If parameter ``versions`` is None`.
2185 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2186 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2187 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2188 """
2189 if versions is None:
2190 raise ValueError(f"Parameter 'versions' is None.")
2192 if isinstance(versions, Version):
2193 self._items = [versions]
2194 elif isinstance(versions, abc_Iterable): 2194 ↛ 2212line 2194 didn't jump to line 2212 because the condition on line 2194 was always true
2195 iterator = iter(versions)
2196 try:
2197 firstVersion = next(iterator)
2198 except StopIteration:
2199 self._items = []
2200 return
2202 if not isinstance(firstVersion, Version): 2202 ↛ 2203line 2202 didn't jump to line 2203 because the condition on line 2202 was never true
2203 raise TypeError(f"First element in parameter 'versions' is not of type Version.")
2205 baseType = firstVersion.__class__
2206 for version in iterator:
2207 if not isinstance(version, baseType):
2208 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2210 self._items = list(sorted(versions))
2211 else:
2212 raise TypeError(f"Parameter 'versions' is not an Iterable.")
2214 def __and__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2215 """
2216 Compute intersection of two version sets.
2218 :param other: Second set of versions.
2219 :returns: Intersection of two version sets.
2220 """
2221 selfIterator = self.__iter__()
2222 otherIterator = other.__iter__()
2224 result = []
2225 try:
2226 selfValue = next(selfIterator)
2227 otherValue = next(otherIterator)
2229 while True:
2230 if selfValue < otherValue:
2231 selfValue = next(selfIterator)
2232 elif otherValue < selfValue:
2233 otherValue = next(otherIterator)
2234 else:
2235 result.append(selfValue)
2236 selfValue = next(selfIterator)
2237 otherValue = next(otherIterator)
2239 except StopIteration:
2240 pass
2242 return VersionSet(result)
2244 def __or__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2245 """
2246 Compute union of two version sets.
2248 :param other: Second set of versions.
2249 :returns: Union of two version sets.
2250 """
2251 selfIterator = self.__iter__()
2252 otherIterator = other.__iter__()
2254 result = []
2255 try:
2256 selfValue = next(selfIterator)
2257 except StopIteration:
2258 for otherValue in otherIterator:
2259 result.append(otherValue)
2261 try:
2262 otherValue = next(otherIterator)
2263 except StopIteration:
2264 for selfValue in selfIterator:
2265 result.append(selfValue)
2267 while True:
2268 if selfValue < otherValue:
2269 result.append(selfValue)
2270 try:
2271 selfValue = next(selfIterator)
2272 except StopIteration:
2273 result.append(otherValue)
2274 for otherValue in otherIterator: 2274 ↛ 2275line 2274 didn't jump to line 2275 because the loop on line 2274 never started
2275 result.append(otherValue)
2277 break
2278 elif otherValue < selfValue:
2279 result.append(otherValue)
2280 try:
2281 otherValue = next(otherIterator)
2282 except StopIteration:
2283 result.append(selfValue)
2284 for selfValue in selfIterator:
2285 result.append(selfValue)
2287 break
2288 else:
2289 result.append(selfValue)
2290 try:
2291 selfValue = next(selfIterator)
2292 except StopIteration:
2293 for otherValue in otherIterator: 2293 ↛ 2294line 2293 didn't jump to line 2294 because the loop on line 2293 never started
2294 result.append(otherValue)
2296 break
2298 try:
2299 otherValue = next(otherIterator)
2300 except StopIteration:
2301 for selfValue in selfIterator:
2302 result.append(selfValue)
2304 break
2306 return VersionSet(result)
2308 def __lt__(self, other: Any) -> bool:
2309 """
2310 Compare a version set and a version numbers if the version set is less than the second operand (version).
2312 :param other: Operand to compare against.
2313 :returns: ``True``, if version set is less than the second operand (version).
2314 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2315 """
2316 # TODO: support VersionRange < VersionRange too
2317 # TODO: support str, int, ... like Version ?
2318 if not isinstance(other, Version):
2319 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2320 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2321 raise ex
2323 return self._items[-1] < other
2325 def __le__(self, other: Any) -> bool:
2326 """
2327 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
2329 :param other: Operand to compare against.
2330 :returns: ``True``, if version set is less than or equal the second operand (version).
2331 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2332 """
2333 # TODO: support VersionRange < VersionRange too
2334 # TODO: support str, int, ... like Version ?
2335 if not isinstance(other, Version): 2335 ↛ 2336line 2335 didn't jump to line 2336 because the condition on line 2335 was never true
2336 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2337 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2338 raise ex
2340 return self._items[-1] <= other
2342 def __gt__(self, other: Any) -> bool:
2343 """
2344 Compare a version set and a version numbers if the version set is greater than the second operand (version).
2346 :param other: Operand to compare against.
2347 :returns: ``True``, if version set is greater than the second operand (version).
2348 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2349 """
2350 # TODO: support VersionRange < VersionRange too
2351 # TODO: support str, int, ... like Version ?
2352 if not isinstance(other, Version): 2352 ↛ 2353line 2352 didn't jump to line 2353 because the condition on line 2352 was never true
2353 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2354 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2355 raise ex
2357 return self._items[0] > other
2359 def __ge__(self, other: Any) -> bool:
2360 """
2361 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
2363 :param other: Operand to compare against.
2364 :returns: ``True``, if version set is greater than or equal the second operand (version).
2365 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2366 """
2367 # TODO: support VersionRange < VersionRange too
2368 # TODO: support str, int, ... like Version ?
2369 if not isinstance(other, Version): 2369 ↛ 2370line 2369 didn't jump to line 2370 because the condition on line 2369 was never true
2370 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2371 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2372 raise ex
2374 return self._items[0] >= other
2376 def __contains__(self, version: V) -> bool:
2377 """
2378 Checks if the version a member of the set.
2380 :param version: The version to check.
2381 :returns: ``True``, if the version is a member of the set.
2382 """
2383 return version in self._items
2385 def __len__(self) -> int:
2386 """
2387 Returns the number of members in the set.
2389 :returns: Number of set members.
2390 """
2391 return len(self._items)
2393 def __iter__(self) -> Iterator[V]:
2394 """
2395 Returns an iterator to iterate all versions of this set from lowest to highest.
2397 :returns: Iterator to iterate versions.
2398 """
2399 return self._items.__iter__()
2401 def __getitem__(self, index: int) -> V:
2402 """
2403 Access to a version of a set by index.
2405 :param index: The index of the version to access.
2406 :returns: The indexed version.
2408 .. hint::
2410 Versions are ordered from lowest to highest version number.
2411 """
2412 return self._items[index]