Coverage for pyTooling/Versioning/__init__.py: 84%
920 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-12 20:40 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-12 20:40 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2025 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Implementation of semantic and date versioning version-numbers.
34.. hint:: See :ref:`high-level help <VERSIONING>` for explanations and usage examples.
35"""
36from collections.abc import Iterable as abc_Iterable
37from enum import Flag, Enum
38from re import compile as re_compile
39from sys import version_info # needed for versions before Python 3.11
40from typing import Optional as Nullable, Union, Callable, Any, Generic, TypeVar, Tuple, Iterable, Iterator, List
42try:
43 from pyTooling.Decorators import export, readonly
44 from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
45 from pyTooling.Exceptions import ToolingException
46 from pyTooling.Common import getFullyQualifiedName
47except (ImportError, ModuleNotFoundError): # pragma: no cover
48 print("[pyTooling.Versioning] Could not import from 'pyTooling.*'!")
50 try:
51 from Decorators import export, readonly
52 from MetaClasses import ExtendedType, abstractmethod, mustoverride
53 from Exceptions import ToolingException
54 from Common import getFullyQualifiedName
55 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover
56 print("[pyTooling.Versioning] Could not import directly!")
57 raise ex
60@export
61class Parts(Flag):
62 """Enumeration describing parts of a version number that can be present."""
63 Unknown = 0 #: Undocumented
64 Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``).
65 Year = 1 #: Year is present. (e.g. X in ``XXXX.10``).
66 Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
67 Month = 2 #: Month is present. (e.g. X in ``2024.YY``).
68 Week = 2 #: Week is present. (e.g. X in ``2024.YY``).
69 Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
70 Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
71 Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``).
72 Level = 8 #: Release level is present.
73 Dev = 16 #: Development part is present.
74 Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
75 Post = 64 #: Post-release number is present.
76 Prefix = 128 #: Prefix is present.
77 Postfix = 256 #: Postfix is present.
78 Hash = 512 #: Hash is present.
79# AHead = 256
82@export
83class ReleaseLevel(Enum):
84 """Enumeration describing the version's maturity level."""
85 Final = 0 #:
86 ReleaseCandidate = -10 #:
87 Development = -20 #:
88 Gamma = -30 #:
89 Beta = -40 #:
90 Alpha = -50 #:
92 def __eq__(self, other: Any):
93 """
94 Compare two release levels if the level is equal to the second operand.
96 :param other: Operand to compare against.
97 :returns: ``True``, if release level is equal the second operand's release level.
98 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
99 """
100 if isinstance(other, str): 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 other = ReleaseLevel(other)
103 if not isinstance(other, ReleaseLevel): 103 ↛ 104line 103 didn't jump to line 104 because the condition on line 103 was never true
104 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
105 if version_info >= (3, 11): # pragma: no cover
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):
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 if version_info >= (3, 11): # pragma: no cover
125 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
126 raise ex
128 return self is not other
130 def __lt__(self, other: Any):
131 """
132 Compare two release levels if the level is less than the second operand.
134 :param other: Operand to compare against.
135 :returns: ``True``, if release level is less than the second operand.
136 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
137 """
138 if isinstance(other, str): 138 ↛ 139line 138 didn't jump to line 139 because the condition on line 138 was never true
139 other = ReleaseLevel(other)
141 if not isinstance(other, ReleaseLevel): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true
142 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
143 if version_info >= (3, 11): # pragma: no cover
144 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
145 raise ex
147 return self.value < other.value
149 def __le__(self, other: Any):
150 """
151 Compare two release levels if the level is less than or equal the second operand.
153 :param other: Operand to compare against.
154 :returns: ``True``, if release level is less than or equal the second operand.
155 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
156 """
157 if isinstance(other, str):
158 other = ReleaseLevel(other)
160 if not isinstance(other, ReleaseLevel):
161 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <=>= operator.")
162 if version_info >= (3, 11): # pragma: no cover
163 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
164 raise ex
166 return self.value <= other.value
168 def __gt__(self, other: Any):
169 """
170 Compare two release levels if the level is greater than the second operand.
172 :param other: Operand to compare against.
173 :returns: ``True``, if release level is greater than the second operand.
174 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
175 """
176 if isinstance(other, str): 176 ↛ 177line 176 didn't jump to line 177 because the condition on line 176 was never true
177 other = ReleaseLevel(other)
179 if not isinstance(other, ReleaseLevel): 179 ↛ 180line 179 didn't jump to line 180 because the condition on line 179 was never true
180 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
181 if version_info >= (3, 11): # pragma: no cover
182 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
183 raise ex
185 return self.value > other.value
187 def __ge__(self, other: Any):
188 """
189 Compare two release levels if the level is greater than or equal the second operand.
191 :param other: Operand to compare against.
192 :returns: ``True``, if release level is greater than or equal the second operand.
193 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
194 """
195 if isinstance(other, str):
196 other = ReleaseLevel(other)
198 if not isinstance(other, ReleaseLevel):
199 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
200 if version_info >= (3, 11): # pragma: no cover
201 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
202 raise ex
204 return self.value >= other.value
206 def __hash__(self) -> int:
207 return hash(self.value)
209 def __str__(self) -> str:
210 """
211 Returns the release level's string equivalent.
213 :returns: The string equivalent of the release level.
214 """
215 if self is ReleaseLevel.Final:
216 return "final"
217 elif self is ReleaseLevel.ReleaseCandidate:
218 return "rc"
219 elif self is ReleaseLevel.Development: 219 ↛ 220line 219 didn't jump to line 220 because the condition on line 219 was never true
220 return "dev"
221 elif self is ReleaseLevel.Beta: 221 ↛ 222line 221 didn't jump to line 222 because the condition on line 221 was never true
222 return "beta"
223 elif self is ReleaseLevel.Alpha: 223 ↛ 226line 223 didn't jump to line 226 because the condition on line 223 was always true
224 return "alpha"
226 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
229@export
230class Flags(Flag):
231 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
232 NoVCS = 0 #: No Version Control System VCS
233 Clean = 1 #: A versioned build was created from a *clean* working directory.
234 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
236 CVS = 16 #: Concurrent Versions System (CVS)
237 SVN = 32 #: Subversion (SVN)
238 Git = 64 #: Git
239 Hg = 128 #: Mercurial (Hg)
242@export
243def WordSizeValidator(
244 bits: Nullable[int] = None,
245 majorBits: Nullable[int] = None,
246 minorBits: Nullable[int] = None,
247 microBits: Nullable[int] = None,
248 buildBits: Nullable[int] = None
249):
250 """
251 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
253 :param bits: Number of bits to encode any positive version number part.
254 :param majorBits: Number of bits to encode a positive major number in a version.
255 :param minorBits: Number of bits to encode a positive minor number in a version.
256 :param microBits: Number of bits to encode a positive micro number in a version.
257 :param buildBits: Number of bits to encode a positive build number in a version.
258 :return: A validation function for Version instances.
259 """
260 majorMax = minorMax = microMax = buildMax = -1
261 if bits is not None:
262 majorMax = minorMax = microMax = buildMax = 2**bits - 1
264 if majorBits is not None:
265 majorMax = 2**majorBits - 1
266 if minorBits is not None:
267 minorMax = 2**minorBits - 1
268 if microBits is not None:
269 microMax = 2 ** microBits - 1
270 if buildBits is not None: 270 ↛ 271line 270 didn't jump to line 271 because the condition on line 270 was never true
271 buildMax = 2**buildBits - 1
273 def validator(version: SemanticVersion) -> bool:
274 if Parts.Major in version._parts and version._major > majorMax:
275 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
277 if Parts.Minor in version._parts and version._minor > minorMax:
278 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
280 if Parts.Micro in version._parts and version._micro > microMax:
281 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
283 if Parts.Build in version._parts and version._build > buildMax: 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true
284 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
286 return True
288 return validator
291@export
292def MaxValueValidator(
293 max: Nullable[int] = None,
294 majorMax: Nullable[int] = None,
295 minorMax: Nullable[int] = None,
296 microMax: Nullable[int] = None,
297 buildMax: Nullable[int] = None
298):
299 """
300 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
302 :param max: The upper bound for any positive version number part.
303 :param majorMax: The upper bound for the positive major number.
304 :param minorMax: The upper bound for the positive minor number.
305 :param microMax: The upper bound for the positive micro number.
306 :param buildMax: The upper bound for the positive build number.
307 :return: A validation function for Version instances.
308 """
309 if max is not None: 309 ↛ 312line 309 didn't jump to line 312 because the condition on line 309 was always true
310 majorMax = minorMax = microMax = buildMax = max
312 def validator(version: SemanticVersion) -> bool:
313 if Parts.Major in version._parts and version._major > majorMax:
314 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
316 if Parts.Minor in version._parts and version._minor > minorMax:
317 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
319 if Parts.Micro in version._parts and version._micro > microMax:
320 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
322 if Parts.Build in version._parts and version._build > buildMax: 322 ↛ 323line 322 didn't jump to line 323 because the condition on line 322 was never true
323 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
325 return True
327 return validator
330@export
331class Version(metaclass=ExtendedType, slots=True):
332 """Base-class for a version representation."""
334 __hash: Nullable[int] #: once computed hash of the object
336 _parts: Parts #: Integer flag enumeration of present parts in a version number.
337 _prefix: str #: Prefix string
338 _major: int #: Major number part of the version number.
339 _minor: int #: Minor number part of the version number.
340 _micro: int #: Micro number part of the version number.
341 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
342 _releaseNumber: int #: Release number (Python calls this a serial).
343 _post: int #: Post-release version number part.
344 _dev: int #: Development number
345 _build: int #: Build number part of the version number.
346 _postfix: str #: Postfix string
347 _hash: str #: Hash from version control system.
348 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
350 def __init__(
351 self,
352 major: int,
353 minor: Nullable[int] = None,
354 micro: Nullable[int] = None,
355 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
356 number: Nullable[int] = None,
357 post: Nullable[int] = None,
358 dev: Nullable[int] = None,
359 *,
360 build: Nullable[int] = None,
361 postfix: Nullable[str] = None,
362 prefix: Nullable[str] = None,
363 hash: Nullable[str] = None,
364 flags: Flags = Flags.NoVCS
365 ) -> None:
366 """
367 Initializes a version number representation.
369 :param major: Major number part of the version number.
370 :param minor: Minor number part of the version number.
371 :param micro: Micro (patch) number part of the version number.
372 :param level: Release level (alpha, beta, release candidate, final, ...) of the version number.
373 :param number: Release number part (in combination with release level) of the version number.
374 :param post: Post number part of the version number.
375 :param dev: Development number part of the version number.
376 :param build: Build number part of the version number.
377 :param postfix: The version number's postfix.
378 :param prefix: The version number's prefix.
379 :param hash: Postfix string.
380 :param flags: The version number's flags.
381 :raises TypeError: If parameter 'major' is not of type int.
382 :raises ValueError: If parameter 'major' is a negative number.
383 :raises TypeError: If parameter 'minor' is not of type int.
384 :raises ValueError: If parameter 'minor' is a negative number.
385 :raises TypeError: If parameter 'micro' is not of type int.
386 :raises ValueError: If parameter 'micro' is a negative number.
387 :raises TypeError: If parameter 'build' is not of type int.
388 :raises ValueError: If parameter 'build' is a negative number.
389 :raises TypeError: If parameter 'prefix' is not of type str.
390 :raises TypeError: If parameter 'postfix' is not of type str.
391 """
392 self.__hash = None
394 if not isinstance(major, int):
395 raise TypeError("Parameter 'major' is not of type 'int'.")
396 elif major < 0:
397 raise ValueError("Parameter 'major' is negative.")
399 self._parts = Parts.Major
400 self._major = major
402 if minor is not None:
403 if not isinstance(minor, int):
404 raise TypeError("Parameter 'minor' is not of type 'int'.")
405 elif minor < 0:
406 raise ValueError("Parameter 'minor' is negative.")
408 self._parts |= Parts.Minor
409 self._minor = minor
410 else:
411 self._minor = 0
413 if micro is not None:
414 if not isinstance(micro, int):
415 raise TypeError("Parameter 'micro' is not of type 'int'.")
416 elif micro < 0:
417 raise ValueError("Parameter 'micro' is negative.")
419 self._parts |= Parts.Micro
420 self._micro = micro
421 else:
422 self._micro = 0
424 if level is None:
425 raise ValueError("Parameter 'level' is None.")
426 elif not isinstance(level, ReleaseLevel):
427 raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
428 elif level is ReleaseLevel.Final:
429 if number is not None:
430 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
432 self._parts |= Parts.Level
433 self._releaseLevel = level
434 self._releaseNumber = 0
435 else:
436 self._parts |= Parts.Level
437 self._releaseLevel = level
439 if number is not None:
440 if not isinstance(number, int):
441 raise TypeError("Parameter 'number' is not of type 'int'.")
442 elif number < 0:
443 raise ValueError("Parameter 'number' is negative.")
445 self._releaseNumber = number
446 else:
447 self._releaseNumber = 0
449 if dev is not None:
450 if not isinstance(dev, int):
451 raise TypeError("Parameter 'dev' is not of type 'int'.")
452 elif dev < 0:
453 raise ValueError("Parameter 'dev' is negative.")
455 self._parts |= Parts.Dev
456 self._dev = dev
457 else:
458 self._dev = 0
460 if post is not None:
461 if not isinstance(post, int):
462 raise TypeError("Parameter 'post' is not of type 'int'.")
463 elif post < 0:
464 raise ValueError("Parameter 'post' is negative.")
466 self._parts |= Parts.Post
467 self._post = post
468 else:
469 self._post = 0
471 if build is not None:
472 if not isinstance(build, int):
473 raise TypeError("Parameter 'build' is not of type 'int'.")
474 elif build < 0:
475 raise ValueError("Parameter 'build' is negative.")
477 self._build = build
478 self._parts |= Parts.Build
479 else:
480 self._build = 0
482 if postfix is not None:
483 if not isinstance(postfix, str):
484 raise TypeError("Parameter 'postfix' is not of type 'str'.")
486 self._parts |= Parts.Postfix
487 self._postfix = postfix
488 else:
489 self._postfix = ""
491 if prefix is not None:
492 if not isinstance(prefix, str):
493 raise TypeError("Parameter 'prefix' is not of type 'str'.")
495 self._parts |= Parts.Prefix
496 self._prefix = prefix
497 else:
498 self._prefix = ""
500 if hash is not None:
501 if not isinstance(hash, str):
502 raise TypeError("Parameter 'hash' is not of type 'str'.")
504 self._parts |= Parts.Hash
505 self._hash = hash
506 else:
507 self._hash = ""
509 if flags is None:
510 raise ValueError("Parameter 'flags' is None.")
511 elif not isinstance(flags, Flags):
512 raise TypeError("Parameter 'flags' is not of type 'Flags'.")
514 self._flags = flags
516 @classmethod
517 @abstractmethod
518 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version":
519 """Parse a version string and return a Version instance."""
521 @readonly
522 def Parts(self) -> Parts:
523 """
524 Read-only property to access the used parts of this version number.
526 :return: A flag enumeration of used version number parts.
527 """
528 return self._parts
530 @readonly
531 def Prefix(self) -> str:
532 """
533 Read-only property to access the version number's prefix.
535 :return: The prefix of the version number.
536 """
537 return self._prefix
539 @readonly
540 def Major(self) -> int:
541 """
542 Read-only property to access the major number.
544 :return: The major number.
545 """
546 return self._major
548 @readonly
549 def Minor(self) -> int:
550 """
551 Read-only property to access the minor number.
553 :return: The minor number.
554 """
555 return self._minor
557 @readonly
558 def Micro(self) -> int:
559 """
560 Read-only property to access the micro number.
562 :return: The micro number.
563 """
564 return self._micro
566 @readonly
567 def ReleaseLevel(self) -> ReleaseLevel:
568 """
569 Read-only property to access the release level.
571 :return: The release level.
572 """
573 return self._releaseLevel
575 @readonly
576 def ReleaseNumber(self) -> int:
577 """
578 Read-only property to access the release number.
580 :return: The release number.
581 """
582 return self._releaseNumber
584 @readonly
585 def Post(self) -> int:
586 """
587 Read-only property to access the post number.
589 :return: The post number.
590 """
591 return self._post
593 @readonly
594 def Dev(self) -> int:
595 """
596 Read-only property to access the development number.
598 :return: The development number.
599 """
600 return self._dev
602 @readonly
603 def Build(self) -> int:
604 """
605 Read-only property to access the build number.
607 :return: The build number.
608 """
609 return self._build
611 @readonly
612 def Postfix(self) -> str:
613 """
614 Read-only property to access the version number's postfix.
616 :return: The postfix of the version number.
617 """
618 return self._postfix
620 @readonly
621 def Hash(self) -> str:
622 """
623 Read-only property to access the version number's hash.
625 :return: The hash.
626 """
627 return self._hash
629 @readonly
630 def Flags(self) -> Flags:
631 """
632 Read-only property to access the version number's flags.
634 :return: The flags of the version number.
635 """
636 return self._flags
638 def _equal(self, left: "Version", right: "Version") -> Nullable[bool]:
639 """
640 Private helper method to compute the equality of two :class:`Version` instances.
642 :param left: Left operand.
643 :param right: Right operand.
644 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
645 """
646 return (
647 (left._major == right._major) and
648 (left._minor == right._minor) and
649 (left._micro == right._micro) and
650 (left._releaseLevel == right._releaseLevel) and
651 (left._releaseNumber == right._releaseNumber) and
652 (left._post == right._post) and
653 (left._dev == right._dev) and
654 (left._build == right._build) and
655 (left._postfix == right._postfix)
656 )
658 def _compare(self, left: "Version", right: "Version") -> Nullable[bool]:
659 """
660 Private helper method to compute the comparison of two :class:`Version` instances.
662 :param left: Left operand.
663 :param right: Right operand.
664 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
665 False if ``left`` is greater than ``right``. |br|
666 Otherwise it's None (both operands are equal).
667 """
668 if left._major < right._major:
669 return True
670 elif left._major > right._major:
671 return False
673 if left._minor < right._minor:
674 return True
675 elif left._minor > right._minor:
676 return False
678 if left._micro < right._micro:
679 return True
680 elif left._micro > right._micro:
681 return False
683 if left._releaseLevel < right._releaseLevel: 683 ↛ 684line 683 didn't jump to line 684 because the condition on line 683 was never true
684 return True
685 elif left._releaseLevel > right._releaseLevel: 685 ↛ 686line 685 didn't jump to line 686 because the condition on line 685 was never true
686 return False
688 if left._releaseNumber < right._releaseNumber: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true
689 return True
690 elif left._releaseNumber > right._releaseNumber: 690 ↛ 691line 690 didn't jump to line 691 because the condition on line 690 was never true
691 return False
693 if left._post < right._post: 693 ↛ 694line 693 didn't jump to line 694 because the condition on line 693 was never true
694 return True
695 elif left._post > right._post: 695 ↛ 696line 695 didn't jump to line 696 because the condition on line 695 was never true
696 return False
698 if left._dev < right._dev: 698 ↛ 699line 698 didn't jump to line 699 because the condition on line 698 was never true
699 return True
700 elif left._dev > right._dev: 700 ↛ 701line 700 didn't jump to line 701 because the condition on line 700 was never true
701 return False
703 if left._build < right._build: 703 ↛ 704line 703 didn't jump to line 704 because the condition on line 703 was never true
704 return True
705 elif left._build > right._build: 705 ↛ 706line 705 didn't jump to line 706 because the condition on line 705 was never true
706 return False
708 return None
710 def _minimum(self, actual: "Version", expected: "Version") -> Nullable[bool]:
711 exactMajor = Parts.Minor in expected._parts
712 exactMinor = Parts.Micro in expected._parts
714 if exactMajor and actual._major != expected._major: 714 ↛ 715line 714 didn't jump to line 715 because the condition on line 714 was never true
715 return False
716 elif not exactMajor and actual._major < expected._major:
717 return False
719 if exactMinor and actual._minor != expected._minor: 719 ↛ 720line 719 didn't jump to line 720 because the condition on line 719 was never true
720 return False
721 elif not exactMinor and actual._minor < expected._minor:
722 return False
724 if Parts.Micro in expected._parts:
725 return actual._micro >= expected._micro
727 return True
729 def _format(self, formatSpec: str) -> str:
730 """
731 Return a string representation of this version number according to the format specification.
733 .. topic:: Format Specifiers
735 * ``%p`` - prefix
736 * ``%M`` - major number
737 * ``%m`` - minor number
738 * ``%u`` - micro number
739 * ``%b`` - build number
741 :param formatSpec: The format specification.
742 :return: Formatted version number.
743 """
744 if formatSpec == "":
745 return self.__str__()
747 result = formatSpec
748 result = result.replace("%p", str(self._prefix))
749 result = result.replace("%M", str(self._major))
750 result = result.replace("%m", str(self._minor))
751 result = result.replace("%u", str(self._micro))
752 result = result.replace("%b", str(self._build))
753 result = result.replace("%r", str(self._releaseLevel)[0])
754 result = result.replace("%R", str(self._releaseLevel))
755 result = result.replace("%n", str(self._releaseNumber))
756 result = result.replace("%d", str(self._dev))
757 result = result.replace("%P", str(self._postfix))
759 return result
761 @mustoverride
762 def __eq__(self, other: Union["Version", str, int, None]) -> bool:
763 """
764 Compare two version numbers for equality.
766 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
767 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
768 number is assumed (all other parts are zero).
770 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
771 number.
773 :param other: Operand to compare against.
774 :returns: ``True``, if both version numbers are equal.
775 :raises ValueError: If parameter ``other`` is None.
776 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
777 """
778 if other is None:
779 raise ValueError(f"Second operand is None.")
780 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
781 pass
782 elif isinstance(other, str):
783 other = self.__class__.Parse(other)
784 elif isinstance(other, int):
785 other = self.__class__(major=other)
786 else:
787 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
788 if version_info >= (3, 11): # pragma: no cover
789 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
790 raise ex
792 return self._equal(self, other)
794 @mustoverride
795 def __ne__(self, other: Union["Version", str, int, None]) -> bool:
796 """
797 Compare two version numbers for inequality.
799 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
800 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
801 number is assumed (all other parts are zero).
803 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
804 number.
806 :param other: Operand to compare against.
807 :returns: ``True``, if both version numbers are not equal.
808 :raises ValueError: If parameter ``other`` is None.
809 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
810 """
811 if other is None:
812 raise ValueError(f"Second operand is None.")
813 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
814 pass
815 elif isinstance(other, str):
816 other = self.__class__.Parse(other)
817 elif isinstance(other, int):
818 other = self.__class__(major=other)
819 else:
820 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
821 if version_info >= (3, 11): # pragma: no cover
822 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
823 raise ex
825 return not self._equal(self, other)
827 @mustoverride
828 def __lt__(self, other: Union["Version", str, int, None]) -> bool:
829 """
830 Compare two version numbers if the version is less than the second operand.
832 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
833 ``str`` and ``int`` are accepted, too. |br|
834 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
835 number is assumed (all other parts are zero).
837 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
838 number.
840 :param other: Operand to compare against.
841 :returns: ``True``, if version is less than the second operand.
842 :raises ValueError: If parameter ``other`` is None.
843 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
844 """
845 if other is None:
846 raise ValueError(f"Second operand is None.")
847 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
848 pass
849 elif isinstance(other, VersionRange):
850 other = other._lowerBound
851 elif isinstance(other, VersionSet):
852 other = other._items[0]
853 elif isinstance(other, str):
854 other = self.__class__.Parse(other)
855 elif isinstance(other, int):
856 other = self.__class__(major=other)
857 else:
858 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
859 if version_info >= (3, 11): # pragma: no cover
860 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
861 raise ex
863 return self._compare(self, other) is True
865 @mustoverride
866 def __le__(self, other: Union["Version", str, int, None]) -> bool:
867 """
868 Compare two version numbers if the version is less than or equal the second operand.
870 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
871 ``str`` and ``int`` are accepted, too. |br|
872 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
873 number is assumed (all other parts are zero).
875 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
876 number.
878 :param other: Operand to compare against.
879 :returns: ``True``, if version is less than or equal the second operand.
880 :raises ValueError: If parameter ``other`` is None.
881 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
882 """
883 equalValue = True
884 if other is None:
885 raise ValueError(f"Second operand is None.")
886 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
887 pass
888 elif isinstance(other, VersionRange):
889 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
890 other = other._lowerBound
891 elif isinstance(other, VersionSet):
892 other = other._items[0]
893 elif isinstance(other, str):
894 other = self.__class__.Parse(other)
895 elif isinstance(other, int):
896 other = self.__class__(major=other)
897 else:
898 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.")
899 if version_info >= (3, 11): # pragma: no cover
900 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
901 raise ex
903 result = self._compare(self, other)
904 return result if result is not None else equalValue
906 @mustoverride
907 def __gt__(self, other: Union["Version", str, int, None]) -> bool:
908 """
909 Compare two version numbers if the version is greater than the second operand.
911 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
912 ``str`` and ``int`` are accepted, too. |br|
913 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
914 number is assumed (all other parts are zero).
916 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
917 number.
919 :param other: Operand to compare against.
920 :returns: ``True``, if version is greater than the second operand.
921 :raises ValueError: If parameter ``other`` is None.
922 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
923 """
924 if other is None:
925 raise ValueError(f"Second operand is None.")
926 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
927 pass
928 elif isinstance(other, VersionRange):
929 other = other._upperBound
930 elif isinstance(other, VersionSet):
931 other = other._items[-1]
932 elif isinstance(other, str):
933 other = self.__class__.Parse(other)
934 elif isinstance(other, int):
935 other = self.__class__(major=other)
936 else:
937 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
938 if version_info >= (3, 11): # pragma: no cover
939 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
940 raise ex
942 return self._compare(self, other) is False
944 @mustoverride
945 def __ge__(self, other: Union["Version", str, int, None]) -> bool:
946 """
947 Compare two version numbers if the version is greater than or equal the second operand.
949 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
950 ``str`` and ``int`` are accepted, too. |br|
951 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
952 number is assumed (all other parts are zero).
954 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
955 number.
957 :param other: Operand to compare against.
958 :returns: ``True``, if version is greater than or equal the second operand.
959 :raises ValueError: If parameter ``other`` is None.
960 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
961 """
962 equalValue = True
963 if other is None:
964 raise ValueError(f"Second operand is None.")
965 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
966 pass
967 elif isinstance(other, VersionRange):
968 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
969 other = other._upperBound
970 elif isinstance(other, VersionSet):
971 other = other._items[-1]
972 elif isinstance(other, str):
973 other = self.__class__.Parse(other)
974 elif isinstance(other, int):
975 other = self.__class__(major=other)
976 else:
977 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
978 if version_info >= (3, 11): # pragma: no cover
979 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
980 raise ex
982 result = self._compare(self, other)
983 return not result if result is not None else equalValue
985 def __rshift__(self, other: Union["Version", str, int, None]) -> bool:
986 if other is None:
987 raise ValueError(f"Second operand is None.")
988 elif isinstance(other, self.__class__):
989 pass
990 elif isinstance(other, str):
991 other = self.__class__.Parse(other)
992 elif isinstance(other, int):
993 other = self.__class__(major=other)
994 else:
995 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by %= operator.")
996 if version_info >= (3, 11): # pragma: no cover
997 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
998 raise ex
1000 return self._minimum(self, other)
1002 def __hash__(self) -> int:
1003 if self.__hash is None: 1003 ↛ 1018line 1003 didn't jump to line 1018 because the condition on line 1003 was always true
1004 self.__hash = hash((
1005 self._prefix,
1006 self._major,
1007 self._minor,
1008 self._micro,
1009 self._releaseLevel,
1010 self._releaseNumber,
1011 self._post,
1012 self._dev,
1013 self._build,
1014 self._postfix,
1015 self._hash,
1016 self._flags
1017 ))
1018 return self.__hash
1021@export
1022class SemanticVersion(Version):
1023 """Representation of a semantic version number like ``3.7.12``."""
1025 _PATTERN = re_compile(
1026 r"^"
1027 r"(?P<prefix>[a-zA-Z]*)"
1028 r"(?P<major>\d+)"
1029 r"(?:\.(?P<minor>\d+))?"
1030 r"(?:\.(?P<micro>\d+))?"
1031 r"(?:"
1032 r"(?:\.(?P<build>\d+))"
1033 r"|"
1034 r"(?:[-](?P<release>dev|final))"
1035 r"|"
1036 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
1037 r")?"
1038 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1039 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1040 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1041 r"$"
1042 )
1043# QUESTION: was this how many commits a version is ahead of the last tagged version?
1044# ahead: int = 0
1046 def __init__(
1047 self,
1048 major: int,
1049 minor: Nullable[int] = None,
1050 micro: Nullable[int] = None,
1051 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1052 number: Nullable[int] = None,
1053 post: Nullable[int] = None,
1054 dev: Nullable[int] = None,
1055 *,
1056 build: Nullable[int] = None,
1057 postfix: Nullable[str] = None,
1058 prefix: Nullable[str] = None,
1059 hash: Nullable[str] = None,
1060 flags: Flags = Flags.NoVCS
1061 ) -> None:
1062 """
1063 Initializes a semantic version number representation.
1065 :param major: Major number part of the version number.
1066 :param minor: Minor number part of the version number.
1067 :param micro: Micro (patch) number part of the version number.
1068 :param build: Build number part of the version number.
1069 :param level: tbd
1070 :param number: tbd
1071 :param post: Post number part of the version number.
1072 :param dev: Development number part of the version number.
1073 :param prefix: The version number's prefix.
1074 :param postfix: The version number's postfix.
1075 :param flags: The version number's flags.
1076 :param hash: tbd
1077 :raises TypeError: If parameter 'major' is not of type int.
1078 :raises ValueError: If parameter 'major' is a negative number.
1079 :raises TypeError: If parameter 'minor' is not of type int.
1080 :raises ValueError: If parameter 'minor' is a negative number.
1081 :raises TypeError: If parameter 'micro' is not of type int.
1082 :raises ValueError: If parameter 'micro' is a negative number.
1083 :raises TypeError: If parameter 'build' is not of type int.
1084 :raises ValueError: If parameter 'build' is a negative number.
1085 :raises TypeError: If parameter 'post' is not of type int.
1086 :raises ValueError: If parameter 'post' is a negative number.
1087 :raises TypeError: If parameter 'dev' is not of type int.
1088 :raises ValueError: If parameter 'dev' is a negative number.
1089 :raises TypeError: If parameter 'prefix' is not of type str.
1090 :raises TypeError: If parameter 'postfix' is not of type str.
1091 """
1092 super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags)
1094 @classmethod
1095 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "SemanticVersion":
1096 """
1097 Parse a version string and return a :class:`SemanticVersion` instance.
1099 Allowed prefix characters:
1101 * ``v|V`` - version, public version, public release
1102 * ``i|I`` - internal version, internal release
1103 * ``r|R`` - release, revision
1104 * ``rev|REV`` - revision
1106 :param versionString: The version string to parse.
1107 :returns: An object representing a semantic version.
1108 :raises TypeError: If parameter ``other`` is not a string.
1109 :raises ValueError: If parameter ``other`` is None.
1110 :raises ValueError: If parameter ``other`` is empty.
1111 """
1112 if versionString is None:
1113 raise ValueError("Parameter 'versionString' is None.")
1114 elif not isinstance(versionString, str):
1115 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1116 if version_info >= (3, 11): # pragma: no cover
1117 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1118 raise ex
1120 versionString = versionString.strip()
1121 if versionString == "":
1122 raise ValueError("Parameter 'versionString' is empty.")
1124 match = cls._PATTERN.match(versionString)
1125 if match is None:
1126 raise ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1128 def toInt(value: Nullable[str]) -> Nullable[int]:
1129 if value is None or value == "":
1130 return None
1131 try:
1132 return int(value)
1133 except ValueError as ex: # pragma: no cover
1134 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1136 release = match["release"]
1137 if release is not None:
1138 if release == "dev": 1138 ↛ 1140line 1138 didn't jump to line 1140 because the condition on line 1138 was always true
1139 releaseLevel = ReleaseLevel.Development
1140 elif release == "final":
1141 releaseLevel = ReleaseLevel.Final
1142 else: # pragma: no cover
1143 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1144 else:
1145 level = match["level"]
1146 if level is not None:
1147 level = level.lower()
1148 if level == "a" or level == "alpha":
1149 releaseLevel = ReleaseLevel.Alpha
1150 elif level == "b" or level == "beta":
1151 releaseLevel = ReleaseLevel.Beta
1152 elif level == "c" or level == "gamma":
1153 releaseLevel = ReleaseLevel.Gamma
1154 elif level == "rc":
1155 releaseLevel = ReleaseLevel.ReleaseCandidate
1156 else: # pragma: no cover
1157 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1158 else:
1159 releaseLevel = ReleaseLevel.Final
1161 version = cls(
1162 major=toInt(match["major"]),
1163 minor=toInt(match["minor"]),
1164 micro=toInt(match["micro"]),
1165 level=releaseLevel,
1166 number=toInt(match["number"]),
1167 post=toInt(match["post"]),
1168 dev=toInt(match["dev"]),
1169 build=toInt(match["build"]),
1170 postfix=match["postfix"],
1171 prefix=match["prefix"],
1172 # hash=match["hash"],
1173 flags=Flags.Clean
1174 )
1175 if validator is not None and not validator(version):
1176 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1178 return version
1180 @readonly
1181 def Patch(self) -> int:
1182 """
1183 Read-only property to access the patch number.
1185 The patch number is identical to the micro number.
1187 :return: The patch number.
1188 """
1189 return self._micro
1191 def _equal(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1192 """
1193 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1195 :param left: Left operand.
1196 :param right: Right operand.
1197 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1198 """
1199 return super()._equal(left, right)
1201 def _compare(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1202 """
1203 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1205 :param left: Left operand.
1206 :param right: Right operand.
1207 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1208 False if ``left`` is greater than ``right``. |br|
1209 Otherwise it's None (both operands are equal).
1210 """
1211 return super()._compare(left, right)
1213 def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1214 """
1215 Compare two version numbers for equality.
1217 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1218 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1219 number is assumed (all other parts are zero).
1221 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1222 number.
1224 :param other: Operand to compare against.
1225 :returns: ``True``, if both version numbers are equal.
1226 :raises ValueError: If parameter ``other`` is None.
1227 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1228 """
1229 return super().__eq__(other)
1231 def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1232 """
1233 Compare two version numbers for inequality.
1235 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1236 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1237 number is assumed (all other parts are zero).
1239 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1240 number.
1242 :param other: Operand to compare against.
1243 :returns: ``True``, if both version numbers are not equal.
1244 :raises ValueError: If parameter ``other`` is None.
1245 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1246 """
1247 return super().__ne__(other)
1249 def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1250 """
1251 Compare two version numbers if the version is less than the second operand.
1253 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1254 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1255 number is assumed (all other parts are zero).
1257 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1258 number.
1260 :param other: Operand to compare against.
1261 :returns: ``True``, if version is less than the second operand.
1262 :raises ValueError: If parameter ``other`` is None.
1263 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1264 """
1265 return super().__lt__(other)
1267 def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1268 """
1269 Compare two version numbers if the version is less than or equal the second operand.
1271 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1272 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1273 number is assumed (all other parts are zero).
1275 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1276 number.
1278 :param other: Operand to compare against.
1279 :returns: ``True``, if version is less than or equal the second operand.
1280 :raises ValueError: If parameter ``other`` is None.
1281 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1282 """
1283 return super().__le__(other)
1285 def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1286 """
1287 Compare two version numbers if the version is greater than the second operand.
1289 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1290 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1291 number is assumed (all other parts are zero).
1293 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1294 number.
1296 :param other: Operand to compare against.
1297 :returns: ``True``, if version is greater than the second operand.
1298 :raises ValueError: If parameter ``other`` is None.
1299 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1300 """
1301 return super().__gt__(other)
1303 def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1304 """
1305 Compare two version numbers if the version is greater than or equal the second operand.
1307 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1308 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1309 number is assumed (all other parts are zero).
1311 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1312 number.
1314 :param other: Operand to compare against.
1315 :returns: ``True``, if version is greater than or equal the second operand.
1316 :raises ValueError: If parameter ``other`` is None.
1317 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1318 """
1319 return super().__ge__(other)
1321 def __rshift__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1322 return super().__rshift__(other)
1324 def __hash__(self) -> int:
1325 return super().__hash__()
1327 def __format__(self, formatSpec: str) -> str:
1328 result = self._format(formatSpec)
1330 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1331 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1333 return result.replace("%%", "%")
1335 def __repr__(self) -> str:
1336 """
1337 Return a string representation of this version number without prefix ``v``.
1339 :returns: Raw version number representation without a prefix.
1340 """
1341 return f"{self._prefix if Parts.Prefix in self._parts else ''}{self._major}.{self._minor}.{self._micro}"
1343 def __str__(self) -> str:
1344 """
1345 Return a string representation of this version number.
1347 :returns: Version number representation.
1348 """
1349 result = self._prefix if Parts.Prefix in self._parts else ""
1350 result += f"{self._major}" # major is always present
1351 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1352 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1353 result += f".{self._build}" if Parts.Build in self._parts else ""
1354 if self._releaseLevel is ReleaseLevel.Development:
1355 result += "-dev"
1356 elif self._releaseLevel is ReleaseLevel.Alpha:
1357 result += f".alpha{self._releaseNumber}"
1358 elif self._releaseLevel is ReleaseLevel.Beta:
1359 result += f".beta{self._releaseNumber}"
1360 elif self._releaseLevel is ReleaseLevel.Gamma: 1360 ↛ 1361line 1360 didn't jump to line 1361 because the condition on line 1360 was never true
1361 result += f".gamma{self._releaseNumber}"
1362 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1363 result += f".rc{self._releaseNumber}"
1364 result += f".post{self._post}" if Parts.Post in self._parts else ""
1365 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1366 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1368 return result
1371@export
1372class PythonVersion(SemanticVersion):
1373 """
1374 Represents a Python version.
1375 """
1377 @classmethod
1378 def FromSysVersionInfo(cls) -> "PythonVersion":
1379 """
1380 Create a Python version from :data:`sys.version_info`.
1382 :returns: A PythonVersion instance of the current Python interpreter's version.
1383 """
1384 from sys import version_info
1386 if version_info.releaselevel == "final":
1387 rl = ReleaseLevel.Final
1388 number = None
1389 else: # pragma: no cover
1390 number = version_info.serial
1392 if version_info.releaselevel == "alpha":
1393 rl = ReleaseLevel.Alpha
1394 elif version_info.releaselevel == "beta":
1395 rl = ReleaseLevel.Beta
1396 elif version_info.releaselevel == "candidate":
1397 rl = ReleaseLevel.ReleaseCandidate
1398 else: # pragma: no cover
1399 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1401 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1403 def __hash__(self) -> int:
1404 return super().__hash__()
1406 def __str__(self) -> str:
1407 """
1408 Return a string representation of this version number.
1410 :returns: Version number representation.
1411 """
1412 result = self._prefix if Parts.Prefix in self._parts else ""
1413 result += f"{self._major}" # major is always present
1414 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1415 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1416 if self._releaseLevel is ReleaseLevel.Alpha:
1417 result += f"a{self._releaseNumber}"
1418 elif self._releaseLevel is ReleaseLevel.Beta:
1419 result += f"b{self._releaseNumber}"
1420 elif self._releaseLevel is ReleaseLevel.Gamma:
1421 result += f"c{self._releaseNumber}"
1422 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1423 result += f"rc{self._releaseNumber}"
1424 result += f".post{self._post}" if Parts.Post in self._parts else ""
1425 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1426 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1428 return result
1431@export
1432class CalendarVersion(Version):
1433 """Representation of a calendar version number like ``2021.10``."""
1435 def __init__(
1436 self,
1437 major: int,
1438 minor: Nullable[int] = None,
1439 micro: Nullable[int] = None,
1440 build: Nullable[int] = None,
1441 flags: Flags = Flags.Clean,
1442 prefix: Nullable[str] = None,
1443 postfix: Nullable[str] = None
1444 ) -> None:
1445 """
1446 Initializes a calendar version number representation.
1448 :param major: Major number part of the version number.
1449 :param minor: Minor number part of the version number.
1450 :param micro: Micro (patch) number part of the version number.
1451 :param build: Build number part of the version number.
1452 :param flags: The version number's flags.
1453 :param prefix: The version number's prefix.
1454 :param postfix: The version number's postfix.
1455 :raises TypeError: If parameter 'major' is not of type int.
1456 :raises ValueError: If parameter 'major' is a negative number.
1457 :raises TypeError: If parameter 'minor' is not of type int.
1458 :raises ValueError: If parameter 'minor' is a negative number.
1459 :raises TypeError: If parameter 'micro' is not of type int.
1460 :raises ValueError: If parameter 'micro' is a negative number.
1461 :raises TypeError: If parameter 'build' is not of type int.
1462 :raises ValueError: If parameter 'build' is a negative number.
1463 :raises TypeError: If parameter 'prefix' is not of type str.
1464 :raises TypeError: If parameter 'postfix' is not of type str.
1465 """
1466 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1468 @classmethod
1469 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion":
1470 """
1471 Parse a version string and return a :class:`CalendarVersion` instance.
1473 :param versionString: The version string to parse.
1474 :returns: An object representing a calendar version.
1475 :raises TypeError: If parameter ``other`` is not a string.
1476 :raises ValueError: If parameter ``other`` is None.
1477 :raises ValueError: If parameter ``other`` is empty.
1478 """
1479 parts = Parts.Unknown
1481 if versionString is None:
1482 raise ValueError("Parameter 'versionString' is None.")
1483 elif not isinstance(versionString, str):
1484 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1485 if version_info >= (3, 11): # pragma: no cover
1486 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1487 raise ex
1488 elif versionString == "":
1489 raise ValueError("Parameter 'versionString' is empty.")
1491 split = versionString.split(".")
1492 length = len(split)
1493 major = int(split[0])
1494 minor = 0
1495 parts |= Parts.Major
1497 if length >= 2:
1498 minor = int(split[1])
1499 parts |= Parts.Minor
1501 flags = Flags.Clean
1503 version = cls(major, minor, flags=flags)
1504 if validator is not None and not validator(version):
1505 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1507 return version
1509 @property
1510 def Year(self) -> int:
1511 """
1512 Read-only property to access the year part.
1514 :return: The year part.
1515 """
1516 return self._major
1518 def _equal(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1519 """
1520 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1522 :param left: Left parameter.
1523 :param right: Right parameter.
1524 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1525 """
1526 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1528 def _compare(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1529 """
1530 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1532 :param left: Left parameter.
1533 :param right: Right parameter.
1534 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1535 False if ``left`` is greater than ``right``. |br|
1536 Otherwise it's None (both parameters are equal).
1537 """
1538 if left._major < right._major:
1539 return True
1540 elif left._major > right._major:
1541 return False
1543 if left._minor < right._minor:
1544 return True
1545 elif left._minor > right._minor:
1546 return False
1548 if left._micro < right._micro: 1548 ↛ 1549line 1548 didn't jump to line 1549 because the condition on line 1548 was never true
1549 return True
1550 elif left._micro > right._micro: 1550 ↛ 1551line 1550 didn't jump to line 1551 because the condition on line 1550 was never true
1551 return False
1553 return None
1555 def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1556 """
1557 Compare two version numbers for equality.
1559 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1560 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1561 number is assumed (all other parts are zero).
1563 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1564 number.
1566 :param other: Parameter to compare against.
1567 :returns: ``True``, if both version numbers are equal.
1568 :raises ValueError: If parameter ``other`` is None.
1569 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1570 """
1571 return super().__eq__(other)
1573 def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1574 """
1575 Compare two version numbers for inequality.
1577 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1578 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1579 number is assumed (all other parts are zero).
1581 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1582 number.
1584 :param other: Parameter to compare against.
1585 :returns: ``True``, if both version numbers are not equal.
1586 :raises ValueError: If parameter ``other`` is None.
1587 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1588 """
1589 return super().__ne__(other)
1591 def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1592 """
1593 Compare two version numbers if the version is less than the second operand.
1595 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1596 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1597 number is assumed (all other parts are zero).
1599 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1600 number.
1602 :param other: Parameter to compare against.
1603 :returns: ``True``, if version is less than the second operand.
1604 :raises ValueError: If parameter ``other`` is None.
1605 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1606 """
1607 return super().__lt__(other)
1609 def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1610 """
1611 Compare two version numbers if the version is less than or equal the second operand.
1613 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1614 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1615 number is assumed (all other parts are zero).
1617 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1618 number.
1620 :param other: Parameter to compare against.
1621 :returns: ``True``, if version is less than or equal the second operand.
1622 :raises ValueError: If parameter ``other`` is None.
1623 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1624 """
1625 return super().__le__(other)
1627 def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1628 """
1629 Compare two version numbers if the version is greater than the second operand.
1631 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1632 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1633 number is assumed (all other parts are zero).
1635 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1636 number.
1638 :param other: Parameter to compare against.
1639 :returns: ``True``, if version is greater than the second operand.
1640 :raises ValueError: If parameter ``other`` is None.
1641 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1642 """
1643 return super().__gt__(other)
1645 def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1646 """
1647 Compare two version numbers if the version is greater than or equal the second operand.
1649 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1650 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1651 number is assumed (all other parts are zero).
1653 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1654 number.
1656 :param other: Parameter to compare against.
1657 :returns: ``True``, if version is greater than or equal the second operand.
1658 :raises ValueError: If parameter ``other`` is None.
1659 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1660 """
1661 return super().__ge__(other)
1663 def __hash__(self) -> int:
1664 return super().__hash__()
1666 def __format__(self, formatSpec: str) -> str:
1667 """
1668 Return a string representation of this version number according to the format specification.
1670 .. topic:: Format Specifiers
1672 * ``%M`` - major number (year)
1673 * ``%m`` - minor number (month/week)
1675 :param formatSpec: The format specification.
1676 :return: Formatted version number.
1677 """
1678 if formatSpec == "":
1679 return self.__str__()
1681 result = formatSpec
1682 # result = result.replace("%P", str(self._prefix))
1683 result = result.replace("%M", str(self._major))
1684 result = result.replace("%m", str(self._minor))
1685 # result = result.replace("%p", str(self._pre))
1687 return result.replace("%%", "%")
1689 def __repr__(self) -> str:
1690 """
1691 Return a string representation of this version number without prefix ``v``.
1693 :returns: Raw version number representation without a prefix.
1694 """
1695 return f"{self._major}.{self._minor}"
1697 def __str__(self) -> str:
1698 """
1699 Return a string representation of this version number with prefix ``v``.
1701 :returns: Version number representation including a prefix.
1702 """
1703 result = f"{self._major}"
1704 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1706 return result
1709@export
1710class YearMonthVersion(CalendarVersion):
1711 """Representation of a calendar version number made of year and month like ``2021.10``."""
1713 def __init__(
1714 self,
1715 year: int,
1716 month: Nullable[int] = None,
1717 build: Nullable[int] = None,
1718 flags: Flags = Flags.Clean,
1719 prefix: Nullable[str] = None,
1720 postfix: Nullable[str] = None
1721 ) -> None:
1722 """
1723 Initializes a year-month version number representation.
1725 :param year: Year part of the version number.
1726 :param month: Month part of the version number.
1727 :param build: Build number part of the version number.
1728 :param flags: The version number's flags.
1729 :param prefix: The version number's prefix.
1730 :param postfix: The version number's postfix.
1731 :raises TypeError: If parameter 'major' is not of type int.
1732 :raises ValueError: If parameter 'major' is a negative number.
1733 :raises TypeError: If parameter 'minor' is not of type int.
1734 :raises ValueError: If parameter 'minor' is a negative number.
1735 :raises TypeError: If parameter 'micro' is not of type int.
1736 :raises ValueError: If parameter 'micro' is a negative number.
1737 :raises TypeError: If parameter 'build' is not of type int.
1738 :raises ValueError: If parameter 'build' is a negative number.
1739 :raises TypeError: If parameter 'prefix' is not of type str.
1740 :raises TypeError: If parameter 'postfix' is not of type str.
1741 """
1742 super().__init__(year, month, 0, build, flags, prefix, postfix)
1744 @property
1745 def Month(self) -> int:
1746 """
1747 Read-only property to access the month part.
1749 :return: The month part.
1750 """
1751 return self._minor
1753 def __hash__(self) -> int:
1754 return super().__hash__()
1757@export
1758class YearWeekVersion(CalendarVersion):
1759 """Representation of a calendar version number made of year and week like ``2021.47``."""
1761 def __init__(
1762 self,
1763 year: int,
1764 week: Nullable[int] = None,
1765 build: Nullable[int] = None,
1766 flags: Flags = Flags.Clean,
1767 prefix: Nullable[str] = None,
1768 postfix: Nullable[str] = None
1769 ) -> None:
1770 """
1771 Initializes a year-week version number representation.
1773 :param year: Year part of the version number.
1774 :param week: Week part of the version number.
1775 :param build: Build number part of the version number.
1776 :param flags: The version number's flags.
1777 :param prefix: The version number's prefix.
1778 :param postfix: The version number's postfix.
1779 :raises TypeError: If parameter 'major' is not of type int.
1780 :raises ValueError: If parameter 'major' is a negative number.
1781 :raises TypeError: If parameter 'minor' is not of type int.
1782 :raises ValueError: If parameter 'minor' is a negative number.
1783 :raises TypeError: If parameter 'micro' is not of type int.
1784 :raises ValueError: If parameter 'micro' is a negative number.
1785 :raises TypeError: If parameter 'build' is not of type int.
1786 :raises ValueError: If parameter 'build' is a negative number.
1787 :raises TypeError: If parameter 'prefix' is not of type str.
1788 :raises TypeError: If parameter 'postfix' is not of type str.
1789 """
1790 super().__init__(year, week, 0, build, flags, prefix, postfix)
1792 @property
1793 def Week(self) -> int:
1794 """
1795 Read-only property to access the week part.
1797 :return: The week part.
1798 """
1799 return self._minor
1801 def __hash__(self) -> int:
1802 return super().__hash__()
1805@export
1806class YearReleaseVersion(CalendarVersion):
1807 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
1809 def __init__(
1810 self,
1811 year: int,
1812 release: Nullable[int] = None,
1813 build: Nullable[int] = None,
1814 flags: Flags = Flags.Clean,
1815 prefix: Nullable[str] = None,
1816 postfix: Nullable[str] = None
1817 ) -> None:
1818 """
1819 Initializes a year-release version number representation.
1821 :param year: Year part of the version number.
1822 :param release: Release number of the version number.
1823 :param build: Build number part of the version number.
1824 :param flags: The version number's flags.
1825 :param prefix: The version number's prefix.
1826 :param postfix: The version number's postfix.
1827 :raises TypeError: If parameter 'major' is not of type int.
1828 :raises ValueError: If parameter 'major' is a negative number.
1829 :raises TypeError: If parameter 'minor' is not of type int.
1830 :raises ValueError: If parameter 'minor' is a negative number.
1831 :raises TypeError: If parameter 'micro' is not of type int.
1832 :raises ValueError: If parameter 'micro' is a negative number.
1833 :raises TypeError: If parameter 'build' is not of type int.
1834 :raises ValueError: If parameter 'build' is a negative number.
1835 :raises TypeError: If parameter 'prefix' is not of type str.
1836 :raises TypeError: If parameter 'postfix' is not of type str.
1837 """
1838 super().__init__(year, release, 0, build, flags, prefix, postfix)
1840 @property
1841 def Release(self) -> int:
1842 """
1843 Read-only property to access the release number.
1845 :return: The release number.
1846 """
1847 return self._minor
1849 def __hash__(self) -> int:
1850 return super().__hash__()
1853@export
1854class YearMonthDayVersion(CalendarVersion):
1855 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
1857 def __init__(
1858 self,
1859 year: int,
1860 month: Nullable[int] = None,
1861 day: Nullable[int] = None,
1862 build: Nullable[int] = None,
1863 flags: Flags = Flags.Clean,
1864 prefix: Nullable[str] = None,
1865 postfix: Nullable[str] = None
1866 ) -> None:
1867 """
1868 Initializes a year-month-day version number representation.
1870 :param year: Year part of the version number.
1871 :param month: Month part of the version number.
1872 :param day: Day part of the version number.
1873 :param build: Build number part of the version number.
1874 :param flags: The version number's flags.
1875 :param prefix: The version number's prefix.
1876 :param postfix: The version number's postfix.
1877 :raises TypeError: If parameter 'major' is not of type int.
1878 :raises ValueError: If parameter 'major' is a negative number.
1879 :raises TypeError: If parameter 'minor' is not of type int.
1880 :raises ValueError: If parameter 'minor' is a negative number.
1881 :raises TypeError: If parameter 'micro' is not of type int.
1882 :raises ValueError: If parameter 'micro' is a negative number.
1883 :raises TypeError: If parameter 'build' is not of type int.
1884 :raises ValueError: If parameter 'build' is a negative number.
1885 :raises TypeError: If parameter 'prefix' is not of type str.
1886 :raises TypeError: If parameter 'postfix' is not of type str.
1887 """
1888 super().__init__(year, month, day, build, flags, prefix, postfix)
1890 @property
1891 def Month(self) -> int:
1892 """
1893 Read-only property to access the month part.
1895 :return: The month part.
1896 """
1897 return self._minor
1899 @property
1900 def Day(self) -> int:
1901 """
1902 Read-only property to access the day part.
1904 :return: The day part.
1905 """
1906 return self._micro
1908 def __hash__(self) -> int:
1909 return super().__hash__()
1912V = TypeVar("V", bound=Version)
1914@export
1915class RangeBoundHandling(Flag):
1916 """
1917 A flag defining how to handle bounds in a range.
1919 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
1920 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
1921 """
1922 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
1923 LowerBoundInclusive = 0 #: Lower bound is inclusive.
1924 UpperBoundInclusive = 0 #: Upper bound is inclusive.
1925 LowerBoundExclusive = 1 #: Lower bound is exclusive.
1926 UpperBoundExclusive = 2 #: Upper bound is exclusive.
1927 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
1930@export
1931class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
1932 """
1933 Representation of a version range described by a lower bound and upper bound version.
1935 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
1936 """
1937 _lowerBound: V
1938 _upperBound: V
1939 _boundHandling: RangeBoundHandling
1941 def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None:
1942 """
1943 Initializes a version range described by a lower and upper bound.
1945 :param lowerBound: lowest version (inclusive).
1946 :param upperBound: hightest version (inclusive).
1947 :raises TypeError: If parameter ``lowerBound`` is not of type :class:`Version`.
1948 :raises TypeError: If parameter ``upperBound`` is not of type :class:`Version`.
1949 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
1950 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
1951 """
1952 if not isinstance(lowerBound, Version):
1953 ex = TypeError(f"Parameter 'lowerBound' is not of type 'Version'.")
1954 if version_info >= (3, 11): # pragma: no cover
1955 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
1956 raise ex
1958 if not isinstance(upperBound, Version):
1959 ex = TypeError(f"Parameter 'upperBound' is not of type 'Version'.")
1960 if version_info >= (3, 11): # pragma: no cover
1961 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
1962 raise ex
1964 if not ((lBC := lowerBound.__class__) is (uBC := upperBound.__class__) or issubclass(lBC, uBC) or issubclass(uBC, lBC)):
1965 ex = TypeError(f"Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
1966 if version_info >= (3, 11): # pragma: no cover
1967 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and type '{getFullyQualifiedName(upperBound)}' for upperBound.")
1968 raise ex
1970 if not (lowerBound <= upperBound):
1971 ex = ValueError(f"Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
1972 if version_info >= (3, 11): # pragma: no cover
1973 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
1974 raise ex
1976 self._lowerBound = lowerBound
1977 self._upperBound = upperBound
1978 self._boundHandling = boundHandling
1980 @readonly
1981 def LowerBound(self) -> V:
1982 """
1983 Read-only property to access the range's lower bound.
1985 :return: Lower bound of the version range.
1986 """
1987 return self._lowerBound
1989 @readonly
1990 def UpperBound(self) -> V:
1991 """
1992 Read-only property to access the range's upper bound.
1994 :return: Upper bound of the version range.
1995 """
1996 return self._upperBound
1998 @readonly
1999 def BoundHandling(self) -> RangeBoundHandling:
2000 """
2001 Read-only property to access the range's bound handling strategy.
2003 :return: The range's bound handling strategy.
2004 """
2005 return self._boundHandling
2007 def __and__(self, other: Any) -> "VersionRange[T]":
2008 """
2009 Compute the intersection of two version ranges.
2011 :param other: Second version range to intersect with.
2012 :returns: Intersected version range.
2013 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2014 :raises ValueError: If intersection is empty.
2015 """
2016 if not isinstance(other, VersionRange): 2016 ↛ 2017line 2016 didn't jump to line 2017 because the condition on line 2016 was never true
2017 ex = TypeError(f"Parameter 'other' is not of type 'VersionRange'.")
2018 if version_info >= (3, 11): # pragma: no cover
2019 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2020 raise ex
2022 if not (isinstance(other._lowerBound, self._lowerBound.__class__) and isinstance(self._lowerBound, other._lowerBound.__class__)): 2022 ↛ 2023line 2022 didn't jump to line 2023 because the condition on line 2022 was never true
2023 ex = TypeError(f"Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible with each other.")
2024 if version_info >= (3, 11): # pragma: no cover
2025 ex.add_note(
2026 f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2027 raise ex
2029 if other._lowerBound < self._lowerBound:
2030 lBound = self._lowerBound
2031 elif other._lowerBound in self: 2031 ↛ 2034line 2031 didn't jump to line 2034 because the condition on line 2031 was always true
2032 lBound = other._lowerBound
2033 else:
2034 raise ValueError()
2036 if other._upperBound > self._upperBound:
2037 uBound = self._upperBound
2038 elif other._upperBound in self: 2038 ↛ 2041line 2038 didn't jump to line 2041 because the condition on line 2038 was always true
2039 uBound = other._upperBound
2040 else:
2041 raise ValueError()
2043 return self.__class__(lBound, uBound)
2045 def __lt__(self, other: Any) -> bool:
2046 """
2047 Compare a version range and a version numbers if the version range is less than the second operand (version).
2049 :param other: Operand to compare against.
2050 :returns: ``True``, if version range is less than the second operand (version).
2051 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2052 """
2053 # TODO: support VersionRange < VersionRange too
2054 # TODO: support str, int, ... like Version ?
2055 if not isinstance(other, Version):
2056 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2057 if version_info >= (3, 11): # pragma: no cover
2058 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2059 raise ex
2061 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2061 ↛ 2062line 2061 didn't jump to line 2062 because the condition on line 2061 was never true
2062 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2063 if version_info >= (3, 11): # pragma: no cover
2064 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2065 raise ex
2067 return self._upperBound < other
2069 def __le__(self, other: Any) -> bool:
2070 """
2071 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2073 :param other: Operand to compare against.
2074 :returns: ``True``, if version range is less than or equal the second operand (version).
2075 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2076 """
2077 # TODO: support VersionRange < VersionRange too
2078 # TODO: support str, int, ... like Version ?
2079 if not isinstance(other, Version): 2079 ↛ 2080line 2079 didn't jump to line 2080 because the condition on line 2079 was never true
2080 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2081 if version_info >= (3, 11): # pragma: no cover
2082 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2083 raise ex
2085 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 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 compatible with version range.")
2087 if version_info >= (3, 11): # pragma: no cover
2088 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2089 raise ex
2091 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2092 return self._upperBound < other
2093 else:
2094 return self._upperBound <= other
2096 def __gt__(self, other: Any) -> bool:
2097 """
2098 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2100 :param other: Operand to compare against.
2101 :returns: ``True``, if version range is greater than the second operand (version).
2102 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2103 """
2104 # TODO: support VersionRange < VersionRange too
2105 # TODO: support str, int, ... like Version ?
2106 if not isinstance(other, Version): 2106 ↛ 2107line 2106 didn't jump to line 2107 because the condition on line 2106 was never true
2107 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2108 if version_info >= (3, 11): # pragma: no cover
2109 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2110 raise ex
2112 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2112 ↛ 2113line 2112 didn't jump to line 2113 because the condition on line 2112 was never true
2113 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2114 if version_info >= (3, 11): # pragma: no cover
2115 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2116 raise ex
2118 return self._lowerBound > other
2120 def __ge__(self, other: Any) -> bool:
2121 """
2122 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2124 :param other: Operand to compare against.
2125 :returns: ``True``, if version range is greater than or equal the second operand (version).
2126 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2127 """
2128 # TODO: support VersionRange < VersionRange too
2129 # TODO: support str, int, ... like Version ?
2130 if not isinstance(other, Version): 2130 ↛ 2131line 2130 didn't jump to line 2131 because the condition on line 2130 was never true
2131 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2132 if version_info >= (3, 11): # pragma: no cover
2133 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2134 raise ex
2136 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2136 ↛ 2137line 2136 didn't jump to line 2137 because the condition on line 2136 was never true
2137 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2138 if version_info >= (3, 11): # pragma: no cover
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 if version_info >= (3, 11): # pragma: no cover
2158 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2159 raise ex
2161 if self._boundHandling is RangeBoundHandling.BothBoundsInclusive: 2161 ↛ 2163line 2161 didn't jump to line 2163 because the condition on line 2161 was always true
2162 return self._lowerBound <= version <= self._upperBound
2163 elif self._boundHandling is (RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive):
2164 return self._lowerBound <= version < self._upperBound
2165 elif self._boundHandling is (RangeBoundHandling.LowerBoundExclusive | RangeBoundHandling.UpperBoundInclusive):
2166 return self._lowerBound < version <= self._upperBound
2167 else:
2168 return self._lowerBound < version < self._upperBound
2171@export
2172class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2173 """
2174 Representation of an ordered set of versions.
2176 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2177 """
2178 _items: List[V] #: An ordered list of set members.
2180 def __init__(self, versions: Union[Version, Iterable[V]]):
2181 """
2182 Initializes a version set either by a single version or an iterable of versions.
2184 :param versions: A single version or an iterable of versions.
2185 :raises ValueError: If parameter ``versions`` is None`.
2186 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2187 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2188 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2189 """
2190 if versions is None:
2191 raise ValueError(f"Parameter 'versions' is None.")
2193 if isinstance(versions, Version):
2194 self._items = [versions]
2195 elif isinstance(versions, abc_Iterable): 2195 ↛ 2213line 2195 didn't jump to line 2213 because the condition on line 2195 was always true
2196 iterator = iter(versions)
2197 try:
2198 firstVersion = next(iterator)
2199 except StopIteration:
2200 self._items = []
2201 return
2203 if not isinstance(firstVersion, Version): 2203 ↛ 2204line 2203 didn't jump to line 2204 because the condition on line 2203 was never true
2204 raise TypeError(f"First element in parameter 'versions' is not of type Version.")
2206 baseType = firstVersion.__class__
2207 for version in iterator:
2208 if not isinstance(version, baseType):
2209 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2211 self._items = list(sorted(versions))
2212 else:
2213 raise TypeError(f"Parameter 'versions' is not an Iterable.")
2215 def __and__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2216 """
2217 Compute intersection of two version sets.
2219 :param other: Second set of versions.
2220 :returns: Intersection of two version sets.
2221 """
2222 selfIterator = self.__iter__()
2223 otherIterator = other.__iter__()
2225 result = []
2226 try:
2227 selfValue = next(selfIterator)
2228 otherValue = next(otherIterator)
2230 while True:
2231 if selfValue < otherValue:
2232 selfValue = next(selfIterator)
2233 elif otherValue < selfValue:
2234 otherValue = next(otherIterator)
2235 else:
2236 result.append(selfValue)
2237 selfValue = next(selfIterator)
2238 otherValue = next(otherIterator)
2240 except StopIteration:
2241 pass
2243 return VersionSet(result)
2245 def __or__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2246 """
2247 Compute union of two version sets.
2249 :param other: Second set of versions.
2250 :returns: Union of two version sets.
2251 """
2252 selfIterator = self.__iter__()
2253 otherIterator = other.__iter__()
2255 result = []
2256 try:
2257 selfValue = next(selfIterator)
2258 except StopIteration:
2259 for otherValue in otherIterator:
2260 result.append(otherValue)
2262 try:
2263 otherValue = next(otherIterator)
2264 except StopIteration:
2265 for selfValue in selfIterator:
2266 result.append(selfValue)
2268 while True:
2269 if selfValue < otherValue:
2270 result.append(selfValue)
2271 try:
2272 selfValue = next(selfIterator)
2273 except StopIteration:
2274 result.append(otherValue)
2275 for otherValue in otherIterator: 2275 ↛ 2276line 2275 didn't jump to line 2276 because the loop on line 2275 never started
2276 result.append(otherValue)
2278 break
2279 elif otherValue < selfValue:
2280 result.append(otherValue)
2281 try:
2282 otherValue = next(otherIterator)
2283 except StopIteration:
2284 result.append(selfValue)
2285 for selfValue in selfIterator:
2286 result.append(selfValue)
2288 break
2289 else:
2290 result.append(selfValue)
2291 try:
2292 selfValue = next(selfIterator)
2293 except StopIteration:
2294 for otherValue in otherIterator: 2294 ↛ 2295line 2294 didn't jump to line 2295 because the loop on line 2294 never started
2295 result.append(otherValue)
2297 break
2299 try:
2300 otherValue = next(otherIterator)
2301 except StopIteration:
2302 for selfValue in selfIterator:
2303 result.append(selfValue)
2305 break
2307 return VersionSet(result)
2309 def __lt__(self, other: Any) -> bool:
2310 """
2311 Compare a version set and a version numbers if the version set is less than the second operand (version).
2313 :param other: Operand to compare against.
2314 :returns: ``True``, if version set is less than the second operand (version).
2315 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2316 """
2317 # TODO: support VersionRange < VersionRange too
2318 # TODO: support str, int, ... like Version ?
2319 if not isinstance(other, Version):
2320 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2321 if version_info >= (3, 11): # pragma: no cover
2322 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2323 raise ex
2325 return self._items[-1] < other
2327 def __le__(self, other: Any) -> bool:
2328 """
2329 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
2331 :param other: Operand to compare against.
2332 :returns: ``True``, if version set is less than or equal the second operand (version).
2333 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2334 """
2335 # TODO: support VersionRange < VersionRange too
2336 # TODO: support str, int, ... like Version ?
2337 if not isinstance(other, Version): 2337 ↛ 2338line 2337 didn't jump to line 2338 because the condition on line 2337 was never true
2338 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2339 if version_info >= (3, 11): # pragma: no cover
2340 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2341 raise ex
2343 return self._items[-1] <= other
2345 def __gt__(self, other: Any) -> bool:
2346 """
2347 Compare a version set and a version numbers if the version set is greater than the second operand (version).
2349 :param other: Operand to compare against.
2350 :returns: ``True``, if version set is greater than the second operand (version).
2351 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2352 """
2353 # TODO: support VersionRange < VersionRange too
2354 # TODO: support str, int, ... like Version ?
2355 if not isinstance(other, Version): 2355 ↛ 2356line 2355 didn't jump to line 2356 because the condition on line 2355 was never true
2356 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2357 if version_info >= (3, 11): # pragma: no cover
2358 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2359 raise ex
2361 return self._items[0] > other
2363 def __ge__(self, other: Any) -> bool:
2364 """
2365 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
2367 :param other: Operand to compare against.
2368 :returns: ``True``, if version set is greater than or equal the second operand (version).
2369 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2370 """
2371 # TODO: support VersionRange < VersionRange too
2372 # TODO: support str, int, ... like Version ?
2373 if not isinstance(other, Version): 2373 ↛ 2374line 2373 didn't jump to line 2374 because the condition on line 2373 was never true
2374 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2375 if version_info >= (3, 11): # pragma: no cover
2376 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2377 raise ex
2379 return self._items[0] >= other
2381 def __contains__(self, version: V) -> bool:
2382 """
2383 Checks if the version a member of the set.
2385 :param version: The version to check.
2386 :returns: ``True``, if the version is a member of the set.
2387 """
2388 return version in self._items
2390 def __len__(self) -> int:
2391 """
2392 Returns the number of members in the set.
2394 :returns: Number of set members.
2395 """
2396 return len(self._items)
2398 def __iter__(self) -> Iterator[V]:
2399 """
2400 Returns an iterator to iterate all versions of this set from lowest to highest.
2402 :returns: Iterator to iterate versions.
2403 """
2404 return self._items.__iter__()
2406 def __getitem__(self, index: int) -> V:
2407 """
2408 Access to a version of a set by index.
2410 :param index: The index of the version to access.
2411 :returns: The indexed version.
2413 .. hint:: Versions are ordered from lowest to highest version number.
2414 """
2415 return self._items[index]