Coverage for pyTooling/Versioning/__init__.py: 84%
1022 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-11 23:59 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-11 23:59 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Implementation of semantic and date versioning version-numbers.
34.. hint::
36 See :ref:`high-level help <VERSIONING>` for explanations and usage examples.
38.. seealso::
40 :mod:`pyTooling.Packaging`
41 |rarr| Reading a package's version from its dunder variables.
42 :mod:`pyTooling.Dependency`
43 |rarr| Resolving requirements against these version numbers.
44"""
45from __future__ import annotations
47from collections.abc import Iterable as abc_Iterable
48from enum import Flag, Enum
49from re import compile as re_compile, Pattern
50from typing import Optional as Nullable, Union, Callable, Any, ClassVar, Generic, TypeVar, Iterable, Iterator
52from pyTooling.Decorators import export, readonly
53from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
54from pyTooling.Exceptions import ToolingException
55from pyTooling.Common import getFullyQualifiedName
58@export
59class VersionValidatorException(ToolingException):
60 """
61 Raised when a parsed version is rejected by the validator it was parsed with.
63 The version string itself was well-formed - it parsed - so this is not a :exc:`ValueError` about the input, but
64 a statement that the resulting version is not acceptable to the caller. The version that failed is carried in
65 :attr:`Version`, so a caller can report what was wrong with it.
66 """
68 _version: Nullable[Version] #: The version rejected by a validator.
70 def __init__(self, message: str, /, *, version: Nullable[Version] = None) -> None:
71 """
72 Initializes the exception with the rejected version.
74 :param message: The exception's message.
75 :param version: Optional, the version the validator rejected.
76 """
77 super().__init__(message)
78 self._version = version
80 @readonly
81 def Version(self) -> Nullable[Version]:
82 """
83 Read-only property to access the version the validator rejected (:attr:`_version`).
85 :returns: The rejected version, or ``None`` if it wasn't recorded.
86 """
87 return self._version
90@export
91class Parts(Flag):
92 """Enumeration describing parts of a version number that can be present."""
93 Unknown = 0 #: Undocumented
94 Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``).
95 Year = 1 #: Year is present. (e.g. X in ``XXXX.10``).
96 Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
97 Month = 2 #: Month is present. (e.g. X in ``2024.YY``).
98 Week = 2 #: Week is present. (e.g. X in ``2024.YY``).
99 Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
100 Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
101 Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``).
102 Level = 8 #: Release level is present.
103 Dev = 16 #: Development part is present.
104 Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
105 Post = 64 #: Post-release number is present.
106 Prefix = 128 #: Prefix is present.
107 Postfix = 256 #: Postfix is present.
108 Hash = 512 #: Hash is present.
109# AHead = 256
112@export
113class ReleaseLevel(Enum):
114 """Enumeration describing the version's maturity level."""
115 Final = 0 #:
116 ReleaseCandidate = -10 #:
117 Development = -20 #:
118 Gamma = -30 #:
119 Beta = -40 #:
120 Alpha = -50 #:
122 def __eq__(self, other: Any) -> bool:
123 """
124 Compare two release levels if the level is equal to the second operand.
126 :param other: Operand to compare against.
127 :returns: ``True``, if release level is equal the second operand's release level.
128 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
129 """
130 if isinstance(other, str): 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true
131 other = ReleaseLevel(other)
133 if not isinstance(other, ReleaseLevel): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true
134 ex = TypeError(f"Second operand is not supported by == operator.")
135 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
136 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
137 raise ex
139 return self is other
141 def __ne__(self, other: Any) -> bool:
142 """
143 Compare two release levels if the level is unequal to the second operand.
145 :param other: Operand to compare against.
146 :returns: ``True``, if release level is unequal the second operand's release level.
147 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
148 """
149 if isinstance(other, str):
150 other = ReleaseLevel(other)
152 if not isinstance(other, ReleaseLevel):
153 ex = TypeError(f"Second operand is not supported by != operator.")
154 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
155 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
156 raise ex
158 return self is not other
160 def __lt__(self, other: Any) -> bool:
161 """
162 Compare two release levels if the level is less than the second operand.
164 :param other: Operand to compare against.
165 :returns: ``True``, if release level is less than the second operand.
166 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
167 """
168 if isinstance(other, str): 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 other = ReleaseLevel(other)
171 if not isinstance(other, ReleaseLevel): 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true
172 ex = TypeError(f"Second operand is not supported by < operator.")
173 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
174 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
175 raise ex
177 return self.value < other.value
179 def __le__(self, other: Any) -> bool:
180 """
181 Compare two release levels if the level is less than or equal the second operand.
183 :param other: Operand to compare against.
184 :returns: ``True``, if release level is less than or equal the second operand.
185 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
186 """
187 if isinstance(other, str):
188 other = ReleaseLevel(other)
190 if not isinstance(other, ReleaseLevel):
191 ex = TypeError(f"Second operand is not supported by <=>= operator.")
192 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
193 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
194 raise ex
196 return self.value <= other.value
198 def __gt__(self, other: Any) -> bool:
199 """
200 Compare two release levels if the level is greater than the second operand.
202 :param other: Operand to compare against.
203 :returns: ``True``, if release level is greater than the second operand.
204 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
205 """
206 if isinstance(other, str): 206 ↛ 207line 206 didn't jump to line 207 because the condition on line 206 was never true
207 other = ReleaseLevel(other)
209 if not isinstance(other, ReleaseLevel): 209 ↛ 210line 209 didn't jump to line 210 because the condition on line 209 was never true
210 ex = TypeError(f"Second operand is not supported by > operator.")
211 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
212 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
213 raise ex
215 return self.value > other.value
217 def __ge__(self, other: Any) -> bool:
218 """
219 Compare two release levels if the level is greater than or equal the second operand.
221 :param other: Operand to compare against.
222 :returns: ``True``, if release level is greater than or equal the second operand.
223 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
224 """
225 if isinstance(other, str):
226 other = ReleaseLevel(other)
228 if not isinstance(other, ReleaseLevel):
229 ex = TypeError(f"Second operand is not supported by >= operator.")
230 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
231 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
232 raise ex
234 return self.value >= other.value
236 def __hash__(self) -> int:
237 """
238 Compute a hash for this release level, so it can be used as a key in a dictionary or an element of a set.
240 The hash is derived from the release level's value, so two release levels compare and hash alike.
242 :returns: Hash of the release level's value.
243 """
244 return hash(self.value)
246 def __str__(self) -> str:
247 """
248 Returns the release level's string equivalent.
250 :returns: The string equivalent of the release level.
251 :raises ToolingException: If the release level is unknown, so it has no string equivalent.
252 """
253 if self is ReleaseLevel.Final:
254 return "final"
255 elif self is ReleaseLevel.ReleaseCandidate:
256 return "rc"
257 elif self is ReleaseLevel.Development: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true
258 return "dev"
259 elif self is ReleaseLevel.Beta: 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 return "beta"
261 elif self is ReleaseLevel.Alpha: 261 ↛ 264line 261 didn't jump to line 264 because the condition on line 261 was always true
262 return "alpha"
264 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
267@export
268class Flags(Flag):
269 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
270 NoVCS = 0 #: No Version Control System VCS
271 Clean = 1 #: A versioned build was created from a *clean* working directory.
272 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
274 CVS = 16 #: Concurrent Versions System (CVS)
275 SVN = 32 #: Subversion (SVN)
276 Git = 64 #: Git
277 Hg = 128 #: Mercurial (Hg)
280@export
281def WordSizeValidator(
282 bits: Nullable[int] = None,
283 majorBits: Nullable[int] = None,
284 minorBits: Nullable[int] = None,
285 microBits: Nullable[int] = None,
286 buildBits: Nullable[int] = None
287):
288 """
289 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
291 :param bits: Optional, number of bits to encode any positive version number part.
292 :param majorBits: Optional, number of bits to encode a positive major number in a version.
293 :param minorBits: Optional, number of bits to encode a positive minor number in a version.
294 :param microBits: Optional, number of bits to encode a positive micro number in a version.
295 :param buildBits: Optional, number of bits to encode a positive build number in a version.
296 :returns: A validation function for Version instances.
297 """
298 majorMax = minorMax = microMax = buildMax = -1
299 if bits is not None:
300 majorMax = minorMax = microMax = buildMax = 2**bits - 1
302 if majorBits is not None:
303 majorMax = 2**majorBits - 1
304 if minorBits is not None:
305 minorMax = 2**minorBits - 1
306 if microBits is not None:
307 microMax = 2 ** microBits - 1
308 if buildBits is not None: 308 ↛ 309line 308 didn't jump to line 309 because the condition on line 308 was never true
309 buildMax = 2**buildBits - 1
311 def validator(version: SemanticVersion) -> bool:
312 """
313 Validator function, which checks each version part against the maximum its word size allows.
315 :param version: Optional, the version to validate.
316 :returns: ``True``, if every part fits into its word size.
317 :raises ValueError: If a part exceeds the maximum value of its word size.
318 """
319 if Parts.Major in version._parts and version._major > majorMax:
320 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
322 if Parts.Minor in version._parts and version._minor > minorMax:
323 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
325 if Parts.Micro in version._parts and version._micro > microMax:
326 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
328 if Parts.Build in version._parts and version._build > buildMax: 328 ↛ 329line 328 didn't jump to line 329 because the condition on line 328 was never true
329 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
331 return True
333 return validator
336@export
337def MaxValueValidator(
338 max: Nullable[int] = None,
339 majorMax: Nullable[int] = None,
340 minorMax: Nullable[int] = None,
341 microMax: Nullable[int] = None,
342 buildMax: Nullable[int] = None
343):
344 """
345 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
347 :param max: Optional, the upper bound for any positive version number part.
348 :param majorMax: Optional, the upper bound for the positive major number.
349 :param minorMax: Optional, the upper bound for the positive minor number.
350 :param microMax: Optional, the upper bound for the positive micro number.
351 :param buildMax: Optional, the upper bound for the positive build number.
352 :returns: A validation function for Version instances.
353 """
354 if max is not None: 354 ↛ 357line 354 didn't jump to line 357 because the condition on line 354 was always true
355 majorMax = minorMax = microMax = buildMax = max
357 def validator(version: SemanticVersion) -> bool:
358 """
359 Validator function, which checks each version part against its maximum value.
361 :param version: Optional, the version to validate.
362 :returns: ``True``, if every part is within its maximum.
363 :raises ValueError: If a part exceeds its maximum value.
364 """
365 if Parts.Major in version._parts and version._major > majorMax:
366 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
368 if Parts.Minor in version._parts and version._minor > minorMax:
369 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
371 if Parts.Micro in version._parts and version._micro > microMax:
372 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
374 if Parts.Build in version._parts and version._build > buildMax: 374 ↛ 375line 374 didn't jump to line 375 because the condition on line 374 was never true
375 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
377 return True
379 return validator
382@export
383class Version(metaclass=ExtendedType, slots=True):
384 """Base-class for a version representation."""
386 __hash: Nullable[int] #: once computed hash of the object
388 _parts: Parts #: Integer flag enumeration of present parts in a version number.
389 _prefix: str #: Prefix string
390 _major: int #: Major number part of the version number.
391 _minor: int #: Minor number part of the version number.
392 _micro: int #: Micro number part of the version number.
393 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
394 _releaseNumber: int #: Release number (Python calls this a serial).
395 _post: int #: Post-release version number part.
396 _dev: int #: Development number
397 _build: int #: Build number part of the version number.
398 _postfix: str #: Postfix string
399 _hash: str #: Hash from version control system.
400 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
402 def __init__(
403 self,
404 major: int,
405 minor: Nullable[int] = None,
406 micro: Nullable[int] = None,
407 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
408 number: Nullable[int] = None,
409 post: Nullable[int] = None,
410 dev: Nullable[int] = None,
411 *,
412 build: Nullable[int] = None,
413 postfix: Nullable[str] = None,
414 prefix: Nullable[str] = None,
415 hash: Nullable[str] = None,
416 flags: Flags = Flags.NoVCS
417 ) -> None:
418 """
419 Initializes a version number representation.
421 :param major: Major number part of the version number.
422 :param minor: Optional, minor number part of the version number.
423 :param micro: Optional, micro (patch) number part of the version number.
424 :param level: Optional, release level (alpha, beta, release candidate, final, ...) of the version number.
425 :param number: Optional, release number part (in combination with release level) of the version number.
426 :param post: Optional, post number part of the version number.
427 :param dev: Optional, development number part of the version number.
428 :param build: Optional, build number part of the version number.
429 :param postfix: Optional, the version number's postfix.
430 :param prefix: Optional, the version number's prefix.
431 :param hash: Optional, postfix string.
432 :param flags: Optional, the version number's flags.
433 :raises TypeError: If parameter 'major' is not of type integer.
434 :raises ValueError: If parameter 'major' is a negative number.
435 :raises TypeError: If parameter 'minor' is not of type integer.
436 :raises ValueError: If parameter 'minor' is a negative number.
437 :raises TypeError: If parameter 'micro' is not of type integer.
438 :raises ValueError: If parameter 'micro' is a negative number.
439 :raises TypeError: If parameter 'build' is not of type integer.
440 :raises ValueError: If parameter 'build' is a negative number.
441 :raises TypeError: If parameter 'prefix' is not of type string.
442 :raises TypeError: If parameter 'postfix' is not of type string.
443 """
444 self.__hash = None
446 if not isinstance(major, int):
447 raise TypeError("Parameter 'major' is not of type 'int'.")
448 elif major < 0:
449 raise ValueError("Parameter 'major' is negative.")
451 self._parts = Parts.Major
452 self._major = major
454 if minor is not None:
455 if not isinstance(minor, int):
456 raise TypeError("Parameter 'minor' is not of type 'int'.")
457 elif minor < 0:
458 raise ValueError("Parameter 'minor' is negative.")
460 self._parts |= Parts.Minor
461 self._minor = minor
462 else:
463 self._minor = 0
465 if micro is not None:
466 if not isinstance(micro, int):
467 raise TypeError("Parameter 'micro' is not of type 'int'.")
468 elif micro < 0:
469 raise ValueError("Parameter 'micro' is negative.")
471 self._parts |= Parts.Micro
472 self._micro = micro
473 else:
474 self._micro = 0
476 if level is None:
477 raise ValueError("Parameter 'level' is None.")
478 elif not isinstance(level, ReleaseLevel):
479 raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
480 elif level is ReleaseLevel.Final:
481 if number is not None:
482 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
484 self._parts |= Parts.Level
485 self._releaseLevel = level
486 self._releaseNumber = 0
487 else:
488 self._parts |= Parts.Level
489 self._releaseLevel = level
491 if number is not None:
492 if not isinstance(number, int):
493 raise TypeError("Parameter 'number' is not of type 'int'.")
494 elif number < 0:
495 raise ValueError("Parameter 'number' is negative.")
497 self._releaseNumber = number
498 else:
499 self._releaseNumber = 0
501 if dev is not None:
502 if not isinstance(dev, int):
503 raise TypeError("Parameter 'dev' is not of type 'int'.")
504 elif dev < 0:
505 raise ValueError("Parameter 'dev' is negative.")
507 self._parts |= Parts.Dev
508 self._dev = dev
509 else:
510 self._dev = 0
512 if post is not None:
513 if not isinstance(post, int):
514 raise TypeError("Parameter 'post' is not of type 'int'.")
515 elif post < 0:
516 raise ValueError("Parameter 'post' is negative.")
518 self._parts |= Parts.Post
519 self._post = post
520 else:
521 self._post = 0
523 if build is not None:
524 if not isinstance(build, int):
525 raise TypeError("Parameter 'build' is not of type 'int'.")
526 elif build < 0:
527 raise ValueError("Parameter 'build' is negative.")
529 self._build = build
530 self._parts |= Parts.Build
531 else:
532 self._build = 0
534 if postfix is not None:
535 if not isinstance(postfix, str):
536 raise TypeError("Parameter 'postfix' is not of type 'str'.")
538 self._parts |= Parts.Postfix
539 self._postfix = postfix
540 else:
541 self._postfix = ""
543 if prefix is not None:
544 if not isinstance(prefix, str):
545 raise TypeError("Parameter 'prefix' is not of type 'str'.")
547 self._parts |= Parts.Prefix
548 self._prefix = prefix
549 else:
550 self._prefix = ""
552 if hash is not None:
553 if not isinstance(hash, str):
554 raise TypeError("Parameter 'hash' is not of type 'str'.")
556 self._parts |= Parts.Hash
557 self._hash = hash
558 else:
559 self._hash = ""
561 if flags is None:
562 raise ValueError("Parameter 'flags' is None.")
563 elif not isinstance(flags, Flags):
564 raise TypeError("Parameter 'flags' is not of type 'Flags'.")
566 self._flags = flags
568 @classmethod
569 @abstractmethod
570 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[SemanticVersion], bool]] = None) -> Version:
571 """
572 Parse a version string and return a Version instance.
574 :param versionString: The version string to parse.
575 :param validator: Optional, validator rejecting a parsed version, e.g. by word size or maximum value.
576 :returns: The parsed version number.
577 """
579 @readonly
580 def Parts(self) -> Parts:
581 """
582 Read-only property to access the used parts of this version number.
584 :returns: A flag enumeration of used version number parts.
585 """
586 return self._parts
588 @readonly
589 def Prefix(self) -> str:
590 """
591 Read-only property to access the version number's prefix.
593 :returns: The prefix of the version number.
594 """
595 return self._prefix
597 @readonly
598 def Major(self) -> int:
599 """
600 Read-only property to access the major number.
602 :returns: The major number.
603 """
604 return self._major
606 @readonly
607 def Minor(self) -> int:
608 """
609 Read-only property to access the minor number.
611 :returns: The minor number.
612 """
613 return self._minor
615 @readonly
616 def Micro(self) -> int:
617 """
618 Read-only property to access the micro number.
620 :returns: The micro number.
621 """
622 return self._micro
624 @readonly
625 def ReleaseLevel(self) -> ReleaseLevel:
626 """
627 Read-only property to access the release level.
629 :returns: The release level.
630 """
631 return self._releaseLevel
633 @readonly
634 def ReleaseNumber(self) -> int:
635 """
636 Read-only property to access the release number.
638 :returns: The release number.
639 """
640 return self._releaseNumber
642 @readonly
643 def Post(self) -> int:
644 """
645 Read-only property to access the post number.
647 :returns: The post number.
648 """
649 return self._post
651 @readonly
652 def Dev(self) -> int:
653 """
654 Read-only property to access the development number.
656 :returns: The development number.
657 """
658 return self._dev
660 @readonly
661 def Build(self) -> int:
662 """
663 Read-only property to access the build number.
665 :returns: The build number.
666 """
667 return self._build
669 @readonly
670 def Postfix(self) -> str:
671 """
672 Read-only property to access the version number's postfix.
674 :returns: The postfix of the version number.
675 """
676 return self._postfix
678 @readonly
679 def Hash(self) -> str:
680 """
681 Read-only property to access the version number's hash.
683 :returns: The hash.
684 """
685 return self._hash
687 @readonly
688 def Flags(self) -> Flags:
689 """
690 Read-only property to access the version number's flags.
692 :returns: The flags of the version number.
693 """
694 return self._flags
696 def _equal(self, left: Version, right: Version) -> Nullable[bool]:
697 """
698 Private helper method to compute the equality of two :class:`Version` instances.
700 :param left: Left operand.
701 :param right: Right operand.
702 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
703 """
704 return (
705 (left._major == right._major) and
706 (left._minor == right._minor) and
707 (left._micro == right._micro) and
708 (left._releaseLevel == right._releaseLevel) and
709 (left._releaseNumber == right._releaseNumber) and
710 (left._post == right._post) and
711 (left._dev == right._dev) and
712 (left._build == right._build) and
713 (left._postfix == right._postfix)
714 )
716 def _compare(self, left: Version, right: Version) -> Nullable[bool]:
717 """
718 Private helper method to compute the comparison of two :class:`Version` instances.
720 :param left: Left operand.
721 :param right: Right operand.
722 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
723 False if ``left`` is greater than ``right``. |br|
724 Otherwise it's None (both operands are equal).
725 """
726 if left._major < right._major:
727 return True
728 elif left._major > right._major:
729 return False
731 if left._minor < right._minor:
732 return True
733 elif left._minor > right._minor:
734 return False
736 if left._micro < right._micro:
737 return True
738 elif left._micro > right._micro:
739 return False
741 if left._releaseLevel < right._releaseLevel: 741 ↛ 742line 741 didn't jump to line 742 because the condition on line 741 was never true
742 return True
743 elif left._releaseLevel > right._releaseLevel: 743 ↛ 744line 743 didn't jump to line 744 because the condition on line 743 was never true
744 return False
746 if left._releaseNumber < right._releaseNumber: 746 ↛ 747line 746 didn't jump to line 747 because the condition on line 746 was never true
747 return True
748 elif left._releaseNumber > right._releaseNumber: 748 ↛ 749line 748 didn't jump to line 749 because the condition on line 748 was never true
749 return False
751 if left._post < right._post: 751 ↛ 752line 751 didn't jump to line 752 because the condition on line 751 was never true
752 return True
753 elif left._post > right._post: 753 ↛ 754line 753 didn't jump to line 754 because the condition on line 753 was never true
754 return False
756 if left._dev < right._dev: 756 ↛ 757line 756 didn't jump to line 757 because the condition on line 756 was never true
757 return True
758 elif left._dev > right._dev: 758 ↛ 759line 758 didn't jump to line 759 because the condition on line 758 was never true
759 return False
761 if left._build < right._build: 761 ↛ 762line 761 didn't jump to line 762 because the condition on line 761 was never true
762 return True
763 elif left._build > right._build: 763 ↛ 764line 763 didn't jump to line 764 because the condition on line 763 was never true
764 return False
766 return None
768 def _minimum(self, actual: Version, expected: Version) -> Nullable[bool]:
769 """
770 Check if a version fulfills a minimum requirement.
772 How exact the comparison is depends on how detailed the expected version is: a minor number in the expectation
773 requires an exact major number, and a micro number requires an exact minor number.
775 :param actual: The version to check.
776 :param expected: The minimum version, whose parts decide how exact the comparison is.
777 :returns: ``True``, if the actual version fulfills the expectation.
778 """
779 exactMajor = Parts.Minor in expected._parts
780 exactMinor = Parts.Micro in expected._parts
782 if exactMajor and actual._major != expected._major: 782 ↛ 783line 782 didn't jump to line 783 because the condition on line 782 was never true
783 return False
784 elif not exactMajor and actual._major < expected._major:
785 return False
787 if exactMinor and actual._minor != expected._minor: 787 ↛ 788line 787 didn't jump to line 788 because the condition on line 787 was never true
788 return False
789 elif not exactMinor and actual._minor < expected._minor:
790 return False
792 if Parts.Micro in expected._parts:
793 return actual._micro >= expected._micro
795 return True
797 def _format(self, formatSpec: str) -> str:
798 """
799 Return a string representation of this version number according to the format specification.
801 .. topic:: Format Specifiers
803 * ``%p`` - prefix
804 * ``%M`` - major number
805 * ``%m`` - minor number
806 * ``%u`` - micro number
807 * ``%b`` - build number
809 :param formatSpec: The format specification.
810 :returns: Formatted version number.
811 """
812 if formatSpec == "":
813 return self.__str__()
815 result = formatSpec
816 result = result.replace("%p", str(self._prefix))
817 result = result.replace("%M", str(self._major))
818 result = result.replace("%m", str(self._minor))
819 result = result.replace("%u", str(self._micro))
820 result = result.replace("%b", str(self._build))
821 result = result.replace("%r", str(self._releaseLevel)[0])
822 result = result.replace("%R", str(self._releaseLevel))
823 result = result.replace("%n", str(self._releaseNumber))
824 result = result.replace("%d", str(self._dev))
825 result = result.replace("%P", str(self._postfix))
827 return result
829 @mustoverride
830 def __eq__(self, other: Any) -> bool:
831 """
832 Compare two version numbers for equality.
834 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
835 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
836 number is assumed (all other parts are zero).
838 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
839 number.
841 :param other: Operand to compare against.
842 :returns: ``True``, if both version numbers are equal.
843 :raises ValueError: If parameter ``other`` is None.
844 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer.
845 """
846 if other is None:
847 raise ValueError(f"Second operand is None.")
848 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
849 pass
850 elif isinstance(other, str):
851 other = self.__class__.Parse(other)
852 elif isinstance(other, int):
853 other = self.__class__(major=other)
854 else:
855 ex = TypeError(f"Second operand is not supported by == operator.")
856 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
857 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
858 raise ex
860 return self._equal(self, other)
862 @mustoverride
863 def __ne__(self, other: Any) -> bool:
864 """
865 Compare two version numbers for inequality.
867 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
868 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
869 number is assumed (all other parts are zero).
871 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
872 number.
874 :param other: Operand to compare against.
875 :returns: ``True``, if both version numbers are not equal.
876 :raises ValueError: If parameter ``other`` is None.
877 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer.
878 """
879 if other is None:
880 raise ValueError(f"Second operand is None.")
881 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
882 pass
883 elif isinstance(other, str):
884 other = self.__class__.Parse(other)
885 elif isinstance(other, int):
886 other = self.__class__(major=other)
887 else:
888 ex = TypeError(f"Second operand is not supported by == operator.")
889 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
890 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
891 raise ex
893 return not self._equal(self, other)
895 @mustoverride
896 def __lt__(self, other: Any) -> bool:
897 """
898 Compare two version numbers if the version is less than the second operand.
900 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
901 ``str`` and ``int`` are accepted, too. |br|
902 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
903 number is assumed (all other parts are zero).
905 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
906 number.
908 :param other: Operand to compare against.
909 :returns: ``True``, if version is less than the second operand.
910 :raises ValueError: If parameter ``other`` is None.
911 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
912 :class:`VersionSet`, string or integer.
913 """
914 if other is None:
915 raise ValueError(f"Second operand is None.")
916 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
917 pass
918 elif isinstance(other, VersionRange):
919 other = other._lowerBound
920 elif isinstance(other, VersionSet):
921 other = other._items[0]
922 elif isinstance(other, str):
923 other = self.__class__.Parse(other)
924 elif isinstance(other, int):
925 other = self.__class__(major=other)
926 else:
927 ex = TypeError(f"Second operand is not supported by < operator.")
928 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
929 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
930 raise ex
932 return self._compare(self, other) is True
934 @mustoverride
935 def __le__(self, other: Any) -> bool:
936 """
937 Compare two version numbers if the version is less than or equal the second operand.
939 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
940 ``str`` and ``int`` are accepted, too. |br|
941 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
942 number is assumed (all other parts are zero).
944 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
945 number.
947 :param other: Operand to compare against.
948 :returns: ``True``, if version is less than or equal the second operand.
949 :raises ValueError: If parameter ``other`` is None.
950 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
951 :class:`VersionSet`, string or integer.
952 """
953 equalValue = True
954 if other is None:
955 raise ValueError(f"Second operand is None.")
956 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
957 pass
958 elif isinstance(other, VersionRange):
959 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
960 other = other._lowerBound
961 elif isinstance(other, VersionSet):
962 other = other._items[0]
963 elif isinstance(other, str):
964 other = self.__class__.Parse(other)
965 elif isinstance(other, int):
966 other = self.__class__(major=other)
967 else:
968 ex = TypeError(f"Second operand is not supported by <= operator.")
969 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
970 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
971 raise ex
973 result = self._compare(self, other)
974 return result if result is not None else equalValue
976 @mustoverride
977 def __gt__(self, other: Any) -> bool:
978 """
979 Compare two version numbers if the version is greater than the second operand.
981 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
982 ``str`` and ``int`` are accepted, too. |br|
983 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
984 number is assumed (all other parts are zero).
986 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
987 number.
989 :param other: Operand to compare against.
990 :returns: ``True``, if version is greater than the second operand.
991 :raises ValueError: If parameter ``other`` is None.
992 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
993 :class:`VersionSet`, string or integer.
994 """
995 if other is None:
996 raise ValueError(f"Second operand is None.")
997 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
998 pass
999 elif isinstance(other, VersionRange):
1000 other = other._upperBound
1001 elif isinstance(other, VersionSet):
1002 other = other._items[-1]
1003 elif isinstance(other, str):
1004 other = self.__class__.Parse(other)
1005 elif isinstance(other, int):
1006 other = self.__class__(major=other)
1007 else:
1008 ex = TypeError(f"Second operand is not supported by > operator.")
1009 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1010 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
1011 raise ex
1013 return self._compare(self, other) is False
1015 @mustoverride
1016 def __ge__(self, other: Any) -> bool:
1017 """
1018 Compare two version numbers if the version is greater than or equal the second operand.
1020 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1021 ``str`` and ``int`` are accepted, too. |br|
1022 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1023 number is assumed (all other parts are zero).
1025 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1026 number.
1028 :param other: Operand to compare against.
1029 :returns: ``True``, if version is greater than or equal the second operand.
1030 :raises ValueError: If parameter ``other`` is None.
1031 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1032 :class:`VersionSet`, string or integer.
1033 """
1034 equalValue = True
1035 if other is None:
1036 raise ValueError(f"Second operand is None.")
1037 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1038 pass
1039 elif isinstance(other, VersionRange):
1040 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
1041 other = other._upperBound
1042 elif isinstance(other, VersionSet):
1043 other = other._items[-1]
1044 elif isinstance(other, str):
1045 other = self.__class__.Parse(other)
1046 elif isinstance(other, int):
1047 other = self.__class__(major=other)
1048 else:
1049 ex = TypeError(f"Second operand is not supported by >= operator.")
1050 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1051 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
1052 raise ex
1054 result = self._compare(self, other)
1055 return not result if result is not None else equalValue
1057 def __rshift__(self, other: Union[Version, str, int, None]) -> bool:
1058 """
1059 Return the minimum of this version and a second operand.
1061 :param other: Second operand, a version, a version string or a major version number.
1062 :returns: ``True``, if this version is the minimum of both operands.
1063 :raises ValueError: If the second operand is ``None``.
1064 :raises TypeError: If the second operand is not a version, a string or an integer.
1065 """
1066 if other is None:
1067 raise ValueError(f"Second operand is None.")
1068 elif isinstance(other, self.__class__):
1069 pass
1070 elif isinstance(other, str):
1071 other = self.__class__.Parse(other)
1072 elif isinstance(other, int):
1073 other = self.__class__(major=other)
1074 else:
1075 ex = TypeError(f"Second operand is not supported by >> operator.")
1076 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1077 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
1078 raise ex
1080 return self._minimum(self, other)
1082 def __hash__(self) -> int:
1083 """
1084 Compute a hash for this version number and cache it.
1086 All parts of the version are part of the hash, so two versions differing in a postfix or a build number don't
1087 collide.
1089 :returns: Hash of this version number.
1090 """
1091 if self.__hash is None:
1092 self.__hash = hash((
1093 self._prefix,
1094 self._major,
1095 self._minor,
1096 self._micro,
1097 self._releaseLevel,
1098 self._releaseNumber,
1099 self._post,
1100 self._dev,
1101 self._build,
1102 self._postfix,
1103 self._hash,
1104 self._flags
1105 ))
1106 return self.__hash
1109@export
1110class SemanticVersion(Version):
1111 """Representation of a semantic version number like ``3.7.12``."""
1113 _PATTERN: ClassVar[Pattern] = re_compile(
1114 r"^"
1115 r"(?P<prefix>rev|REV|[vViIrR])?"
1116 r"(?P<major>\d+)"
1117 r"(?:\.(?P<minor>\d+))?"
1118 r"(?:\.(?P<micro>\d+))?"
1119 r"(?:"
1120 r"(?:\.(?P<build>\d+))"
1121 r"|"
1122 r"(?:[-](?P<release>dev|final))"
1123 r"|"
1124 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
1125 r")?"
1126 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1127 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1128 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1129 r"$"
1130 ) #: Regular expression to parse a semantic version from a string.
1131# QUESTION: was this how many commits a version is ahead of the last tagged version?
1132# ahead: int = 0
1134 def __init__(
1135 self,
1136 major: int,
1137 minor: Nullable[int] = None,
1138 micro: Nullable[int] = None,
1139 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1140 number: Nullable[int] = None,
1141 post: Nullable[int] = None,
1142 dev: Nullable[int] = None,
1143 *,
1144 build: Nullable[int] = None,
1145 postfix: Nullable[str] = None,
1146 prefix: Nullable[str] = None,
1147 hash: Nullable[str] = None,
1148 flags: Flags = Flags.NoVCS
1149 ) -> None:
1150 """
1151 Initializes a semantic version number representation.
1153 :param major: Major number part of the version number.
1154 :param minor: Optional, minor number part of the version number.
1155 :param micro: Optional, micro (patch) number part of the version number.
1156 :param level: Optional, release level of the version number (alpha, beta, release candidate, final, ...).
1157 :param number: Optional, number within the release level, e.g. ``2`` in ``rc2``.
1158 :param post: Optional, post number part of the version number.
1159 :param dev: Optional, development number part of the version number.
1160 :param build: Optional, build number part of the version number.
1161 :param postfix: Optional, the version number's postfix.
1162 :param prefix: Optional, the version number's prefix.
1163 :param hash: Optional, hash of the version control system's commit this version was built from.
1164 :param flags: Optional, the version number's flags.
1165 :raises TypeError: If parameter 'major' is not of type integer.
1166 :raises ValueError: If parameter 'major' is a negative number.
1167 :raises TypeError: If parameter 'minor' is not of type integer.
1168 :raises ValueError: If parameter 'minor' is a negative number.
1169 :raises TypeError: If parameter 'micro' is not of type integer.
1170 :raises ValueError: If parameter 'micro' is a negative number.
1171 :raises TypeError: If parameter 'build' is not of type integer.
1172 :raises ValueError: If parameter 'build' is a negative number.
1173 :raises TypeError: If parameter 'post' is not of type integer.
1174 :raises ValueError: If parameter 'post' is a negative number.
1175 :raises TypeError: If parameter 'dev' is not of type integer.
1176 :raises ValueError: If parameter 'dev' is a negative number.
1177 :raises TypeError: If parameter 'prefix' is not of type string.
1178 :raises TypeError: If parameter 'postfix' is not of type string.
1179 """
1180 super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags)
1182 @classmethod
1183 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[SemanticVersion], bool]] = None) -> SemanticVersion:
1184 """
1185 Parse a version string and return a :class:`SemanticVersion` instance.
1187 Allowed prefix characters:
1189 * ``v|V`` - version, public version, public release
1190 * ``i|I`` - internal version, internal release
1191 * ``r|R`` - release, revision
1192 * ``rev|REV`` - revision
1194 :param versionString: The version string to parse.
1195 :param validator: Optional, a validation function.
1196 :returns: An object representing a semantic version.
1197 :raises TypeError: When parameter ``versionString`` is not a string.
1198 :raises ValueError: When parameter ``versionString`` is None or empty.
1199 :raises ValueError: When parameter ``versionString`` isn't a semantic version number. |br|
1200 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g. ``v1.2.3``.
1201 :raises VersionValidatorException: When the parsed version is rejected by ``validator``.
1202 """
1203 if versionString is None:
1204 raise ValueError("Parameter 'versionString' is None.")
1205 elif not isinstance(versionString, str):
1206 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1207 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1208 raise ex
1209 elif (versionString := versionString.strip()) == "":
1210 raise ValueError("Parameter 'versionString' is empty.")
1212 if (match := cls._PATTERN.match(versionString)) is None:
1213 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1214 ex.add_note(f"It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v1.2.3'.")
1215 raise ex
1217 def toInt(value: Nullable[str]) -> Nullable[int]:
1218 """
1219 Nested function converting an optional part of a version string to an integer.
1221 :param value: The matched part, or ``None`` if the pattern didn't match it.
1222 :returns: The part as an integer, or ``None`` if it wasn't present.
1223 :raises ValueError: If the part isn't a number.
1224 """
1225 if value is None or value == "":
1226 return None
1228 try:
1229 return int(value)
1230 except ValueError as ex: # pragma: no cover
1231 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1233 prefix = match["prefix"]
1235 release = match["release"]
1236 if release is not None:
1237 if release == "dev": 1237 ↛ 1239line 1237 didn't jump to line 1239 because the condition on line 1237 was always true
1238 releaseLevel = ReleaseLevel.Development
1239 elif release == "final":
1240 releaseLevel = ReleaseLevel.Final
1241 else: # pragma: no cover
1242 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1243 else:
1244 level = match["level"]
1245 if level is not None:
1246 level = level.lower()
1247 if level == "a" or level == "alpha":
1248 releaseLevel = ReleaseLevel.Alpha
1249 elif level == "b" or level == "beta":
1250 releaseLevel = ReleaseLevel.Beta
1251 elif level == "c" or level == "gamma":
1252 releaseLevel = ReleaseLevel.Gamma
1253 elif level == "rc":
1254 releaseLevel = ReleaseLevel.ReleaseCandidate
1255 else: # pragma: no cover
1256 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1257 else:
1258 releaseLevel = ReleaseLevel.Final
1260 version = cls(
1261 major=toInt(match["major"]),
1262 minor=toInt(match["minor"]),
1263 micro=toInt(match["micro"]),
1264 level=releaseLevel,
1265 number=toInt(match["number"]),
1266 post=toInt(match["post"]),
1267 dev=toInt(match["dev"]),
1268 build=toInt(match["build"]),
1269 postfix=match["postfix"],
1270 prefix=prefix if prefix != "" else None,
1271 # hash=match["hash"],
1272 flags=Flags.Clean
1273 )
1275 if validator is not None and not validator(version):
1276 raise VersionValidatorException(f"Failed to validate version string '{versionString}'.", version=version)
1278 return version
1280 @readonly
1281 def Patch(self) -> int:
1282 """
1283 Read-only property to access the patch number.
1285 The patch number is identical to the micro number.
1287 :returns: The patch number.
1288 """
1289 return self._micro
1291 def _equal(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]:
1292 """
1293 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1295 :param left: Left operand.
1296 :param right: Right operand.
1297 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1298 """
1299 return super()._equal(left, right)
1301 def _compare(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]:
1302 """
1303 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1305 :param left: Left operand.
1306 :param right: Right operand.
1307 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1308 False if ``left`` is greater than ``right``. |br|
1309 Otherwise it's None (both operands are equal).
1310 """
1311 return super()._compare(left, right)
1313 def __eq__(self, other: Any) -> bool:
1314 """
1315 Compare two version numbers for equality.
1317 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1318 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1319 number is assumed (all other parts are zero).
1321 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1322 number.
1324 :param other: Operand to compare against.
1325 :returns: ``True``, if both version numbers are equal.
1326 :raises ValueError: If parameter ``other`` is None.
1327 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1328 """
1329 return super().__eq__(other)
1331 def __ne__(self, other: Any) -> bool:
1332 """
1333 Compare two version numbers for inequality.
1335 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1336 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1337 number is assumed (all other parts are zero).
1339 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1340 number.
1342 :param other: Operand to compare against.
1343 :returns: ``True``, if both version numbers are not equal.
1344 :raises ValueError: If parameter ``other`` is None.
1345 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1346 """
1347 return super().__ne__(other)
1349 def __lt__(self, other: Any) -> bool:
1350 """
1351 Compare two version numbers if the version is less than the second operand.
1353 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1354 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1355 number is assumed (all other parts are zero).
1357 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1358 number.
1360 :param other: Operand to compare against.
1361 :returns: ``True``, if version is less than the second operand.
1362 :raises ValueError: If parameter ``other`` is None.
1363 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1364 """
1365 return super().__lt__(other)
1367 def __le__(self, other: Any) -> bool:
1368 """
1369 Compare two version numbers if the version is less than or equal the second operand.
1371 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1372 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1373 number is assumed (all other parts are zero).
1375 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1376 number.
1378 :param other: Operand to compare against.
1379 :returns: ``True``, if version is less than or equal the second operand.
1380 :raises ValueError: If parameter ``other`` is None.
1381 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1382 """
1383 return super().__le__(other)
1385 def __gt__(self, other: Any) -> bool:
1386 """
1387 Compare two version numbers if the version is greater than the second operand.
1389 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1390 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1391 number is assumed (all other parts are zero).
1393 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1394 number.
1396 :param other: Operand to compare against.
1397 :returns: ``True``, if version is greater than the second operand.
1398 :raises ValueError: If parameter ``other`` is None.
1399 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1400 """
1401 return super().__gt__(other)
1403 def __ge__(self, other: Any) -> bool:
1404 """
1405 Compare two version numbers if the version is greater than or equal the second operand.
1407 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1408 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1409 number is assumed (all other parts are zero).
1411 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1412 number.
1414 :param other: Operand to compare against.
1415 :returns: ``True``, if version is greater than or equal the second operand.
1416 :raises ValueError: If parameter ``other`` is None.
1417 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1418 """
1419 return super().__ge__(other)
1421 def __rshift__(self, other: Union[SemanticVersion, str, int, None]) -> bool:
1422 """
1423 Return the minimum of this semantic version and a second operand.
1425 :param other: Second operand, a version, a version string or a major version number.
1426 :returns: ``True``, if this version is the minimum of both operands.
1427 :raises ValueError: If the second operand is ``None``.
1428 :raises TypeError: If the second operand is not a version, a string or an integer.
1429 """
1430 return super().__rshift__(other)
1432 def __hash__(self) -> int:
1433 """
1434 Compute a hash for this version number.
1436 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1437 version unhashable.
1439 :returns: Hash of this version number.
1440 """
1441 return super().__hash__()
1443 def __format__(self, formatSpec: str) -> str:
1444 """
1445 Return a string representation of this version number according to the format specification.
1447 :param formatSpec: The format specification, using ``%``-placeholders for the version's parts.
1448 :returns: Formatted version number.
1449 :raises ValueError: If the format specification contains an unknown placeholder.
1450 """
1451 result = self._format(formatSpec)
1453 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1454 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1456 return result.replace("%%", "%")
1458 def __repr__(self) -> str:
1459 """
1460 Return a normalized string representation of this version number.
1462 .. note::
1464 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1465 :meth:`__str__` to render a version number including its prefix.
1467 :returns: Raw version number representation without a prefix.
1468 """
1469 return f"{self._major}.{self._minor}.{self._micro}"
1471 def __str__(self) -> str:
1472 """
1473 Return a string representation of this version number.
1475 :returns: Version number representation.
1476 """
1477 result = self._prefix if Parts.Prefix in self._parts else ""
1478 result += f"{self._major}" # major is always present
1479 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1480 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1481 result += f".{self._build}" if Parts.Build in self._parts else ""
1482 if self._releaseLevel is ReleaseLevel.Development:
1483 result += "-dev"
1484 elif self._releaseLevel is ReleaseLevel.Alpha:
1485 result += f".alpha{self._releaseNumber}"
1486 elif self._releaseLevel is ReleaseLevel.Beta:
1487 result += f".beta{self._releaseNumber}"
1488 elif self._releaseLevel is ReleaseLevel.Gamma: 1488 ↛ 1489line 1488 didn't jump to line 1489 because the condition on line 1488 was never true
1489 result += f".gamma{self._releaseNumber}"
1490 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1491 result += f".rc{self._releaseNumber}"
1492 result += f".post{self._post}" if Parts.Post in self._parts else ""
1493 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1494 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1496 return result
1499@export
1500class PythonVersion(SemanticVersion):
1501 """
1502 Represents a Python version.
1503 """
1505 @classmethod
1506 def FromSysVersionInfo(cls) -> PythonVersion:
1507 """
1508 Create a Python version from :data:`sys.version_info`.
1510 :returns: A PythonVersion instance of the current Python interpreter's version.
1511 :raises ToolingException: If the interpreter reports a release level this class doesn't know.
1512 """
1513 from sys import version_info
1515 if version_info.releaselevel == "final":
1516 rl = ReleaseLevel.Final
1517 number = None
1518 else: # pragma: no cover
1519 number = version_info.serial
1521 if version_info.releaselevel == "alpha":
1522 rl = ReleaseLevel.Alpha
1523 elif version_info.releaselevel == "beta":
1524 rl = ReleaseLevel.Beta
1525 elif version_info.releaselevel == "candidate":
1526 rl = ReleaseLevel.ReleaseCandidate
1527 else: # pragma: no cover
1528 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1530 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1532 def __hash__(self) -> int:
1533 """
1534 Compute a hash for this version number.
1536 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1537 version unhashable.
1539 :returns: Hash of this version number.
1540 """
1541 return super().__hash__()
1543 def __str__(self) -> str:
1544 """
1545 Return a string representation of this version number.
1547 :returns: Version number representation.
1548 """
1549 result = self._prefix if Parts.Prefix in self._parts else ""
1550 result += f"{self._major}" # major is always present
1551 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1552 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1553 if self._releaseLevel is ReleaseLevel.Alpha: 1553 ↛ 1554line 1553 didn't jump to line 1554 because the condition on line 1553 was never true
1554 result += f"a{self._releaseNumber}"
1555 elif self._releaseLevel is ReleaseLevel.Beta: 1555 ↛ 1556line 1555 didn't jump to line 1556 because the condition on line 1555 was never true
1556 result += f"b{self._releaseNumber}"
1557 elif self._releaseLevel is ReleaseLevel.Gamma: 1557 ↛ 1558line 1557 didn't jump to line 1558 because the condition on line 1557 was never true
1558 result += f"c{self._releaseNumber}"
1559 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: 1559 ↛ 1560line 1559 didn't jump to line 1560 because the condition on line 1559 was never true
1560 result += f"rc{self._releaseNumber}"
1561 result += f".post{self._post}" if Parts.Post in self._parts else ""
1562 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1563 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1565 return result
1568@export
1569class CalendarVersion(Version):
1570 """Representation of a calendar version number like ``2021.10``."""
1572 _PARTCOUNT: ClassVar[int] = 3 #: Number of numeric parts a version number of this class can carry.
1574 _PATTERN: ClassVar[Pattern] = re_compile(
1575 r"^"
1576 r"(?P<prefix>rev|REV|[vViIrR])?"
1577 r"(?P<major>\d+)"
1578 r"(?:\.(?P<minor>\d+))?"
1579 r"(?:\.(?P<micro>\d+))?"
1580 r"$"
1581 ) #: Regular expression to parse a calendar version from a string.
1583 def __init__(
1584 self,
1585 major: int,
1586 minor: Nullable[int] = None,
1587 micro: Nullable[int] = None,
1588 build: Nullable[int] = None,
1589 flags: Flags = Flags.Clean,
1590 prefix: Nullable[str] = None,
1591 postfix: Nullable[str] = None
1592 ) -> None:
1593 """
1594 Initializes a calendar version number representation.
1596 :param major: Major number part of the version number.
1597 :param minor: Optional, minor number part of the version number.
1598 :param micro: Optional, micro (patch) number part of the version number.
1599 :param build: Optional, build number part of the version number.
1600 :param flags: Optional, the version number's flags.
1601 :param prefix: Optional, the version number's prefix.
1602 :param postfix: Optional, the version number's postfix.
1603 :raises TypeError: If parameter 'major' is not of type integer.
1604 :raises ValueError: If parameter 'major' is a negative number.
1605 :raises TypeError: If parameter 'minor' is not of type integer.
1606 :raises ValueError: If parameter 'minor' is a negative number.
1607 :raises TypeError: If parameter 'micro' is not of type integer.
1608 :raises ValueError: If parameter 'micro' is a negative number.
1609 :raises TypeError: If parameter 'build' is not of type integer.
1610 :raises ValueError: If parameter 'build' is a negative number.
1611 :raises TypeError: If parameter 'prefix' is not of type string.
1612 :raises TypeError: If parameter 'postfix' is not of type string.
1613 """
1614 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1616 @classmethod
1617 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[[CalendarVersion], bool]] = None) -> CalendarVersion:
1618 """
1619 Parse a version string and return a :class:`CalendarVersion` instance.
1621 Allowed prefix characters:
1623 * ``v|V`` - version, public version, public release
1624 * ``i|I`` - internal version, internal release
1625 * ``r|R`` - release, revision
1626 * ``rev|REV`` - revision
1628 A version number carries up to :attr:`_PARTCOUNT` numeric parts. :class:`YearMonthVersion`,
1629 :class:`YearWeekVersion` and :class:`YearReleaseVersion` describe two parts, so a third part is rejected for
1630 them.
1632 :param versionString: The version string to parse.
1633 :param validator: Optional, a validation function.
1634 :returns: An object representing a calendar version.
1635 :raises TypeError: If parameter ``versionString`` is not a string.
1636 :raises ValueError: If parameter ``versionString`` is None or empty.
1637 :raises ValueError: If parameter ``versionString`` isn't a calendar version number. |br|
1638 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g.
1639 ``v2024.04``.
1640 :raises ValueError: If parameter ``versionString`` has more parts than the class describes. |br|
1641 Use :class:`CalendarVersion` or :class:`YearMonthDayVersion` to parse a
1642 three-part calendar version number.
1643 :raises VersionValidatorException: If the parsed version is rejected by ``validator``.
1644 """
1645 if versionString is None:
1646 raise ValueError("Parameter 'versionString' is None.")
1647 elif not isinstance(versionString, str):
1648 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1649 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1650 raise ex
1651 elif (versionString := versionString.strip()) == "":
1652 raise ValueError("Parameter 'versionString' is empty.")
1654 if (match := cls._PATTERN.match(versionString)) is None:
1655 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1656 ex.add_note(f"A calendar version number is made of up to {cls._PARTCOUNT} numeric parts, e.g. '2024.04'.")
1657 ex.add_note(f"It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v2024.04'.")
1658 raise ex
1660 prefix = match["prefix"]
1661 minor = match["minor"]
1662 micro = match["micro"]
1664 if micro is not None and cls._PARTCOUNT < 3:
1665 ex = ValueError(f"Version number '{versionString}' has 3 parts, but '{cls.__name__}' describes {cls._PARTCOUNT}.")
1666 ex.add_note(f"Use 'CalendarVersion' or 'YearMonthDayVersion' to parse a 3-part calendar version number.")
1667 raise ex
1669 numbers = [int(match["major"])]
1670 if minor is not None:
1671 numbers.append(int(minor))
1672 if micro is not None:
1673 numbers.append(int(micro))
1675 version = cls(*numbers, flags=Flags.Clean, prefix=prefix if prefix != "" else None)
1677 if validator is not None and not validator(version):
1678 raise VersionValidatorException(f"Failed to validate version string '{versionString}'.", version=version)
1680 return version
1682 @readonly
1683 def Year(self) -> int:
1684 """
1685 Read-only property to access the year part.
1687 :returns: The year part.
1688 """
1689 return self._major
1691 def _equal(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]:
1692 """
1693 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1695 :param left: Left parameter.
1696 :param right: Right parameter.
1697 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1698 """
1699 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1701 def _compare(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]:
1702 """
1703 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1705 :param left: Left parameter.
1706 :param right: Right parameter.
1707 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1708 False if ``left`` is greater than ``right``. |br|
1709 Otherwise it's None (both parameters are equal).
1710 """
1711 if left._major < right._major:
1712 return True
1713 elif left._major > right._major:
1714 return False
1716 if left._minor < right._minor:
1717 return True
1718 elif left._minor > right._minor:
1719 return False
1721 if left._micro < right._micro: 1721 ↛ 1722line 1721 didn't jump to line 1722 because the condition on line 1721 was never true
1722 return True
1723 elif left._micro > right._micro: 1723 ↛ 1724line 1723 didn't jump to line 1724 because the condition on line 1723 was never true
1724 return False
1726 return None
1728 def __eq__(self, other: Any) -> bool:
1729 """
1730 Compare two version numbers for equality.
1732 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1733 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1734 number is assumed (all other parts are zero).
1736 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1737 number.
1739 :param other: Parameter to compare against.
1740 :returns: ``True``, if both version numbers are equal.
1741 :raises ValueError: If parameter ``other`` is None.
1742 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1743 """
1744 return super().__eq__(other)
1746 def __ne__(self, other: Any) -> bool:
1747 """
1748 Compare two version numbers for inequality.
1750 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1751 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1752 number is assumed (all other parts are zero).
1754 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1755 number.
1757 :param other: Parameter to compare against.
1758 :returns: ``True``, if both version numbers are not equal.
1759 :raises ValueError: If parameter ``other`` is None.
1760 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1761 """
1762 return super().__ne__(other)
1764 def __lt__(self, other: Any) -> bool:
1765 """
1766 Compare two version numbers if the version is less than the second operand.
1768 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1769 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1770 number is assumed (all other parts are zero).
1772 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1773 number.
1775 :param other: Parameter to compare against.
1776 :returns: ``True``, if version is less than the second operand.
1777 :raises ValueError: If parameter ``other`` is None.
1778 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1779 """
1780 return super().__lt__(other)
1782 def __le__(self, other: Any) -> bool:
1783 """
1784 Compare two version numbers if the version is less than or equal the second operand.
1786 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1787 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1788 number is assumed (all other parts are zero).
1790 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1791 number.
1793 :param other: Parameter to compare against.
1794 :returns: ``True``, if version is less than or equal the second operand.
1795 :raises ValueError: If parameter ``other`` is None.
1796 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1797 """
1798 return super().__le__(other)
1800 def __gt__(self, other: Any) -> bool:
1801 """
1802 Compare two version numbers if the version is greater than the second operand.
1804 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1805 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1806 number is assumed (all other parts are zero).
1808 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1809 number.
1811 :param other: Parameter to compare against.
1812 :returns: ``True``, if version is greater than the second operand.
1813 :raises ValueError: If parameter ``other`` is None.
1814 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1815 """
1816 return super().__gt__(other)
1818 def __ge__(self, other: Any) -> bool:
1819 """
1820 Compare two version numbers if the version is greater than or equal the second operand.
1822 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1823 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1824 number is assumed (all other parts are zero).
1826 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1827 number.
1829 :param other: Parameter to compare against.
1830 :returns: ``True``, if version is greater than or equal the second operand.
1831 :raises ValueError: If parameter ``other`` is None.
1832 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
1833 """
1834 return super().__ge__(other)
1836 def __hash__(self) -> int:
1837 """
1838 Compute a hash for this version number.
1840 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1841 version unhashable.
1843 :returns: Hash of this version number.
1844 """
1845 return super().__hash__()
1847 def __format__(self, formatSpec: str) -> str:
1848 """
1849 Return a string representation of this version number according to the format specification.
1851 .. topic:: Format Specifiers
1853 * ``%M`` - major number (year)
1854 * ``%m`` - minor number (month/week)
1855 * ``%u`` - micro number (day)
1857 :param formatSpec: The format specification.
1858 :returns: Formatted version number.
1859 """
1860 if formatSpec == "":
1861 return self.__str__()
1863 result = formatSpec
1864 # result = result.replace("%P", str(self._prefix))
1865 result = result.replace("%M", str(self._major))
1866 result = result.replace("%m", str(self._minor))
1867 result = result.replace("%u", str(self._micro))
1868 # result = result.replace("%p", str(self._pre))
1870 return result.replace("%%", "%")
1872 def __repr__(self) -> str:
1873 """
1874 Return a normalized string representation of this version number.
1876 .. note::
1878 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1879 :meth:`__str__` to render a version number including its prefix.
1881 :returns: Raw version number representation without a prefix.
1882 """
1883 result = f"{self._major}.{self._minor}"
1884 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1886 return result
1888 def __str__(self) -> str:
1889 """
1890 Return a string representation of this version number with only the present parts.
1892 :returns: Version number representation including a prefix.
1893 """
1894 result = self._prefix if Parts.Prefix in self._parts else ""
1895 result += f"{self._major}"
1896 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1897 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1899 return result
1902@export
1903class YearMonthVersion(CalendarVersion):
1904 """Representation of a calendar version number made of year and month like ``2021.10``."""
1906 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and month.
1908 def __init__(
1909 self,
1910 year: int,
1911 month: Nullable[int] = None,
1912 build: Nullable[int] = None,
1913 flags: Flags = Flags.Clean,
1914 prefix: Nullable[str] = None,
1915 postfix: Nullable[str] = None
1916 ) -> None:
1917 """
1918 Initializes a year-month version number representation.
1920 :param year: Year part of the version number.
1921 :param month: Optional, month part of the version number.
1922 :param build: Optional, build number part of the version number.
1923 :param flags: Optional, the version number's flags.
1924 :param prefix: Optional, the version number's prefix.
1925 :param postfix: Optional, the version number's postfix.
1926 :raises TypeError: If parameter 'major' is not of type integer.
1927 :raises ValueError: If parameter 'major' is a negative number.
1928 :raises TypeError: If parameter 'minor' is not of type integer.
1929 :raises ValueError: If parameter 'minor' is a negative number.
1930 :raises TypeError: If parameter 'micro' is not of type integer.
1931 :raises ValueError: If parameter 'micro' is a negative number.
1932 :raises TypeError: If parameter 'build' is not of type integer.
1933 :raises ValueError: If parameter 'build' is a negative number.
1934 :raises TypeError: If parameter 'prefix' is not of type string.
1935 :raises TypeError: If parameter 'postfix' is not of type string.
1936 """
1937 super().__init__(year, month, None, build, flags, prefix, postfix)
1939 @readonly
1940 def Month(self) -> int:
1941 """
1942 Read-only property to access the month part.
1944 :returns: The month part.
1945 """
1946 return self._minor
1948 def __hash__(self) -> int:
1949 """
1950 Compute a hash for this version number.
1952 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1953 version unhashable.
1955 :returns: Hash of this version number.
1956 """
1957 return super().__hash__()
1960@export
1961class YearWeekVersion(CalendarVersion):
1962 """Representation of a calendar version number made of year and week like ``2021.47``."""
1964 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and week.
1966 def __init__(
1967 self,
1968 year: int,
1969 week: Nullable[int] = None,
1970 build: Nullable[int] = None,
1971 flags: Flags = Flags.Clean,
1972 prefix: Nullable[str] = None,
1973 postfix: Nullable[str] = None
1974 ) -> None:
1975 """
1976 Initializes a year-week version number representation.
1978 :param year: Year part of the version number.
1979 :param week: Optional, week part of the version number.
1980 :param build: Optional, build number part of the version number.
1981 :param flags: Optional, the version number's flags.
1982 :param prefix: Optional, the version number's prefix.
1983 :param postfix: Optional, the version number's postfix.
1984 :raises TypeError: If parameter 'major' is not of type integer.
1985 :raises ValueError: If parameter 'major' is a negative number.
1986 :raises TypeError: If parameter 'minor' is not of type integer.
1987 :raises ValueError: If parameter 'minor' is a negative number.
1988 :raises TypeError: If parameter 'micro' is not of type integer.
1989 :raises ValueError: If parameter 'micro' is a negative number.
1990 :raises TypeError: If parameter 'build' is not of type integer.
1991 :raises ValueError: If parameter 'build' is a negative number.
1992 :raises TypeError: If parameter 'prefix' is not of type string.
1993 :raises TypeError: If parameter 'postfix' is not of type string.
1994 """
1995 super().__init__(year, week, None, build, flags, prefix, postfix)
1997 @readonly
1998 def Week(self) -> int:
1999 """
2000 Read-only property to access the week part.
2002 :returns: The week part.
2003 """
2004 return self._minor
2006 def __hash__(self) -> int:
2007 """
2008 Compute a hash for this version number.
2010 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2011 version unhashable.
2013 :returns: Hash of this version number.
2014 """
2015 return super().__hash__()
2018@export
2019class YearReleaseVersion(CalendarVersion):
2020 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
2022 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and release.
2024 def __init__(
2025 self,
2026 year: int,
2027 release: Nullable[int] = None,
2028 build: Nullable[int] = None,
2029 flags: Flags = Flags.Clean,
2030 prefix: Nullable[str] = None,
2031 postfix: Nullable[str] = None
2032 ) -> None:
2033 """
2034 Initializes a year-release version number representation.
2036 :param year: Year part of the version number.
2037 :param release: Optional, release number of the version number.
2038 :param build: Optional, build number part of the version number.
2039 :param flags: Optional, the version number's flags.
2040 :param prefix: Optional, the version number's prefix.
2041 :param postfix: Optional, the version number's postfix.
2042 :raises TypeError: If parameter 'major' is not of type integer.
2043 :raises ValueError: If parameter 'major' is a negative number.
2044 :raises TypeError: If parameter 'minor' is not of type integer.
2045 :raises ValueError: If parameter 'minor' is a negative number.
2046 :raises TypeError: If parameter 'micro' is not of type integer.
2047 :raises ValueError: If parameter 'micro' is a negative number.
2048 :raises TypeError: If parameter 'build' is not of type integer.
2049 :raises ValueError: If parameter 'build' is a negative number.
2050 :raises TypeError: If parameter 'prefix' is not of type string.
2051 :raises TypeError: If parameter 'postfix' is not of type string.
2052 """
2053 super().__init__(year, release, None, build, flags, prefix, postfix)
2055 @readonly
2056 def Release(self) -> int:
2057 """
2058 Read-only property to access the release number.
2060 :returns: The release number.
2061 """
2062 return self._minor
2064 def __hash__(self) -> int:
2065 """
2066 Compute a hash for this version number.
2068 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2069 version unhashable.
2071 :returns: Hash of this version number.
2072 """
2073 return super().__hash__()
2076@export
2077class YearMonthDayVersion(CalendarVersion):
2078 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
2080 def __init__(
2081 self,
2082 year: int,
2083 month: Nullable[int] = None,
2084 day: Nullable[int] = None,
2085 build: Nullable[int] = None,
2086 flags: Flags = Flags.Clean,
2087 prefix: Nullable[str] = None,
2088 postfix: Nullable[str] = None
2089 ) -> None:
2090 """
2091 Initializes a year-month-day version number representation.
2093 :param year: Year part of the version number.
2094 :param month: Optional, month part of the version number.
2095 :param day: Optional, day part of the version number.
2096 :param build: Optional, build number part of the version number.
2097 :param flags: Optional, the version number's flags.
2098 :param prefix: Optional, the version number's prefix.
2099 :param postfix: Optional, the version number's postfix.
2100 :raises TypeError: If parameter 'major' is not of type integer.
2101 :raises ValueError: If parameter 'major' is a negative number.
2102 :raises TypeError: If parameter 'minor' is not of type integer.
2103 :raises ValueError: If parameter 'minor' is a negative number.
2104 :raises TypeError: If parameter 'micro' is not of type integer.
2105 :raises ValueError: If parameter 'micro' is a negative number.
2106 :raises TypeError: If parameter 'build' is not of type integer.
2107 :raises ValueError: If parameter 'build' is a negative number.
2108 :raises TypeError: If parameter 'prefix' is not of type string.
2109 :raises TypeError: If parameter 'postfix' is not of type string.
2110 """
2111 super().__init__(year, month, day, build, flags, prefix, postfix)
2113 @readonly
2114 def Month(self) -> int:
2115 """
2116 Read-only property to access the month part.
2118 :returns: The month part.
2119 """
2120 return self._minor
2122 @readonly
2123 def Day(self) -> int:
2124 """
2125 Read-only property to access the day part.
2127 :returns: The day part.
2128 """
2129 return self._micro
2131 def __hash__(self) -> int:
2132 """
2133 Compute a hash for this version number.
2135 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2136 version unhashable.
2138 :returns: Hash of this version number.
2139 """
2140 return super().__hash__()
2143V = TypeVar("V", bound=Version)
2145@export
2146class RangeBoundHandling(Flag):
2147 """
2148 A flag defining how to handle bounds in a range.
2150 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
2151 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
2152 """
2153 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
2154 LowerBoundInclusive = 0 #: Lower bound is inclusive.
2155 UpperBoundInclusive = 0 #: Upper bound is inclusive.
2156 LowerBoundExclusive = 1 #: Lower bound is exclusive.
2157 UpperBoundExclusive = 2 #: Upper bound is exclusive.
2158 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
2161@export
2162class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
2163 """
2164 Representation of a version range described by a lower bound and upper bound version.
2166 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2167 """
2168 _lowerBound: V #: Lower bound of the version range.
2169 _upperBound: V #: Upper bound of the version range.
2170 _boundHandling: RangeBoundHandling #: Strategy deciding whether the bounds are part of the range.
2172 def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None:
2173 """
2174 Initializes a version range described by a lower and upper bound.
2176 :param lowerBound: lowest version (inclusive).
2177 :param upperBound: hightest version (inclusive).
2178 :param boundHandling: Optional, strategy deciding whether the bounds are part of the range.
2179 :raises TypeError: If parameter ``lowerBound`` is not of type :class:`Version`.
2180 :raises TypeError: If parameter ``upperBound`` is not of type :class:`Version`.
2181 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
2182 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
2183 """
2184 if not isinstance(lowerBound, Version):
2185 ex = TypeError(f"Parameter 'lowerBound' is not of type 'Version'.")
2186 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
2187 raise ex
2189 if not isinstance(upperBound, Version):
2190 ex = TypeError(f"Parameter 'upperBound' is not of type 'Version'.")
2191 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
2192 raise ex
2194 if not ((lBC := lowerBound.__class__) is (uBC := upperBound.__class__) or issubclass(lBC, uBC) or issubclass(uBC, lBC)):
2195 ex = TypeError(f"Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
2196 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and type '{getFullyQualifiedName(upperBound)}' for upperBound.")
2197 raise ex
2199 if not (lowerBound <= upperBound):
2200 ex = ValueError(f"Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
2201 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
2202 raise ex
2204 self._lowerBound = lowerBound
2205 self._upperBound = upperBound
2206 self._boundHandling = boundHandling
2208 @property
2209 def LowerBound(self) -> V:
2210 """
2211 Property to access the range's lower bound.
2213 :returns: Lower bound of the version range.
2214 :raises TypeError: If an assigned value is not of type :class:`Version`.
2215 """
2216 return self._lowerBound
2218 @LowerBound.setter
2219 def LowerBound(self, value: V) -> None:
2220 if not isinstance(value, Version):
2221 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
2222 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2223 raise ex
2225 self._lowerBound = value
2227 @property
2228 def UpperBound(self) -> V:
2229 """
2230 Property to access the range's upper bound.
2232 :returns: Upper bound of the version range.
2233 :raises TypeError: If an assigned value is not of type :class:`Version`.
2234 """
2235 return self._upperBound
2237 @UpperBound.setter
2238 def UpperBound(self, value: V) -> None:
2239 if not isinstance(value, Version):
2240 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
2241 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2242 raise ex
2244 self._upperBound = value
2246 @property
2247 def BoundHandling(self) -> RangeBoundHandling:
2248 """
2249 Property to access the range's bound handling strategy.
2251 :returns: The range's bound handling strategy.
2252 :raises TypeError: If an assigned value is not of type :class:`RangeBoundHandling`.
2253 """
2254 return self._boundHandling
2256 @BoundHandling.setter
2257 def BoundHandling(self, value: RangeBoundHandling) -> None:
2258 if not isinstance(value, RangeBoundHandling):
2259 ex = TypeError(f"Parameter 'value' is not of type 'RangeBoundHandling'.")
2260 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2261 raise ex
2263 self._boundHandling = value
2265 def __and__(self, other: Any) -> VersionRange[T]:
2266 """
2267 Compute the intersection of two version ranges.
2269 :param other: Second version range to intersect with.
2270 :returns: Intersected version range.
2271 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2272 :raises ValueError: If intersection is empty.
2273 """
2274 if not isinstance(other, VersionRange): 2274 ↛ 2275line 2274 didn't jump to line 2275 because the condition on line 2274 was never true
2275 ex = TypeError(f"Parameter 'other' is not of type 'VersionRange'.")
2276 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2277 raise ex
2279 if not (isinstance(other._lowerBound, self._lowerBound.__class__) and isinstance(self._lowerBound, other._lowerBound.__class__)): 2279 ↛ 2280line 2279 didn't jump to line 2280 because the condition on line 2279 was never true
2280 ex = TypeError(f"Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible with each other.")
2281 ex.add_note(
2282 f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2283 raise ex
2285 if other._lowerBound < self._lowerBound:
2286 lBound = self._lowerBound
2287 elif other._lowerBound in self:
2288 lBound = other._lowerBound
2289 else:
2290 ex = ValueError("The intersection of both version ranges is empty.")
2291 ex.add_note(f"Got value '{other._lowerBound}' for other's lower bound.")
2292 ex.add_note(f"This range's upper bound is '{self._upperBound}'.")
2293 raise ex
2295 if other._upperBound > self._upperBound:
2296 uBound = self._upperBound
2297 elif other._upperBound in self:
2298 uBound = other._upperBound
2299 else:
2300 ex = ValueError("The intersection of both version ranges is empty.")
2301 ex.add_note(f"Got value '{other._upperBound}' for other's upper bound.")
2302 ex.add_note(f"This range's lower bound is '{self._lowerBound}'.")
2303 raise ex
2305 return self.__class__(lBound, uBound)
2307 def __lt__(self, other: Any) -> bool:
2308 """
2309 Compare a version range and a version numbers if the version range is less than the second operand (version).
2311 :param other: Operand to compare against.
2312 :returns: ``True``, if version range is less than the second operand (version).
2313 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2314 """
2315 # TODO: support VersionRange < VersionRange too
2316 # TODO: support str, int, ... like Version ?
2317 if not isinstance(other, Version):
2318 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2319 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2320 raise ex
2322 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2322 ↛ 2323line 2322 didn't jump to line 2323 because the condition on line 2322 was never true
2323 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2324 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2325 raise ex
2327 return self._upperBound < other
2329 def __le__(self, other: Any) -> bool:
2330 """
2331 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2333 :param other: Operand to compare against.
2334 :returns: ``True``, if version range is less than or equal the second operand (version).
2335 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2336 """
2337 # TODO: support VersionRange < VersionRange too
2338 # TODO: support str, int, ... like Version ?
2339 if not isinstance(other, Version): 2339 ↛ 2340line 2339 didn't jump to line 2340 because the condition on line 2339 was never true
2340 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2341 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2342 raise ex
2344 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2344 ↛ 2345line 2344 didn't jump to line 2345 because the condition on line 2344 was never true
2345 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2346 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2347 raise ex
2349 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2350 return self._upperBound < other
2351 else:
2352 return self._upperBound <= other
2354 def __gt__(self, other: Any) -> bool:
2355 """
2356 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2358 :param other: Operand to compare against.
2359 :returns: ``True``, if version range is greater than the second operand (version).
2360 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2361 """
2362 # TODO: support VersionRange < VersionRange too
2363 # TODO: support str, int, ... like Version ?
2364 if not isinstance(other, Version): 2364 ↛ 2365line 2364 didn't jump to line 2365 because the condition on line 2364 was never true
2365 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2366 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2367 raise ex
2369 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2369 ↛ 2370line 2369 didn't jump to line 2370 because the condition on line 2369 was never true
2370 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2371 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2372 raise ex
2374 return self._lowerBound > other
2376 def __ge__(self, other: Any) -> bool:
2377 """
2378 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2380 :param other: Operand to compare against.
2381 :returns: ``True``, if version range is greater than or equal the second operand (version).
2382 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2383 """
2384 # TODO: support VersionRange < VersionRange too
2385 # TODO: support str, int, ... like Version ?
2386 if not isinstance(other, Version): 2386 ↛ 2387line 2386 didn't jump to line 2387 because the condition on line 2386 was never true
2387 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2388 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2389 raise ex
2391 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2391 ↛ 2392line 2391 didn't jump to line 2392 because the condition on line 2391 was never true
2392 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2393 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2394 raise ex
2396 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2396 ↛ 2397line 2396 didn't jump to line 2397 because the condition on line 2396 was never true
2397 return self._lowerBound > other
2398 else:
2399 return self._lowerBound >= other
2401 def __contains__(self, version: Version) -> bool:
2402 """
2403 Check if the version is in the version range.
2405 :param version: Optional, version to check.
2406 :returns: ``True``, if version is in range.
2407 :raises TypeError: If parameter ``version`` is not of type :class:`Version`.
2408 """
2409 if not isinstance(version, Version): 2409 ↛ 2410line 2409 didn't jump to line 2410 because the condition on line 2409 was never true
2410 ex = TypeError(f"Parameter 'item' is not of type 'Version'.")
2411 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2412 raise ex
2414 if self._boundHandling is RangeBoundHandling.BothBoundsInclusive: 2414 ↛ 2416line 2414 didn't jump to line 2416 because the condition on line 2414 was always true
2415 return self._lowerBound <= version <= self._upperBound
2416 elif self._boundHandling is (RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive):
2417 return self._lowerBound <= version < self._upperBound
2418 elif self._boundHandling is (RangeBoundHandling.LowerBoundExclusive | RangeBoundHandling.UpperBoundInclusive):
2419 return self._lowerBound < version <= self._upperBound
2420 else:
2421 return self._lowerBound < version < self._upperBound
2424@export
2425class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2426 """
2427 Representation of an ordered set of versions.
2429 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2430 """
2431 _items: list[V] #: An ordered list of set members.
2433 def __init__(self, versions: Union[Version, Iterable[V]]) -> None:
2434 """
2435 Initializes a version set either by a single version or an iterable of versions.
2437 :param versions: A single version or an iterable of versions.
2438 :raises ValueError: If parameter ``versions`` is None`.
2439 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2440 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2441 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2442 """
2443 if versions is None:
2444 raise ValueError(f"Parameter 'versions' is None.")
2446 if isinstance(versions, Version):
2447 self._items = [versions]
2448 elif isinstance(versions, abc_Iterable): 2448 ↛ 2466line 2448 didn't jump to line 2466 because the condition on line 2448 was always true
2449 iterator = iter(versions)
2450 try:
2451 firstVersion = next(iterator)
2452 except StopIteration:
2453 self._items = []
2454 return
2456 if not isinstance(firstVersion, Version): 2456 ↛ 2457line 2456 didn't jump to line 2457 because the condition on line 2456 was never true
2457 raise TypeError(f"First element in parameter 'versions' is not of type Version.")
2459 baseType = firstVersion.__class__
2460 for version in iterator:
2461 if not isinstance(version, baseType):
2462 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2464 self._items = list(sorted(versions))
2465 else:
2466 raise TypeError(f"Parameter 'versions' is not an Iterable.")
2468 def __and__(self, other: VersionSet[V]) -> VersionSet[T]:
2469 """
2470 Compute intersection of two version sets.
2472 :param other: Second set of versions.
2473 :returns: Intersection of two version sets.
2474 """
2475 selfIterator = self.__iter__()
2476 otherIterator = other.__iter__()
2478 result = []
2479 try:
2480 selfValue = next(selfIterator)
2481 otherValue = next(otherIterator)
2483 while True:
2484 if selfValue < otherValue:
2485 selfValue = next(selfIterator)
2486 elif otherValue < selfValue:
2487 otherValue = next(otherIterator)
2488 else:
2489 result.append(selfValue)
2490 selfValue = next(selfIterator)
2491 otherValue = next(otherIterator)
2493 except StopIteration:
2494 pass
2496 return VersionSet(result)
2498 def __or__(self, other: VersionSet[V]) -> VersionSet[T]:
2499 """
2500 Compute union of two version sets.
2502 :param other: Second set of versions.
2503 :returns: Union of two version sets.
2504 """
2505 selfIterator = self.__iter__()
2506 otherIterator = other.__iter__()
2508 result = []
2509 try:
2510 selfValue = next(selfIterator)
2511 except StopIteration:
2512 for otherValue in otherIterator:
2513 result.append(otherValue)
2515 try:
2516 otherValue = next(otherIterator)
2517 except StopIteration:
2518 for selfValue in selfIterator:
2519 result.append(selfValue)
2521 while True:
2522 if selfValue < otherValue:
2523 result.append(selfValue)
2524 try:
2525 selfValue = next(selfIterator)
2526 except StopIteration:
2527 result.append(otherValue)
2528 for otherValue in otherIterator: 2528 ↛ 2529line 2528 didn't jump to line 2529 because the loop on line 2528 never started
2529 result.append(otherValue)
2531 break
2532 elif otherValue < selfValue:
2533 result.append(otherValue)
2534 try:
2535 otherValue = next(otherIterator)
2536 except StopIteration:
2537 result.append(selfValue)
2538 for selfValue in selfIterator:
2539 result.append(selfValue)
2541 break
2542 else:
2543 result.append(selfValue)
2544 try:
2545 selfValue = next(selfIterator)
2546 except StopIteration:
2547 for otherValue in otherIterator: 2547 ↛ 2548line 2547 didn't jump to line 2548 because the loop on line 2547 never started
2548 result.append(otherValue)
2550 break
2552 try:
2553 otherValue = next(otherIterator)
2554 except StopIteration:
2555 for selfValue in selfIterator:
2556 result.append(selfValue)
2558 break
2560 return VersionSet(result)
2562 def __lt__(self, other: Any) -> bool:
2563 """
2564 Compare a version set and a version numbers if the version set is less than the second operand (version).
2566 :param other: Operand to compare against.
2567 :returns: ``True``, if version set is less than the second operand (version).
2568 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2569 """
2570 # TODO: support VersionRange < VersionRange too
2571 # TODO: support str, int, ... like Version ?
2572 if not isinstance(other, Version):
2573 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2574 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2575 raise ex
2577 return self._items[-1] < other
2579 def __le__(self, other: Any) -> bool:
2580 """
2581 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
2583 :param other: Operand to compare against.
2584 :returns: ``True``, if version set is less than or equal the second operand (version).
2585 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2586 """
2587 # TODO: support VersionRange < VersionRange too
2588 # TODO: support str, int, ... like Version ?
2589 if not isinstance(other, Version): 2589 ↛ 2590line 2589 didn't jump to line 2590 because the condition on line 2589 was never true
2590 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2591 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2592 raise ex
2594 return self._items[-1] <= other
2596 def __gt__(self, other: Any) -> bool:
2597 """
2598 Compare a version set and a version numbers if the version set is greater than the second operand (version).
2600 :param other: Operand to compare against.
2601 :returns: ``True``, if version set is greater than the second operand (version).
2602 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2603 """
2604 # TODO: support VersionRange < VersionRange too
2605 # TODO: support str, int, ... like Version ?
2606 if not isinstance(other, Version): 2606 ↛ 2607line 2606 didn't jump to line 2607 because the condition on line 2606 was never true
2607 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2608 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2609 raise ex
2611 return self._items[0] > other
2613 def __ge__(self, other: Any) -> bool:
2614 """
2615 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
2617 :param other: Operand to compare against.
2618 :returns: ``True``, if version set is greater than or equal the second operand (version).
2619 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2620 """
2621 # TODO: support VersionRange < VersionRange too
2622 # TODO: support str, int, ... like Version ?
2623 if not isinstance(other, Version): 2623 ↛ 2624line 2623 didn't jump to line 2624 because the condition on line 2623 was never true
2624 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2625 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2626 raise ex
2628 return self._items[0] >= other
2630 def __contains__(self, version: V) -> bool:
2631 """
2632 Checks if the version a member of the set.
2634 :param version: Optional, the version to check.
2635 :returns: ``True``, if the version is a member of the set.
2636 """
2637 return version in self._items
2639 def __len__(self) -> int:
2640 """
2641 Returns the number of members in the set.
2643 :returns: Number of set members.
2644 """
2645 return len(self._items)
2647 def __iter__(self) -> Iterator[V]:
2648 """
2649 Returns an iterator to iterate all versions of this set from lowest to highest.
2651 :returns: Iterator to iterate versions.
2652 """
2653 return self._items.__iter__()
2655 def __getitem__(self, index: int) -> V:
2656 """
2657 Access to a version of a set by index.
2659 :param index: The index of the version to access.
2660 :returns: The indexed version.
2662 .. hint::
2664 Versions are ordered from lowest to highest version number.
2665 """
2666 return self._items[index]