Coverage for pyTooling/Versioning/__init__.py: 84%
972 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-31 07:24 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Implementation of semantic and date versioning version-numbers.
34.. hint::
36 See :ref:`high-level help <VERSIONING>` for explanations and usage examples.
37"""
38from collections.abc import Iterable as abc_Iterable
39from enum import Flag, Enum
40from re import compile as re_compile, Pattern
41from typing import Optional as Nullable, Union, Callable, Any, ClassVar, Generic, TypeVar, Iterable, Iterator, List
43from pyTooling.Decorators import export, readonly
44from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
45from pyTooling.Exceptions import ToolingException
46from pyTooling.Common import getFullyQualifiedName
49@export
50class Parts(Flag):
51 """Enumeration describing parts of a version number that can be present."""
52 Unknown = 0 #: Undocumented
53 Major = 1 #: Major number is present. (e.g. X in ``vX.0.0``).
54 Year = 1 #: Year is present. (e.g. X in ``XXXX.10``).
55 Minor = 2 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
56 Month = 2 #: Month is present. (e.g. X in ``2024.YY``).
57 Week = 2 #: Week is present. (e.g. X in ``2024.YY``).
58 Micro = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
59 Patch = 4 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
60 Day = 4 #: Day is present. (e.g. X in ``2024.10.ZZ``).
61 Level = 8 #: Release level is present.
62 Dev = 16 #: Development part is present.
63 Build = 32 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
64 Post = 64 #: Post-release number is present.
65 Prefix = 128 #: Prefix is present.
66 Postfix = 256 #: Postfix is present.
67 Hash = 512 #: Hash is present.
68# AHead = 256
71@export
72class ReleaseLevel(Enum):
73 """Enumeration describing the version's maturity level."""
74 Final = 0 #:
75 ReleaseCandidate = -10 #:
76 Development = -20 #:
77 Gamma = -30 #:
78 Beta = -40 #:
79 Alpha = -50 #:
81 def __eq__(self, other: Any) -> bool:
82 """
83 Compare two release levels if the level is equal to the second operand.
85 :param other: Operand to compare against.
86 :returns: ``True``, if release level is equal the second operand's release level.
87 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
88 """
89 if isinstance(other, str): 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true
90 other = ReleaseLevel(other)
92 if not isinstance(other, ReleaseLevel): 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
94 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
95 raise ex
97 return self is other
99 def __ne__(self, other: Any) -> bool:
100 """
101 Compare two release levels if the level is unequal to the second operand.
103 :param other: Operand to compare against.
104 :returns: ``True``, if release level is unequal the second operand's release level.
105 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
106 """
107 if isinstance(other, str):
108 other = ReleaseLevel(other)
110 if not isinstance(other, ReleaseLevel):
111 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by != operator.")
112 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
113 raise ex
115 return self is not other
117 def __lt__(self, other: Any) -> bool:
118 """
119 Compare two release levels if the level is less than the second operand.
121 :param other: Operand to compare against.
122 :returns: ``True``, if release level is less than the second operand.
123 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
124 """
125 if isinstance(other, str): 125 ↛ 126line 125 didn't jump to line 126 because the condition on line 125 was never true
126 other = ReleaseLevel(other)
128 if not isinstance(other, ReleaseLevel): 128 ↛ 129line 128 didn't jump to line 129 because the condition on line 128 was never true
129 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
130 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
131 raise ex
133 return self.value < other.value
135 def __le__(self, other: Any) -> bool:
136 """
137 Compare two release levels if the level is less than or equal the second operand.
139 :param other: Operand to compare against.
140 :returns: ``True``, if release level is less than or equal the second operand.
141 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
142 """
143 if isinstance(other, str):
144 other = ReleaseLevel(other)
146 if not isinstance(other, ReleaseLevel):
147 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <=>= operator.")
148 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
149 raise ex
151 return self.value <= other.value
153 def __gt__(self, other: Any) -> bool:
154 """
155 Compare two release levels if the level is greater than the second operand.
157 :param other: Operand to compare against.
158 :returns: ``True``, if release level is greater than the second operand.
159 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
160 """
161 if isinstance(other, str): 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true
162 other = ReleaseLevel(other)
164 if not isinstance(other, ReleaseLevel): 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true
165 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
166 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
167 raise ex
169 return self.value > other.value
171 def __ge__(self, other: Any) -> bool:
172 """
173 Compare two release levels if the level is greater than or equal the second operand.
175 :param other: Operand to compare against.
176 :returns: ``True``, if release level is greater than or equal the second operand.
177 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or :class:`str`.
178 """
179 if isinstance(other, str):
180 other = ReleaseLevel(other)
182 if not isinstance(other, ReleaseLevel):
183 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
184 ex.add_note(f"Supported types for second operand: {self.__class__.__name__} or 'str'.")
185 raise ex
187 return self.value >= other.value
189 def __hash__(self) -> int:
190 return hash(self.value)
192 def __str__(self) -> str:
193 """
194 Returns the release level's string equivalent.
196 :returns: The string equivalent of the release level.
197 """
198 if self is ReleaseLevel.Final:
199 return "final"
200 elif self is ReleaseLevel.ReleaseCandidate:
201 return "rc"
202 elif self is ReleaseLevel.Development: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true
203 return "dev"
204 elif self is ReleaseLevel.Beta: 204 ↛ 205line 204 didn't jump to line 205 because the condition on line 204 was never true
205 return "beta"
206 elif self is ReleaseLevel.Alpha: 206 ↛ 209line 206 didn't jump to line 209 because the condition on line 206 was always true
207 return "alpha"
209 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
212@export
213class Flags(Flag):
214 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
215 NoVCS = 0 #: No Version Control System VCS
216 Clean = 1 #: A versioned build was created from a *clean* working directory.
217 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
219 CVS = 16 #: Concurrent Versions System (CVS)
220 SVN = 32 #: Subversion (SVN)
221 Git = 64 #: Git
222 Hg = 128 #: Mercurial (Hg)
225@export
226def WordSizeValidator(
227 bits: Nullable[int] = None,
228 majorBits: Nullable[int] = None,
229 minorBits: Nullable[int] = None,
230 microBits: Nullable[int] = None,
231 buildBits: Nullable[int] = None
232):
233 """
234 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
236 :param bits: Number of bits to encode any positive version number part.
237 :param majorBits: Number of bits to encode a positive major number in a version.
238 :param minorBits: Number of bits to encode a positive minor number in a version.
239 :param microBits: Number of bits to encode a positive micro number in a version.
240 :param buildBits: Number of bits to encode a positive build number in a version.
241 :returns: A validation function for Version instances.
242 """
243 majorMax = minorMax = microMax = buildMax = -1
244 if bits is not None:
245 majorMax = minorMax = microMax = buildMax = 2**bits - 1
247 if majorBits is not None:
248 majorMax = 2**majorBits - 1
249 if minorBits is not None:
250 minorMax = 2**minorBits - 1
251 if microBits is not None:
252 microMax = 2 ** microBits - 1
253 if buildBits is not None: 253 ↛ 254line 253 didn't jump to line 254 because the condition on line 253 was never true
254 buildMax = 2**buildBits - 1
256 def validator(version: SemanticVersion) -> bool:
257 if Parts.Major in version._parts and version._major > majorMax:
258 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
260 if Parts.Minor in version._parts and version._minor > minorMax:
261 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
263 if Parts.Micro in version._parts and version._micro > microMax:
264 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
266 if Parts.Build in version._parts and version._build > buildMax: 266 ↛ 267line 266 didn't jump to line 267 because the condition on line 266 was never true
267 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
269 return True
271 return validator
274@export
275def MaxValueValidator(
276 max: Nullable[int] = None,
277 majorMax: Nullable[int] = None,
278 minorMax: Nullable[int] = None,
279 microMax: Nullable[int] = None,
280 buildMax: Nullable[int] = None
281):
282 """
283 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
285 :param max: The upper bound for any positive version number part.
286 :param majorMax: The upper bound for the positive major number.
287 :param minorMax: The upper bound for the positive minor number.
288 :param microMax: The upper bound for the positive micro number.
289 :param buildMax: The upper bound for the positive build number.
290 :returns: A validation function for Version instances.
291 """
292 if max is not None: 292 ↛ 295line 292 didn't jump to line 295 because the condition on line 292 was always true
293 majorMax = minorMax = microMax = buildMax = max
295 def validator(version: SemanticVersion) -> bool:
296 if Parts.Major in version._parts and version._major > majorMax:
297 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
299 if Parts.Minor in version._parts and version._minor > minorMax:
300 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
302 if Parts.Micro in version._parts and version._micro > microMax:
303 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
305 if Parts.Build in version._parts and version._build > buildMax: 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true
306 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
308 return True
310 return validator
313@export
314class Version(metaclass=ExtendedType, slots=True):
315 """Base-class for a version representation."""
317 __hash: Nullable[int] #: once computed hash of the object
319 _parts: Parts #: Integer flag enumeration of present parts in a version number.
320 _prefix: str #: Prefix string
321 _major: int #: Major number part of the version number.
322 _minor: int #: Minor number part of the version number.
323 _micro: int #: Micro number part of the version number.
324 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
325 _releaseNumber: int #: Release number (Python calls this a serial).
326 _post: int #: Post-release version number part.
327 _dev: int #: Development number
328 _build: int #: Build number part of the version number.
329 _postfix: str #: Postfix string
330 _hash: str #: Hash from version control system.
331 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
333 def __init__(
334 self,
335 major: int,
336 minor: Nullable[int] = None,
337 micro: Nullable[int] = None,
338 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
339 number: Nullable[int] = None,
340 post: Nullable[int] = None,
341 dev: Nullable[int] = None,
342 *,
343 build: Nullable[int] = None,
344 postfix: Nullable[str] = None,
345 prefix: Nullable[str] = None,
346 hash: Nullable[str] = None,
347 flags: Flags = Flags.NoVCS
348 ) -> None:
349 """
350 Initializes a version number representation.
352 :param major: Major number part of the version number.
353 :param minor: Minor number part of the version number.
354 :param micro: Micro (patch) number part of the version number.
355 :param level: Release level (alpha, beta, release candidate, final, ...) of the version number.
356 :param number: Release number part (in combination with release level) of the version number.
357 :param post: Post number part of the version number.
358 :param dev: Development number part of the version number.
359 :param build: Build number part of the version number.
360 :param postfix: The version number's postfix.
361 :param prefix: The version number's prefix.
362 :param hash: Postfix string.
363 :param flags: The version number's flags.
364 :raises TypeError: If parameter 'major' is not of type int.
365 :raises ValueError: If parameter 'major' is a negative number.
366 :raises TypeError: If parameter 'minor' is not of type int.
367 :raises ValueError: If parameter 'minor' is a negative number.
368 :raises TypeError: If parameter 'micro' is not of type int.
369 :raises ValueError: If parameter 'micro' is a negative number.
370 :raises TypeError: If parameter 'build' is not of type int.
371 :raises ValueError: If parameter 'build' is a negative number.
372 :raises TypeError: If parameter 'prefix' is not of type str.
373 :raises TypeError: If parameter 'postfix' is not of type str.
374 """
375 self.__hash = None
377 if not isinstance(major, int):
378 raise TypeError("Parameter 'major' is not of type 'int'.")
379 elif major < 0:
380 raise ValueError("Parameter 'major' is negative.")
382 self._parts = Parts.Major
383 self._major = major
385 if minor is not None:
386 if not isinstance(minor, int):
387 raise TypeError("Parameter 'minor' is not of type 'int'.")
388 elif minor < 0:
389 raise ValueError("Parameter 'minor' is negative.")
391 self._parts |= Parts.Minor
392 self._minor = minor
393 else:
394 self._minor = 0
396 if micro is not None:
397 if not isinstance(micro, int):
398 raise TypeError("Parameter 'micro' is not of type 'int'.")
399 elif micro < 0:
400 raise ValueError("Parameter 'micro' is negative.")
402 self._parts |= Parts.Micro
403 self._micro = micro
404 else:
405 self._micro = 0
407 if level is None:
408 raise ValueError("Parameter 'level' is None.")
409 elif not isinstance(level, ReleaseLevel):
410 raise TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
411 elif level is ReleaseLevel.Final:
412 if number is not None:
413 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
415 self._parts |= Parts.Level
416 self._releaseLevel = level
417 self._releaseNumber = 0
418 else:
419 self._parts |= Parts.Level
420 self._releaseLevel = level
422 if number is not None:
423 if not isinstance(number, int):
424 raise TypeError("Parameter 'number' is not of type 'int'.")
425 elif number < 0:
426 raise ValueError("Parameter 'number' is negative.")
428 self._releaseNumber = number
429 else:
430 self._releaseNumber = 0
432 if dev is not None:
433 if not isinstance(dev, int):
434 raise TypeError("Parameter 'dev' is not of type 'int'.")
435 elif dev < 0:
436 raise ValueError("Parameter 'dev' is negative.")
438 self._parts |= Parts.Dev
439 self._dev = dev
440 else:
441 self._dev = 0
443 if post is not None:
444 if not isinstance(post, int):
445 raise TypeError("Parameter 'post' is not of type 'int'.")
446 elif post < 0:
447 raise ValueError("Parameter 'post' is negative.")
449 self._parts |= Parts.Post
450 self._post = post
451 else:
452 self._post = 0
454 if build is not None:
455 if not isinstance(build, int):
456 raise TypeError("Parameter 'build' is not of type 'int'.")
457 elif build < 0:
458 raise ValueError("Parameter 'build' is negative.")
460 self._build = build
461 self._parts |= Parts.Build
462 else:
463 self._build = 0
465 if postfix is not None:
466 if not isinstance(postfix, str):
467 raise TypeError("Parameter 'postfix' is not of type 'str'.")
469 self._parts |= Parts.Postfix
470 self._postfix = postfix
471 else:
472 self._postfix = ""
474 if prefix is not None:
475 if not isinstance(prefix, str):
476 raise TypeError("Parameter 'prefix' is not of type 'str'.")
478 self._parts |= Parts.Prefix
479 self._prefix = prefix
480 else:
481 self._prefix = ""
483 if hash is not None:
484 if not isinstance(hash, str):
485 raise TypeError("Parameter 'hash' is not of type 'str'.")
487 self._parts |= Parts.Hash
488 self._hash = hash
489 else:
490 self._hash = ""
492 if flags is None:
493 raise ValueError("Parameter 'flags' is None.")
494 elif not isinstance(flags, Flags):
495 raise TypeError("Parameter 'flags' is not of type 'Flags'.")
497 self._flags = flags
499 @classmethod
500 @abstractmethod
501 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "Version":
502 """Parse a version string and return a Version instance."""
504 @readonly
505 def Parts(self) -> Parts:
506 """
507 Read-only property to access the used parts of this version number.
509 :returns: A flag enumeration of used version number parts.
510 """
511 return self._parts
513 @readonly
514 def Prefix(self) -> str:
515 """
516 Read-only property to access the version number's prefix.
518 :returns: The prefix of the version number.
519 """
520 return self._prefix
522 @readonly
523 def Major(self) -> int:
524 """
525 Read-only property to access the major number.
527 :returns: The major number.
528 """
529 return self._major
531 @readonly
532 def Minor(self) -> int:
533 """
534 Read-only property to access the minor number.
536 :returns: The minor number.
537 """
538 return self._minor
540 @readonly
541 def Micro(self) -> int:
542 """
543 Read-only property to access the micro number.
545 :returns: The micro number.
546 """
547 return self._micro
549 @readonly
550 def ReleaseLevel(self) -> ReleaseLevel:
551 """
552 Read-only property to access the release level.
554 :returns: The release level.
555 """
556 return self._releaseLevel
558 @readonly
559 def ReleaseNumber(self) -> int:
560 """
561 Read-only property to access the release number.
563 :returns: The release number.
564 """
565 return self._releaseNumber
567 @readonly
568 def Post(self) -> int:
569 """
570 Read-only property to access the post number.
572 :returns: The post number.
573 """
574 return self._post
576 @readonly
577 def Dev(self) -> int:
578 """
579 Read-only property to access the development number.
581 :returns: The development number.
582 """
583 return self._dev
585 @readonly
586 def Build(self) -> int:
587 """
588 Read-only property to access the build number.
590 :returns: The build number.
591 """
592 return self._build
594 @readonly
595 def Postfix(self) -> str:
596 """
597 Read-only property to access the version number's postfix.
599 :returns: The postfix of the version number.
600 """
601 return self._postfix
603 @readonly
604 def Hash(self) -> str:
605 """
606 Read-only property to access the version number's hash.
608 :returns: The hash.
609 """
610 return self._hash
612 @readonly
613 def Flags(self) -> Flags:
614 """
615 Read-only property to access the version number's flags.
617 :returns: The flags of the version number.
618 """
619 return self._flags
621 def _equal(self, left: "Version", right: "Version") -> Nullable[bool]:
622 """
623 Private helper method to compute the equality of two :class:`Version` instances.
625 :param left: Left operand.
626 :param right: Right operand.
627 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
628 """
629 return (
630 (left._major == right._major) and
631 (left._minor == right._minor) and
632 (left._micro == right._micro) and
633 (left._releaseLevel == right._releaseLevel) and
634 (left._releaseNumber == right._releaseNumber) and
635 (left._post == right._post) and
636 (left._dev == right._dev) and
637 (left._build == right._build) and
638 (left._postfix == right._postfix)
639 )
641 def _compare(self, left: "Version", right: "Version") -> Nullable[bool]:
642 """
643 Private helper method to compute the comparison of two :class:`Version` instances.
645 :param left: Left operand.
646 :param right: Right operand.
647 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
648 False if ``left`` is greater than ``right``. |br|
649 Otherwise it's None (both operands are equal).
650 """
651 if left._major < right._major:
652 return True
653 elif left._major > right._major:
654 return False
656 if left._minor < right._minor:
657 return True
658 elif left._minor > right._minor:
659 return False
661 if left._micro < right._micro:
662 return True
663 elif left._micro > right._micro:
664 return False
666 if left._releaseLevel < right._releaseLevel: 666 ↛ 667line 666 didn't jump to line 667 because the condition on line 666 was never true
667 return True
668 elif left._releaseLevel > right._releaseLevel: 668 ↛ 669line 668 didn't jump to line 669 because the condition on line 668 was never true
669 return False
671 if left._releaseNumber < right._releaseNumber: 671 ↛ 672line 671 didn't jump to line 672 because the condition on line 671 was never true
672 return True
673 elif left._releaseNumber > right._releaseNumber: 673 ↛ 674line 673 didn't jump to line 674 because the condition on line 673 was never true
674 return False
676 if left._post < right._post: 676 ↛ 677line 676 didn't jump to line 677 because the condition on line 676 was never true
677 return True
678 elif left._post > right._post: 678 ↛ 679line 678 didn't jump to line 679 because the condition on line 678 was never true
679 return False
681 if left._dev < right._dev: 681 ↛ 682line 681 didn't jump to line 682 because the condition on line 681 was never true
682 return True
683 elif left._dev > right._dev: 683 ↛ 684line 683 didn't jump to line 684 because the condition on line 683 was never true
684 return False
686 if left._build < right._build: 686 ↛ 687line 686 didn't jump to line 687 because the condition on line 686 was never true
687 return True
688 elif left._build > right._build: 688 ↛ 689line 688 didn't jump to line 689 because the condition on line 688 was never true
689 return False
691 return None
693 def _minimum(self, actual: "Version", expected: "Version") -> Nullable[bool]:
694 exactMajor = Parts.Minor in expected._parts
695 exactMinor = Parts.Micro in expected._parts
697 if exactMajor and actual._major != expected._major: 697 ↛ 698line 697 didn't jump to line 698 because the condition on line 697 was never true
698 return False
699 elif not exactMajor and actual._major < expected._major:
700 return False
702 if exactMinor and actual._minor != expected._minor: 702 ↛ 703line 702 didn't jump to line 703 because the condition on line 702 was never true
703 return False
704 elif not exactMinor and actual._minor < expected._minor:
705 return False
707 if Parts.Micro in expected._parts:
708 return actual._micro >= expected._micro
710 return True
712 def _format(self, formatSpec: str) -> str:
713 """
714 Return a string representation of this version number according to the format specification.
716 .. topic:: Format Specifiers
718 * ``%p`` - prefix
719 * ``%M`` - major number
720 * ``%m`` - minor number
721 * ``%u`` - micro number
722 * ``%b`` - build number
724 :param formatSpec: The format specification.
725 :returns: Formatted version number.
726 """
727 if formatSpec == "":
728 return self.__str__()
730 result = formatSpec
731 result = result.replace("%p", str(self._prefix))
732 result = result.replace("%M", str(self._major))
733 result = result.replace("%m", str(self._minor))
734 result = result.replace("%u", str(self._micro))
735 result = result.replace("%b", str(self._build))
736 result = result.replace("%r", str(self._releaseLevel)[0])
737 result = result.replace("%R", str(self._releaseLevel))
738 result = result.replace("%n", str(self._releaseNumber))
739 result = result.replace("%d", str(self._dev))
740 result = result.replace("%P", str(self._postfix))
742 return result
744 @mustoverride
745 def __eq__(self, other: Union["Version", str, int, None]) -> bool:
746 """
747 Compare two version numbers for equality.
749 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
750 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
751 number is assumed (all other parts are zero).
753 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
754 number.
756 :param other: Operand to compare against.
757 :returns: ``True``, if both version numbers are equal.
758 :raises ValueError: If parameter ``other`` is None.
759 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
760 """
761 if other is None:
762 raise ValueError(f"Second operand is None.")
763 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
764 pass
765 elif isinstance(other, str):
766 other = self.__class__.Parse(other)
767 elif isinstance(other, int):
768 other = self.__class__(major=other)
769 else:
770 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
771 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
772 raise ex
774 return self._equal(self, other)
776 @mustoverride
777 def __ne__(self, other: Union["Version", str, int, None]) -> bool:
778 """
779 Compare two version numbers for inequality.
781 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
782 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
783 number is assumed (all other parts are zero).
785 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
786 number.
788 :param other: Operand to compare against.
789 :returns: ``True``, if both version numbers are not equal.
790 :raises ValueError: If parameter ``other`` is None.
791 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`str` or :class:`ìnt`.
792 """
793 if other is None:
794 raise ValueError(f"Second operand is None.")
795 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
796 pass
797 elif isinstance(other, str):
798 other = self.__class__.Parse(other)
799 elif isinstance(other, int):
800 other = self.__class__(major=other)
801 else:
802 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by == operator.")
803 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
804 raise ex
806 return not self._equal(self, other)
808 @mustoverride
809 def __lt__(self, other: Union["Version", str, int, None]) -> bool:
810 """
811 Compare two version numbers if the version is less than the second operand.
813 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
814 ``str`` and ``int`` are accepted, too. |br|
815 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
816 number is assumed (all other parts are zero).
818 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
819 number.
821 :param other: Operand to compare against.
822 :returns: ``True``, if version is less than the second operand.
823 :raises ValueError: If parameter ``other`` is None.
824 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
825 """
826 if other is None:
827 raise ValueError(f"Second operand is None.")
828 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
829 pass
830 elif isinstance(other, VersionRange):
831 other = other._lowerBound
832 elif isinstance(other, VersionSet):
833 other = other._items[0]
834 elif isinstance(other, str):
835 other = self.__class__.Parse(other)
836 elif isinstance(other, int):
837 other = self.__class__(major=other)
838 else:
839 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by < operator.")
840 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
841 raise ex
843 return self._compare(self, other) is True
845 @mustoverride
846 def __le__(self, other: Union["Version", str, int, None]) -> bool:
847 """
848 Compare two version numbers if the version is less than or equal the second operand.
850 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
851 ``str`` and ``int`` are accepted, too. |br|
852 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
853 number is assumed (all other parts are zero).
855 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
856 number.
858 :param other: Operand to compare against.
859 :returns: ``True``, if version is less than or equal the second operand.
860 :raises ValueError: If parameter ``other`` is None.
861 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
862 """
863 equalValue = True
864 if other is None:
865 raise ValueError(f"Second operand is None.")
866 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
867 pass
868 elif isinstance(other, VersionRange):
869 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
870 other = other._lowerBound
871 elif isinstance(other, VersionSet):
872 other = other._items[0]
873 elif isinstance(other, str):
874 other = self.__class__.Parse(other)
875 elif isinstance(other, int):
876 other = self.__class__(major=other)
877 else:
878 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by <= operator.")
879 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
880 raise ex
882 result = self._compare(self, other)
883 return result if result is not None else equalValue
885 @mustoverride
886 def __gt__(self, other: Union["Version", str, int, None]) -> bool:
887 """
888 Compare two version numbers if the version is greater than the second operand.
890 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
891 ``str`` and ``int`` are accepted, too. |br|
892 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
893 number is assumed (all other parts are zero).
895 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
896 number.
898 :param other: Operand to compare against.
899 :returns: ``True``, if version is greater than the second operand.
900 :raises ValueError: If parameter ``other`` is None.
901 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
902 """
903 if other is None:
904 raise ValueError(f"Second operand is None.")
905 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
906 pass
907 elif isinstance(other, VersionRange):
908 other = other._upperBound
909 elif isinstance(other, VersionSet):
910 other = other._items[-1]
911 elif isinstance(other, str):
912 other = self.__class__.Parse(other)
913 elif isinstance(other, int):
914 other = self.__class__(major=other)
915 else:
916 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by > operator.")
917 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
918 raise ex
920 return self._compare(self, other) is False
922 @mustoverride
923 def __ge__(self, other: Union["Version", str, int, None]) -> bool:
924 """
925 Compare two version numbers if the version is greater than or equal the second operand.
927 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
928 ``str`` and ``int`` are accepted, too. |br|
929 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
930 number is assumed (all other parts are zero).
932 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
933 number.
935 :param other: Operand to compare against.
936 :returns: ``True``, if version is greater than or equal the second operand.
937 :raises ValueError: If parameter ``other`` is None.
938 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, :class:`str` or :class:`ìnt`.
939 """
940 equalValue = True
941 if other is None:
942 raise ValueError(f"Second operand is None.")
943 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
944 pass
945 elif isinstance(other, VersionRange):
946 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
947 other = other._upperBound
948 elif isinstance(other, VersionSet):
949 other = other._items[-1]
950 elif isinstance(other, str):
951 other = self.__class__.Parse(other)
952 elif isinstance(other, int):
953 other = self.__class__(major=other)
954 else:
955 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >= operator.")
956 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, VersionRange, VersionSet, str, int")
957 raise ex
959 result = self._compare(self, other)
960 return not result if result is not None else equalValue
962 def __rshift__(self, other: Union["Version", str, int, None]) -> bool:
963 if other is None:
964 raise ValueError(f"Second operand is None.")
965 elif isinstance(other, self.__class__):
966 pass
967 elif isinstance(other, str):
968 other = self.__class__.Parse(other)
969 elif isinstance(other, int):
970 other = self.__class__(major=other)
971 else:
972 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by >> operator.")
973 ex.add_note(f"Supported types for second operand: {self.__class__.__name__}, str, int")
974 raise ex
976 return self._minimum(self, other)
978 def __hash__(self) -> int:
979 if self.__hash is None:
980 self.__hash = hash((
981 self._prefix,
982 self._major,
983 self._minor,
984 self._micro,
985 self._releaseLevel,
986 self._releaseNumber,
987 self._post,
988 self._dev,
989 self._build,
990 self._postfix,
991 self._hash,
992 self._flags
993 ))
994 return self.__hash
997@export
998class SemanticVersion(Version):
999 """Representation of a semantic version number like ``3.7.12``."""
1001 _PATTERN: ClassVar[Pattern] = re_compile(
1002 r"^"
1003 r"(?P<prefix>rev|REV|[vViIrR])?"
1004 r"(?P<major>\d+)"
1005 r"(?:\.(?P<minor>\d+))?"
1006 r"(?:\.(?P<micro>\d+))?"
1007 r"(?:"
1008 r"(?:\.(?P<build>\d+))"
1009 r"|"
1010 r"(?:[-](?P<release>dev|final))"
1011 r"|"
1012 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|a|b|c|rc|pl)(?P<number>\d+))"
1013 r")?"
1014 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1015 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1016 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1017 r"$"
1018 )
1019# QUESTION: was this how many commits a version is ahead of the last tagged version?
1020# ahead: int = 0
1022 def __init__(
1023 self,
1024 major: int,
1025 minor: Nullable[int] = None,
1026 micro: Nullable[int] = None,
1027 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1028 number: Nullable[int] = None,
1029 post: Nullable[int] = None,
1030 dev: Nullable[int] = None,
1031 *,
1032 build: Nullable[int] = None,
1033 postfix: Nullable[str] = None,
1034 prefix: Nullable[str] = None,
1035 hash: Nullable[str] = None,
1036 flags: Flags = Flags.NoVCS
1037 ) -> None:
1038 """
1039 Initializes a semantic version number representation.
1041 :param major: Major number part of the version number.
1042 :param minor: Minor number part of the version number.
1043 :param micro: Micro (patch) number part of the version number.
1044 :param build: Build number part of the version number.
1045 :param level: tbd
1046 :param number: tbd
1047 :param post: Post number part of the version number.
1048 :param dev: Development number part of the version number.
1049 :param prefix: The version number's prefix.
1050 :param postfix: The version number's postfix.
1051 :param flags: The version number's flags.
1052 :param hash: tbd
1053 :raises TypeError: If parameter 'major' is not of type int.
1054 :raises ValueError: If parameter 'major' is a negative number.
1055 :raises TypeError: If parameter 'minor' is not of type int.
1056 :raises ValueError: If parameter 'minor' is a negative number.
1057 :raises TypeError: If parameter 'micro' is not of type int.
1058 :raises ValueError: If parameter 'micro' is a negative number.
1059 :raises TypeError: If parameter 'build' is not of type int.
1060 :raises ValueError: If parameter 'build' is a negative number.
1061 :raises TypeError: If parameter 'post' is not of type int.
1062 :raises ValueError: If parameter 'post' is a negative number.
1063 :raises TypeError: If parameter 'dev' is not of type int.
1064 :raises ValueError: If parameter 'dev' is a negative number.
1065 :raises TypeError: If parameter 'prefix' is not of type str.
1066 :raises TypeError: If parameter 'postfix' is not of type str.
1067 """
1068 super().__init__(major, minor, micro, level, number, post, dev, build=build, postfix=postfix, prefix=prefix, hash=hash, flags=flags)
1070 @classmethod
1071 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["SemanticVersion"], bool]] = None) -> "SemanticVersion":
1072 """
1073 Parse a version string and return a :class:`SemanticVersion` instance.
1075 Allowed prefix characters:
1077 * ``v|V`` - version, public version, public release
1078 * ``i|I`` - internal version, internal release
1079 * ``r|R`` - release, revision
1080 * ``rev|REV`` - revision
1082 :param versionString: The version string to parse.
1083 :param validator: Optional, a validation function.
1084 :returns: An object representing a semantic version.
1085 :raises TypeError: When parameter ``versionString`` is not a string.
1086 :raises ValueError: When parameter ``versionString`` is None.
1087 :raises ValueError: When parameter ``versionString`` is empty.
1088 """
1089 if versionString is None:
1090 raise ValueError("Parameter 'versionString' is None.")
1091 elif not isinstance(versionString, str):
1092 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1093 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1094 raise ex
1095 elif (versionString := versionString.strip()) == "":
1096 raise ValueError("Parameter 'versionString' is empty.")
1098 if (match := cls._PATTERN.match(versionString)) is None:
1099 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1100 ex.add_note(f"It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v1.2.3'.")
1101 raise ex
1103 def toInt(value: Nullable[str]) -> Nullable[int]:
1104 if value is None or value == "":
1105 return None
1107 try:
1108 return int(value)
1109 except ValueError as ex: # pragma: no cover
1110 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1112 prefix = match["prefix"]
1114 release = match["release"]
1115 if release is not None:
1116 if release == "dev": 1116 ↛ 1118line 1116 didn't jump to line 1118 because the condition on line 1116 was always true
1117 releaseLevel = ReleaseLevel.Development
1118 elif release == "final":
1119 releaseLevel = ReleaseLevel.Final
1120 else: # pragma: no cover
1121 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1122 else:
1123 level = match["level"]
1124 if level is not None:
1125 level = level.lower()
1126 if level == "a" or level == "alpha":
1127 releaseLevel = ReleaseLevel.Alpha
1128 elif level == "b" or level == "beta":
1129 releaseLevel = ReleaseLevel.Beta
1130 elif level == "c" or level == "gamma":
1131 releaseLevel = ReleaseLevel.Gamma
1132 elif level == "rc":
1133 releaseLevel = ReleaseLevel.ReleaseCandidate
1134 else: # pragma: no cover
1135 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1136 else:
1137 releaseLevel = ReleaseLevel.Final
1139 version = cls(
1140 major=toInt(match["major"]),
1141 minor=toInt(match["minor"]),
1142 micro=toInt(match["micro"]),
1143 level=releaseLevel,
1144 number=toInt(match["number"]),
1145 post=toInt(match["post"]),
1146 dev=toInt(match["dev"]),
1147 build=toInt(match["build"]),
1148 postfix=match["postfix"],
1149 prefix=prefix if prefix != "" else None,
1150 # hash=match["hash"],
1151 flags=Flags.Clean
1152 )
1154 if validator is not None and not validator(version): 1154 ↛ 1156line 1154 didn't jump to line 1156 because the condition on line 1154 was never true
1155 # TODO: VersionValidatorException
1156 raise ValueError(f"Failed to validate version string '{versionString}'.")
1158 return version
1160 @readonly
1161 def Patch(self) -> int:
1162 """
1163 Read-only property to access the patch number.
1165 The patch number is identical to the micro number.
1167 :returns: The patch number.
1168 """
1169 return self._micro
1171 def _equal(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1172 """
1173 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1175 :param left: Left operand.
1176 :param right: Right operand.
1177 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1178 """
1179 return super()._equal(left, right)
1181 def _compare(self, left: "SemanticVersion", right: "SemanticVersion") -> Nullable[bool]:
1182 """
1183 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1185 :param left: Left operand.
1186 :param right: Right operand.
1187 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1188 False if ``left`` is greater than ``right``. |br|
1189 Otherwise it's None (both operands are equal).
1190 """
1191 return super()._compare(left, right)
1193 def __eq__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1194 """
1195 Compare two version numbers for equality.
1197 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1198 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1199 number is assumed (all other parts are zero).
1201 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1202 number.
1204 :param other: Operand to compare against.
1205 :returns: ``True``, if both version numbers are equal.
1206 :raises ValueError: If parameter ``other`` is None.
1207 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1208 """
1209 return super().__eq__(other)
1211 def __ne__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1212 """
1213 Compare two version numbers for inequality.
1215 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1216 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1217 number is assumed (all other parts are zero).
1219 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1220 number.
1222 :param other: Operand to compare against.
1223 :returns: ``True``, if both version numbers are not equal.
1224 :raises ValueError: If parameter ``other`` is None.
1225 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1226 """
1227 return super().__ne__(other)
1229 def __lt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1230 """
1231 Compare two version numbers if the version is less than the second operand.
1233 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1234 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1235 number is assumed (all other parts are zero).
1237 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1238 number.
1240 :param other: Operand to compare against.
1241 :returns: ``True``, if version is less than the second operand.
1242 :raises ValueError: If parameter ``other`` is None.
1243 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1244 """
1245 return super().__lt__(other)
1247 def __le__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1248 """
1249 Compare two version numbers if the version is less than or equal the second operand.
1251 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1252 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1253 number is assumed (all other parts are zero).
1255 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1256 number.
1258 :param other: Operand to compare against.
1259 :returns: ``True``, if version is less than or equal the second operand.
1260 :raises ValueError: If parameter ``other`` is None.
1261 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1262 """
1263 return super().__le__(other)
1265 def __gt__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1266 """
1267 Compare two version numbers if the version is greater than the second operand.
1269 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1270 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1271 number is assumed (all other parts are zero).
1273 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1274 number.
1276 :param other: Operand to compare against.
1277 :returns: ``True``, if version is greater than the second operand.
1278 :raises ValueError: If parameter ``other`` is None.
1279 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1280 """
1281 return super().__gt__(other)
1283 def __ge__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1284 """
1285 Compare two version numbers if the version is greater than or equal the second operand.
1287 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1288 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1289 number is assumed (all other parts are zero).
1291 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1292 number.
1294 :param other: Operand to compare against.
1295 :returns: ``True``, if version is greater than or equal the second operand.
1296 :raises ValueError: If parameter ``other`` is None.
1297 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, :class:`str` or :class:`ìnt`.
1298 """
1299 return super().__ge__(other)
1301 def __rshift__(self, other: Union["SemanticVersion", str, int, None]) -> bool:
1302 return super().__rshift__(other)
1304 def __hash__(self) -> int:
1305 return super().__hash__()
1307 def __format__(self, formatSpec: str) -> str:
1308 result = self._format(formatSpec)
1310 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1311 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1313 return result.replace("%%", "%")
1315 def __repr__(self) -> str:
1316 """
1317 Return a normalized string representation of this version number.
1319 .. note::
1321 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1322 :meth:`__str__` to render a version number including its prefix.
1324 :returns: Raw version number representation without a prefix.
1325 """
1326 return f"{self._major}.{self._minor}.{self._micro}"
1328 def __str__(self) -> str:
1329 """
1330 Return a string representation of this version number.
1332 :returns: Version number representation.
1333 """
1334 result = self._prefix if Parts.Prefix in self._parts else ""
1335 result += f"{self._major}" # major is always present
1336 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1337 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1338 result += f".{self._build}" if Parts.Build in self._parts else ""
1339 if self._releaseLevel is ReleaseLevel.Development:
1340 result += "-dev"
1341 elif self._releaseLevel is ReleaseLevel.Alpha:
1342 result += f".alpha{self._releaseNumber}"
1343 elif self._releaseLevel is ReleaseLevel.Beta:
1344 result += f".beta{self._releaseNumber}"
1345 elif self._releaseLevel is ReleaseLevel.Gamma: 1345 ↛ 1346line 1345 didn't jump to line 1346 because the condition on line 1345 was never true
1346 result += f".gamma{self._releaseNumber}"
1347 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1348 result += f".rc{self._releaseNumber}"
1349 result += f".post{self._post}" if Parts.Post in self._parts else ""
1350 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1351 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1353 return result
1356@export
1357class PythonVersion(SemanticVersion):
1358 """
1359 Represents a Python version.
1360 """
1362 @classmethod
1363 def FromSysVersionInfo(cls) -> "PythonVersion":
1364 """
1365 Create a Python version from :data:`sys.version_info`.
1367 :returns: A PythonVersion instance of the current Python interpreter's version.
1368 """
1369 from sys import version_info
1371 if version_info.releaselevel == "final":
1372 rl = ReleaseLevel.Final
1373 number = None
1374 else: # pragma: no cover
1375 number = version_info.serial
1377 if version_info.releaselevel == "alpha":
1378 rl = ReleaseLevel.Alpha
1379 elif version_info.releaselevel == "beta":
1380 rl = ReleaseLevel.Beta
1381 elif version_info.releaselevel == "candidate":
1382 rl = ReleaseLevel.ReleaseCandidate
1383 else: # pragma: no cover
1384 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1386 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1388 def __hash__(self) -> int:
1389 return super().__hash__()
1391 def __str__(self) -> str:
1392 """
1393 Return a string representation of this version number.
1395 :returns: Version number representation.
1396 """
1397 result = self._prefix if Parts.Prefix in self._parts else ""
1398 result += f"{self._major}" # major is always present
1399 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1400 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1401 if self._releaseLevel is ReleaseLevel.Alpha: 1401 ↛ 1402line 1401 didn't jump to line 1402 because the condition on line 1401 was never true
1402 result += f"a{self._releaseNumber}"
1403 elif self._releaseLevel is ReleaseLevel.Beta: 1403 ↛ 1404line 1403 didn't jump to line 1404 because the condition on line 1403 was never true
1404 result += f"b{self._releaseNumber}"
1405 elif self._releaseLevel is ReleaseLevel.Gamma: 1405 ↛ 1406line 1405 didn't jump to line 1406 because the condition on line 1405 was never true
1406 result += f"c{self._releaseNumber}"
1407 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate: 1407 ↛ 1408line 1407 didn't jump to line 1408 because the condition on line 1407 was never true
1408 result += f"rc{self._releaseNumber}"
1409 result += f".post{self._post}" if Parts.Post in self._parts else ""
1410 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1411 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1413 return result
1416@export
1417class CalendarVersion(Version):
1418 """Representation of a calendar version number like ``2021.10``."""
1420 _PARTCOUNT: ClassVar[int] = 3 #: Number of numeric parts a version number of this class can carry.
1422 _PATTERN: ClassVar[Pattern] = re_compile(
1423 r"^"
1424 r"(?P<prefix>rev|REV|[vViIrR])?"
1425 r"(?P<major>\d+)"
1426 r"(?:\.(?P<minor>\d+))?"
1427 r"(?:\.(?P<micro>\d+))?"
1428 r"$"
1429 )
1431 def __init__(
1432 self,
1433 major: int,
1434 minor: Nullable[int] = None,
1435 micro: Nullable[int] = None,
1436 build: Nullable[int] = None,
1437 flags: Flags = Flags.Clean,
1438 prefix: Nullable[str] = None,
1439 postfix: Nullable[str] = None
1440 ) -> None:
1441 """
1442 Initializes a calendar version number representation.
1444 :param major: Major number part of the version number.
1445 :param minor: Minor number part of the version number.
1446 :param micro: Micro (patch) number part of the version number.
1447 :param build: Build number part of the version number.
1448 :param flags: The version number's flags.
1449 :param prefix: The version number's prefix.
1450 :param postfix: The version number's postfix.
1451 :raises TypeError: If parameter 'major' is not of type int.
1452 :raises ValueError: If parameter 'major' is a negative number.
1453 :raises TypeError: If parameter 'minor' is not of type int.
1454 :raises ValueError: If parameter 'minor' is a negative number.
1455 :raises TypeError: If parameter 'micro' is not of type int.
1456 :raises ValueError: If parameter 'micro' is a negative number.
1457 :raises TypeError: If parameter 'build' is not of type int.
1458 :raises ValueError: If parameter 'build' is a negative number.
1459 :raises TypeError: If parameter 'prefix' is not of type str.
1460 :raises TypeError: If parameter 'postfix' is not of type str.
1461 """
1462 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1464 @classmethod
1465 def Parse(cls, versionString: Nullable[str], validator: Nullable[Callable[["CalendarVersion"], bool]] = None) -> "CalendarVersion":
1466 """
1467 Parse a version string and return a :class:`CalendarVersion` instance.
1469 Allowed prefix characters:
1471 * ``v|V`` - version, public version, public release
1472 * ``i|I`` - internal version, internal release
1473 * ``r|R`` - release, revision
1474 * ``rev|REV`` - revision
1476 A version number carries up to :attr:`_PARTCOUNT` numeric parts. :class:`YearMonthVersion`,
1477 :class:`YearWeekVersion` and :class:`YearReleaseVersion` describe two parts, so a third part is rejected for
1478 them.
1480 :param versionString: The version string to parse.
1481 :param validator: Optional, a validation function.
1482 :returns: An object representing a calendar version.
1483 :raises TypeError: If parameter ``versionString`` is not a string.
1484 :raises ValueError: If parameter ``versionString`` is None.
1485 :raises ValueError: If parameter ``versionString`` is empty.
1486 :raises ValueError: If parameter ``versionString`` isn't a calendar version number.
1487 :raises ValueError: If parameter ``versionString`` has more parts than the class describes.
1488 """
1489 if versionString is None:
1490 raise ValueError("Parameter 'versionString' is None.")
1491 elif not isinstance(versionString, str):
1492 ex = TypeError(f"Parameter 'versionString' is not of type 'str'.")
1493 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1494 raise ex
1495 elif (versionString := versionString.strip()) == "":
1496 raise ValueError("Parameter 'versionString' is empty.")
1498 if (match := cls._PATTERN.match(versionString)) is None:
1499 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1500 ex.add_note(f"A calendar version number is made of up to {cls._PARTCOUNT} numeric parts, e.g. '2024.04'.")
1501 ex.add_note(f"It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v2024.04'.")
1502 raise ex
1504 prefix = match["prefix"]
1505 minor = match["minor"]
1506 micro = match["micro"]
1508 if micro is not None and cls._PARTCOUNT < 3:
1509 ex = ValueError(f"Version number '{versionString}' has 3 parts, but '{cls.__name__}' describes {cls._PARTCOUNT}.")
1510 ex.add_note(f"Use 'CalendarVersion' or 'YearMonthDayVersion' to parse a 3-part calendar version number.")
1511 raise ex
1513 numbers = [int(match["major"]), 0 if minor is None else int(minor)]
1514 if micro is not None:
1515 numbers.append(int(micro))
1517 version = cls(*numbers, flags=Flags.Clean, prefix=prefix if prefix != "" else None)
1519 if validator is not None and not validator(version):
1520 raise ValueError(f"Failed to validate version string '{versionString}'.") # pragma: no cover
1522 return version
1524 @readonly
1525 def Year(self) -> int:
1526 """
1527 Read-only property to access the year part.
1529 :returns: The year part.
1530 """
1531 return self._major
1533 def _equal(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1534 """
1535 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1537 :param left: Left parameter.
1538 :param right: Right parameter.
1539 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1540 """
1541 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1543 def _compare(self, left: "CalendarVersion", right: "CalendarVersion") -> Nullable[bool]:
1544 """
1545 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1547 :param left: Left parameter.
1548 :param right: Right parameter.
1549 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1550 False if ``left`` is greater than ``right``. |br|
1551 Otherwise it's None (both parameters are equal).
1552 """
1553 if left._major < right._major:
1554 return True
1555 elif left._major > right._major:
1556 return False
1558 if left._minor < right._minor:
1559 return True
1560 elif left._minor > right._minor:
1561 return False
1563 if left._micro < right._micro: 1563 ↛ 1564line 1563 didn't jump to line 1564 because the condition on line 1563 was never true
1564 return True
1565 elif left._micro > right._micro: 1565 ↛ 1566line 1565 didn't jump to line 1566 because the condition on line 1565 was never true
1566 return False
1568 return None
1570 def __eq__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1571 """
1572 Compare two version numbers for equality.
1574 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1575 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1576 number is assumed (all other parts are zero).
1578 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1579 number.
1581 :param other: Parameter to compare against.
1582 :returns: ``True``, if both version numbers are equal.
1583 :raises ValueError: If parameter ``other`` is None.
1584 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1585 """
1586 return super().__eq__(other)
1588 def __ne__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1589 """
1590 Compare two version numbers for inequality.
1592 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1593 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
1594 number is assumed (all other parts are zero).
1596 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1597 number.
1599 :param other: Parameter to compare against.
1600 :returns: ``True``, if both version numbers are not equal.
1601 :raises ValueError: If parameter ``other`` is None.
1602 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1603 """
1604 return super().__ne__(other)
1606 def __lt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1607 """
1608 Compare two version numbers if the version is less than the second operand.
1610 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1611 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1612 number is assumed (all other parts are zero).
1614 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1615 number.
1617 :param other: Parameter to compare against.
1618 :returns: ``True``, if version is less than the second operand.
1619 :raises ValueError: If parameter ``other`` is None.
1620 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1621 """
1622 return super().__lt__(other)
1624 def __le__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1625 """
1626 Compare two version numbers if the version is less than or equal the second operand.
1628 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1629 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1630 number is assumed (all other parts are zero).
1632 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1633 number.
1635 :param other: Parameter to compare against.
1636 :returns: ``True``, if version is less than or equal the second operand.
1637 :raises ValueError: If parameter ``other`` is None.
1638 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1639 """
1640 return super().__le__(other)
1642 def __gt__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1643 """
1644 Compare two version numbers if the version is greater than the second operand.
1646 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1647 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1648 number is assumed (all other parts are zero).
1650 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1651 number.
1653 :param other: Parameter to compare against.
1654 :returns: ``True``, if version is greater than the second operand.
1655 :raises ValueError: If parameter ``other`` is None.
1656 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1657 """
1658 return super().__gt__(other)
1660 def __ge__(self, other: Union["CalendarVersion", str, int, None]) -> bool:
1661 """
1662 Compare two version numbers if the version is greater than or equal the second operand.
1664 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
1665 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1666 number is assumed (all other parts are zero).
1668 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1669 number.
1671 :param other: Parameter to compare against.
1672 :returns: ``True``, if version is greater than or equal the second operand.
1673 :raises ValueError: If parameter ``other`` is None.
1674 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, :class:`str` or :class:`ìnt`.
1675 """
1676 return super().__ge__(other)
1678 def __hash__(self) -> int:
1679 return super().__hash__()
1681 def __format__(self, formatSpec: str) -> str:
1682 """
1683 Return a string representation of this version number according to the format specification.
1685 .. topic:: Format Specifiers
1687 * ``%M`` - major number (year)
1688 * ``%m`` - minor number (month/week)
1689 * ``%u`` - micro number (day)
1691 :param formatSpec: The format specification.
1692 :returns: Formatted version number.
1693 """
1694 if formatSpec == "":
1695 return self.__str__()
1697 result = formatSpec
1698 # result = result.replace("%P", str(self._prefix))
1699 result = result.replace("%M", str(self._major))
1700 result = result.replace("%m", str(self._minor))
1701 result = result.replace("%u", str(self._micro))
1702 # result = result.replace("%p", str(self._pre))
1704 return result.replace("%%", "%")
1706 def __repr__(self) -> str:
1707 """
1708 Return a normalized string representation of this version number.
1710 .. note::
1712 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1713 :meth:`__str__` to render a version number including its prefix.
1715 :returns: Raw version number representation without a prefix.
1716 """
1717 result = f"{self._major}.{self._minor}"
1718 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1720 return result
1722 def __str__(self) -> str:
1723 """
1724 Return a string representation of this version number with only the present parts.
1726 :returns: Version number representation including a prefix.
1727 """
1728 result = self._prefix if Parts.Prefix in self._parts else ""
1729 result += f"{self._major}"
1730 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1731 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1733 return result
1736@export
1737class YearMonthVersion(CalendarVersion):
1738 """Representation of a calendar version number made of year and month like ``2021.10``."""
1740 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and month.
1742 def __init__(
1743 self,
1744 year: int,
1745 month: Nullable[int] = None,
1746 build: Nullable[int] = None,
1747 flags: Flags = Flags.Clean,
1748 prefix: Nullable[str] = None,
1749 postfix: Nullable[str] = None
1750 ) -> None:
1751 """
1752 Initializes a year-month version number representation.
1754 :param year: Year part of the version number.
1755 :param month: Month part of the version number.
1756 :param build: Build number part of the version number.
1757 :param flags: The version number's flags.
1758 :param prefix: The version number's prefix.
1759 :param postfix: The version number's postfix.
1760 :raises TypeError: If parameter 'major' is not of type int.
1761 :raises ValueError: If parameter 'major' is a negative number.
1762 :raises TypeError: If parameter 'minor' is not of type int.
1763 :raises ValueError: If parameter 'minor' is a negative number.
1764 :raises TypeError: If parameter 'micro' is not of type int.
1765 :raises ValueError: If parameter 'micro' is a negative number.
1766 :raises TypeError: If parameter 'build' is not of type int.
1767 :raises ValueError: If parameter 'build' is a negative number.
1768 :raises TypeError: If parameter 'prefix' is not of type str.
1769 :raises TypeError: If parameter 'postfix' is not of type str.
1770 """
1771 super().__init__(year, month, 0, build, flags, prefix, postfix)
1773 @readonly
1774 def Month(self) -> int:
1775 """
1776 Read-only property to access the month part.
1778 :returns: The month part.
1779 """
1780 return self._minor
1782 def __hash__(self) -> int:
1783 return super().__hash__()
1786@export
1787class YearWeekVersion(CalendarVersion):
1788 """Representation of a calendar version number made of year and week like ``2021.47``."""
1790 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and week.
1792 def __init__(
1793 self,
1794 year: int,
1795 week: Nullable[int] = None,
1796 build: Nullable[int] = None,
1797 flags: Flags = Flags.Clean,
1798 prefix: Nullable[str] = None,
1799 postfix: Nullable[str] = None
1800 ) -> None:
1801 """
1802 Initializes a year-week version number representation.
1804 :param year: Year part of the version number.
1805 :param week: Week part of the version number.
1806 :param build: Build number part of the version number.
1807 :param flags: The version number's flags.
1808 :param prefix: The version number's prefix.
1809 :param postfix: The version number's postfix.
1810 :raises TypeError: If parameter 'major' is not of type int.
1811 :raises ValueError: If parameter 'major' is a negative number.
1812 :raises TypeError: If parameter 'minor' is not of type int.
1813 :raises ValueError: If parameter 'minor' is a negative number.
1814 :raises TypeError: If parameter 'micro' is not of type int.
1815 :raises ValueError: If parameter 'micro' is a negative number.
1816 :raises TypeError: If parameter 'build' is not of type int.
1817 :raises ValueError: If parameter 'build' is a negative number.
1818 :raises TypeError: If parameter 'prefix' is not of type str.
1819 :raises TypeError: If parameter 'postfix' is not of type str.
1820 """
1821 super().__init__(year, week, 0, build, flags, prefix, postfix)
1823 @readonly
1824 def Week(self) -> int:
1825 """
1826 Read-only property to access the week part.
1828 :returns: The week part.
1829 """
1830 return self._minor
1832 def __hash__(self) -> int:
1833 return super().__hash__()
1836@export
1837class YearReleaseVersion(CalendarVersion):
1838 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
1840 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and release.
1842 def __init__(
1843 self,
1844 year: int,
1845 release: Nullable[int] = None,
1846 build: Nullable[int] = None,
1847 flags: Flags = Flags.Clean,
1848 prefix: Nullable[str] = None,
1849 postfix: Nullable[str] = None
1850 ) -> None:
1851 """
1852 Initializes a year-release version number representation.
1854 :param year: Year part of the version number.
1855 :param release: Release number of the version number.
1856 :param build: Build number part of the version number.
1857 :param flags: The version number's flags.
1858 :param prefix: The version number's prefix.
1859 :param postfix: The version number's postfix.
1860 :raises TypeError: If parameter 'major' is not of type int.
1861 :raises ValueError: If parameter 'major' is a negative number.
1862 :raises TypeError: If parameter 'minor' is not of type int.
1863 :raises ValueError: If parameter 'minor' is a negative number.
1864 :raises TypeError: If parameter 'micro' is not of type int.
1865 :raises ValueError: If parameter 'micro' is a negative number.
1866 :raises TypeError: If parameter 'build' is not of type int.
1867 :raises ValueError: If parameter 'build' is a negative number.
1868 :raises TypeError: If parameter 'prefix' is not of type str.
1869 :raises TypeError: If parameter 'postfix' is not of type str.
1870 """
1871 super().__init__(year, release, 0, build, flags, prefix, postfix)
1873 @readonly
1874 def Release(self) -> int:
1875 """
1876 Read-only property to access the release number.
1878 :returns: The release number.
1879 """
1880 return self._minor
1882 def __hash__(self) -> int:
1883 return super().__hash__()
1886@export
1887class YearMonthDayVersion(CalendarVersion):
1888 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
1890 def __init__(
1891 self,
1892 year: int,
1893 month: Nullable[int] = None,
1894 day: Nullable[int] = None,
1895 build: Nullable[int] = None,
1896 flags: Flags = Flags.Clean,
1897 prefix: Nullable[str] = None,
1898 postfix: Nullable[str] = None
1899 ) -> None:
1900 """
1901 Initializes a year-month-day version number representation.
1903 :param year: Year part of the version number.
1904 :param month: Month part of the version number.
1905 :param day: Day part of the version number.
1906 :param build: Build number part of the version number.
1907 :param flags: The version number's flags.
1908 :param prefix: The version number's prefix.
1909 :param postfix: The version number's postfix.
1910 :raises TypeError: If parameter 'major' is not of type int.
1911 :raises ValueError: If parameter 'major' is a negative number.
1912 :raises TypeError: If parameter 'minor' is not of type int.
1913 :raises ValueError: If parameter 'minor' is a negative number.
1914 :raises TypeError: If parameter 'micro' is not of type int.
1915 :raises ValueError: If parameter 'micro' is a negative number.
1916 :raises TypeError: If parameter 'build' is not of type int.
1917 :raises ValueError: If parameter 'build' is a negative number.
1918 :raises TypeError: If parameter 'prefix' is not of type str.
1919 :raises TypeError: If parameter 'postfix' is not of type str.
1920 """
1921 super().__init__(year, month, day, build, flags, prefix, postfix)
1923 @readonly
1924 def Month(self) -> int:
1925 """
1926 Read-only property to access the month part.
1928 :returns: The month part.
1929 """
1930 return self._minor
1932 @readonly
1933 def Day(self) -> int:
1934 """
1935 Read-only property to access the day part.
1937 :returns: The day part.
1938 """
1939 return self._micro
1941 def __hash__(self) -> int:
1942 return super().__hash__()
1945V = TypeVar("V", bound=Version)
1947@export
1948class RangeBoundHandling(Flag):
1949 """
1950 A flag defining how to handle bounds in a range.
1952 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
1953 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
1954 """
1955 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
1956 LowerBoundInclusive = 0 #: Lower bound is inclusive.
1957 UpperBoundInclusive = 0 #: Upper bound is inclusive.
1958 LowerBoundExclusive = 1 #: Lower bound is exclusive.
1959 UpperBoundExclusive = 2 #: Upper bound is exclusive.
1960 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
1963@export
1964class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
1965 """
1966 Representation of a version range described by a lower bound and upper bound version.
1968 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
1969 """
1970 _lowerBound: V
1971 _upperBound: V
1972 _boundHandling: RangeBoundHandling
1974 def __init__(self, lowerBound: V, upperBound: V, boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive) -> None:
1975 """
1976 Initializes a version range described by a lower and upper bound.
1978 :param lowerBound: lowest version (inclusive).
1979 :param upperBound: hightest version (inclusive).
1980 :raises TypeError: If parameter ``lowerBound`` is not of type :class:`Version`.
1981 :raises TypeError: If parameter ``upperBound`` is not of type :class:`Version`.
1982 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
1983 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
1984 """
1985 if not isinstance(lowerBound, Version):
1986 ex = TypeError(f"Parameter 'lowerBound' is not of type 'Version'.")
1987 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
1988 raise ex
1990 if not isinstance(upperBound, Version):
1991 ex = TypeError(f"Parameter 'upperBound' is not of type 'Version'.")
1992 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
1993 raise ex
1995 if not ((lBC := lowerBound.__class__) is (uBC := upperBound.__class__) or issubclass(lBC, uBC) or issubclass(uBC, lBC)):
1996 ex = TypeError(f"Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
1997 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and type '{getFullyQualifiedName(upperBound)}' for upperBound.")
1998 raise ex
2000 if not (lowerBound <= upperBound):
2001 ex = ValueError(f"Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
2002 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
2003 raise ex
2005 self._lowerBound = lowerBound
2006 self._upperBound = upperBound
2007 self._boundHandling = boundHandling
2009 @property
2010 def LowerBound(self) -> V:
2011 """
2012 Property to access the range's lower bound.
2014 :returns: Lower bound of the version range.
2015 """
2016 return self._lowerBound
2018 @LowerBound.setter
2019 def LowerBound(self, value: V) -> None:
2020 if not isinstance(value, Version):
2021 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
2022 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2023 raise ex
2025 self._lowerBound = value
2027 @property
2028 def UpperBound(self) -> V:
2029 """
2030 Property to access the range's upper bound.
2032 :returns: Upper bound of the version range.
2033 """
2034 return self._upperBound
2036 @UpperBound.setter
2037 def UpperBound(self, value: V) -> None:
2038 if not isinstance(value, Version):
2039 ex = TypeError(f"Parameter 'value' is not of type 'Version'.")
2040 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2041 raise ex
2043 self._upperBound = value
2045 @property
2046 def BoundHandling(self) -> RangeBoundHandling:
2047 """
2048 Property to access the range's bound handling strategy.
2050 :returns: The range's bound handling strategy.
2051 """
2052 return self._boundHandling
2054 @BoundHandling.setter
2055 def BoundHandling(self, value: RangeBoundHandling) -> None:
2056 if not isinstance(value, RangeBoundHandling):
2057 ex = TypeError(f"Parameter 'value' is not of type 'RangeBoundHandling'.")
2058 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2059 raise ex
2061 self._boundHandling = value
2063 def __and__(self, other: Any) -> "VersionRange[T]":
2064 """
2065 Compute the intersection of two version ranges.
2067 :param other: Second version range to intersect with.
2068 :returns: Intersected version range.
2069 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2070 :raises ValueError: If intersection is empty.
2071 """
2072 if not isinstance(other, VersionRange): 2072 ↛ 2073line 2072 didn't jump to line 2073 because the condition on line 2072 was never true
2073 ex = TypeError(f"Parameter 'other' is not of type 'VersionRange'.")
2074 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2075 raise ex
2077 if not (isinstance(other._lowerBound, self._lowerBound.__class__) and isinstance(self._lowerBound, other._lowerBound.__class__)): 2077 ↛ 2078line 2077 didn't jump to line 2078 because the condition on line 2077 was never true
2078 ex = TypeError(f"Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible with each other.")
2079 ex.add_note(
2080 f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2081 raise ex
2083 if other._lowerBound < self._lowerBound:
2084 lBound = self._lowerBound
2085 elif other._lowerBound in self: 2085 ↛ 2088line 2085 didn't jump to line 2088 because the condition on line 2085 was always true
2086 lBound = other._lowerBound
2087 else:
2088 raise ValueError()
2090 if other._upperBound > self._upperBound:
2091 uBound = self._upperBound
2092 elif other._upperBound in self: 2092 ↛ 2095line 2092 didn't jump to line 2095 because the condition on line 2092 was always true
2093 uBound = other._upperBound
2094 else:
2095 raise ValueError()
2097 return self.__class__(lBound, uBound)
2099 def __lt__(self, other: Any) -> bool:
2100 """
2101 Compare a version range and a version numbers if the version range is less than the second operand (version).
2103 :param other: Operand to compare against.
2104 :returns: ``True``, if version range is less than the second operand (version).
2105 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2106 """
2107 # TODO: support VersionRange < VersionRange too
2108 # TODO: support str, int, ... like Version ?
2109 if not isinstance(other, Version):
2110 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2111 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2112 raise ex
2114 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2114 ↛ 2115line 2114 didn't jump to line 2115 because the condition on line 2114 was never true
2115 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2116 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2117 raise ex
2119 return self._upperBound < other
2121 def __le__(self, other: Any) -> bool:
2122 """
2123 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2125 :param other: Operand to compare against.
2126 :returns: ``True``, if version range is less than or equal the second operand (version).
2127 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2128 """
2129 # TODO: support VersionRange < VersionRange too
2130 # TODO: support str, int, ... like Version ?
2131 if not isinstance(other, Version): 2131 ↛ 2132line 2131 didn't jump to line 2132 because the condition on line 2131 was never true
2132 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2133 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2134 raise ex
2136 if not (isinstance(other, self._lowerBound.__class__) and isinstance(self._lowerBound, other.__class__)): 2136 ↛ 2137line 2136 didn't jump to line 2137 because the condition on line 2136 was never true
2137 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2138 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2139 raise ex
2141 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2142 return self._upperBound < other
2143 else:
2144 return self._upperBound <= other
2146 def __gt__(self, other: Any) -> bool:
2147 """
2148 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2150 :param other: Operand to compare against.
2151 :returns: ``True``, if version range is greater than the second operand (version).
2152 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2153 """
2154 # TODO: support VersionRange < VersionRange too
2155 # TODO: support str, int, ... like Version ?
2156 if not isinstance(other, Version): 2156 ↛ 2157line 2156 didn't jump to line 2157 because the condition on line 2156 was never true
2157 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2158 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2159 raise ex
2161 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2161 ↛ 2162line 2161 didn't jump to line 2162 because the condition on line 2161 was never true
2162 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2163 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2164 raise ex
2166 return self._lowerBound > other
2168 def __ge__(self, other: Any) -> bool:
2169 """
2170 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2172 :param other: Operand to compare against.
2173 :returns: ``True``, if version range is greater than or equal the second operand (version).
2174 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2175 """
2176 # TODO: support VersionRange < VersionRange too
2177 # TODO: support str, int, ... like Version ?
2178 if not isinstance(other, Version): 2178 ↛ 2179line 2178 didn't jump to line 2179 because the condition on line 2178 was never true
2179 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2180 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2181 raise ex
2183 if not (isinstance(other, self._upperBound.__class__) and isinstance(self._upperBound, other.__class__)): 2183 ↛ 2184line 2183 didn't jump to line 2184 because the condition on line 2183 was never true
2184 ex = TypeError(f"Parameter 'other' is not compatible with version range.")
2185 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2186 raise ex
2188 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2188 ↛ 2189line 2188 didn't jump to line 2189 because the condition on line 2188 was never true
2189 return self._lowerBound > other
2190 else:
2191 return self._lowerBound >= other
2193 def __contains__(self, version: Version) -> bool:
2194 """
2195 Check if the version is in the version range.
2197 :param version: Version to check.
2198 :returns: ``True``, if version is in range.
2199 :raises TypeError: If parameter ``version`` is not of type :class:`Version`.
2200 """
2201 if not isinstance(version, Version): 2201 ↛ 2202line 2201 didn't jump to line 2202 because the condition on line 2201 was never true
2202 ex = TypeError(f"Parameter 'item' is not of type 'Version'.")
2203 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2204 raise ex
2206 if self._boundHandling is RangeBoundHandling.BothBoundsInclusive: 2206 ↛ 2208line 2206 didn't jump to line 2208 because the condition on line 2206 was always true
2207 return self._lowerBound <= version <= self._upperBound
2208 elif self._boundHandling is (RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive):
2209 return self._lowerBound <= version < self._upperBound
2210 elif self._boundHandling is (RangeBoundHandling.LowerBoundExclusive | RangeBoundHandling.UpperBoundInclusive):
2211 return self._lowerBound < version <= self._upperBound
2212 else:
2213 return self._lowerBound < version < self._upperBound
2216@export
2217class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2218 """
2219 Representation of an ordered set of versions.
2221 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2222 """
2223 _items: List[V] #: An ordered list of set members.
2225 def __init__(self, versions: Union[Version, Iterable[V]]) -> None:
2226 """
2227 Initializes a version set either by a single version or an iterable of versions.
2229 :param versions: A single version or an iterable of versions.
2230 :raises ValueError: If parameter ``versions`` is None`.
2231 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2232 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2233 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2234 """
2235 if versions is None:
2236 raise ValueError(f"Parameter 'versions' is None.")
2238 if isinstance(versions, Version):
2239 self._items = [versions]
2240 elif isinstance(versions, abc_Iterable): 2240 ↛ 2258line 2240 didn't jump to line 2258 because the condition on line 2240 was always true
2241 iterator = iter(versions)
2242 try:
2243 firstVersion = next(iterator)
2244 except StopIteration:
2245 self._items = []
2246 return
2248 if not isinstance(firstVersion, Version): 2248 ↛ 2249line 2248 didn't jump to line 2249 because the condition on line 2248 was never true
2249 raise TypeError(f"First element in parameter 'versions' is not of type Version.")
2251 baseType = firstVersion.__class__
2252 for version in iterator:
2253 if not isinstance(version, baseType):
2254 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2256 self._items = list(sorted(versions))
2257 else:
2258 raise TypeError(f"Parameter 'versions' is not an Iterable.")
2260 def __and__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2261 """
2262 Compute intersection of two version sets.
2264 :param other: Second set of versions.
2265 :returns: Intersection of two version sets.
2266 """
2267 selfIterator = self.__iter__()
2268 otherIterator = other.__iter__()
2270 result = []
2271 try:
2272 selfValue = next(selfIterator)
2273 otherValue = next(otherIterator)
2275 while True:
2276 if selfValue < otherValue:
2277 selfValue = next(selfIterator)
2278 elif otherValue < selfValue:
2279 otherValue = next(otherIterator)
2280 else:
2281 result.append(selfValue)
2282 selfValue = next(selfIterator)
2283 otherValue = next(otherIterator)
2285 except StopIteration:
2286 pass
2288 return VersionSet(result)
2290 def __or__(self, other: "VersionSet[V]") -> "VersionSet[T]":
2291 """
2292 Compute union of two version sets.
2294 :param other: Second set of versions.
2295 :returns: Union of two version sets.
2296 """
2297 selfIterator = self.__iter__()
2298 otherIterator = other.__iter__()
2300 result = []
2301 try:
2302 selfValue = next(selfIterator)
2303 except StopIteration:
2304 for otherValue in otherIterator:
2305 result.append(otherValue)
2307 try:
2308 otherValue = next(otherIterator)
2309 except StopIteration:
2310 for selfValue in selfIterator:
2311 result.append(selfValue)
2313 while True:
2314 if selfValue < otherValue:
2315 result.append(selfValue)
2316 try:
2317 selfValue = next(selfIterator)
2318 except StopIteration:
2319 result.append(otherValue)
2320 for otherValue in otherIterator: 2320 ↛ 2321line 2320 didn't jump to line 2321 because the loop on line 2320 never started
2321 result.append(otherValue)
2323 break
2324 elif otherValue < selfValue:
2325 result.append(otherValue)
2326 try:
2327 otherValue = next(otherIterator)
2328 except StopIteration:
2329 result.append(selfValue)
2330 for selfValue in selfIterator:
2331 result.append(selfValue)
2333 break
2334 else:
2335 result.append(selfValue)
2336 try:
2337 selfValue = next(selfIterator)
2338 except StopIteration:
2339 for otherValue in otherIterator: 2339 ↛ 2340line 2339 didn't jump to line 2340 because the loop on line 2339 never started
2340 result.append(otherValue)
2342 break
2344 try:
2345 otherValue = next(otherIterator)
2346 except StopIteration:
2347 for selfValue in selfIterator:
2348 result.append(selfValue)
2350 break
2352 return VersionSet(result)
2354 def __lt__(self, other: Any) -> bool:
2355 """
2356 Compare a version set and a version numbers if the version set is less than the second operand (version).
2358 :param other: Operand to compare against.
2359 :returns: ``True``, if version set is less 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):
2365 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2366 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2367 raise ex
2369 return self._items[-1] < other
2371 def __le__(self, other: Any) -> bool:
2372 """
2373 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
2375 :param other: Operand to compare against.
2376 :returns: ``True``, if version set is less than or equal the second operand (version).
2377 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2378 """
2379 # TODO: support VersionRange < VersionRange too
2380 # TODO: support str, int, ... like Version ?
2381 if not isinstance(other, Version): 2381 ↛ 2382line 2381 didn't jump to line 2382 because the condition on line 2381 was never true
2382 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2383 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2384 raise ex
2386 return self._items[-1] <= other
2388 def __gt__(self, other: Any) -> bool:
2389 """
2390 Compare a version set and a version numbers if the version set is greater than the second operand (version).
2392 :param other: Operand to compare against.
2393 :returns: ``True``, if version set is greater than the second operand (version).
2394 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2395 """
2396 # TODO: support VersionRange < VersionRange too
2397 # TODO: support str, int, ... like Version ?
2398 if not isinstance(other, Version): 2398 ↛ 2399line 2398 didn't jump to line 2399 because the condition on line 2398 was never true
2399 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2400 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2401 raise ex
2403 return self._items[0] > other
2405 def __ge__(self, other: Any) -> bool:
2406 """
2407 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
2409 :param other: Operand to compare against.
2410 :returns: ``True``, if version set is greater than or equal the second operand (version).
2411 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2412 """
2413 # TODO: support VersionRange < VersionRange too
2414 # TODO: support str, int, ... like Version ?
2415 if not isinstance(other, Version): 2415 ↛ 2416line 2415 didn't jump to line 2416 because the condition on line 2415 was never true
2416 ex = TypeError(f"Parameter 'other' is not of type 'Version'.")
2417 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2418 raise ex
2420 return self._items[0] >= other
2422 def __contains__(self, version: V) -> bool:
2423 """
2424 Checks if the version a member of the set.
2426 :param version: The version to check.
2427 :returns: ``True``, if the version is a member of the set.
2428 """
2429 return version in self._items
2431 def __len__(self) -> int:
2432 """
2433 Returns the number of members in the set.
2435 :returns: Number of set members.
2436 """
2437 return len(self._items)
2439 def __iter__(self) -> Iterator[V]:
2440 """
2441 Returns an iterator to iterate all versions of this set from lowest to highest.
2443 :returns: Iterator to iterate versions.
2444 """
2445 return self._items.__iter__()
2447 def __getitem__(self, index: int) -> V:
2448 """
2449 Access to a version of a set by index.
2451 :param index: The index of the version to access.
2452 :returns: The indexed version.
2454 .. hint::
2456 Versions are ordered from lowest to highest version number.
2457 """
2458 return self._items[index]