Coverage for pyTooling/Versioning/__init__.py: 90%
1463 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
1# ==================================================================================================================== #
2# _____ _ _ __ __ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32Implementation of semantic and date versioning version-numbers.
34.. hint::
36 See :ref:`high-level help <VERSIONING>` for explanations and usage examples.
38.. seealso::
40 :mod:`pyTooling.Packaging`
41 |rarr| Reading a package's version from its dunder variables.
42 :mod:`pyTooling.Dependency`
43 |rarr| Resolving requirements against these version numbers.
44"""
45from __future__ import annotations
47from collections.abc import Iterable as abc_Iterable
48from enum import Flag, Enum
49from re import compile as re_compile, escape as re_escape, Pattern
50from sys import version_info
51from typing import Optional as Nullable, Union, Any, ClassVar, Generic, TypeVar, Iterable, Iterator, \
52 Self
54from pyTooling.Decorators import export, readonly
55from pyTooling.MetaClasses import ExtendedType, abstractmethod, mustoverride
56from pyTooling.Exceptions import ToolingException
57from pyTooling.Common import getFullyQualifiedName, getTypeName
58from pyTooling.DataModel import Predicate
61@export
62class VersionValidatorError(ToolingException):
63 """
64 Raised when a parsed version is rejected by the validator it was parsed with.
66 The version string itself was well-formed - it parsed - so this is not a :exc:`ValueError` about the input, but
67 a statement that the resulting version is not acceptable to the caller. The version that failed is carried in
68 :attr:`Version`, so a caller can report what was wrong with it.
69 """
71 _version: Version #: The version rejected by a validator.
73 def __init__(self, message: str, /, *, version: Version) -> None:
74 """
75 Initializes the exception with the rejected version.
77 :param message: The exception's message.
78 :param version: The version the validator rejected.
79 """
80 super().__init__(message)
81 self._version = version
83 @readonly
84 def Version(self) -> Version:
85 """
86 Read-only property to access the version the validator rejected (:attr:`_version`).
88 :returns: The rejected version.
89 """
90 return self._version
93@export
94class Parts(Flag):
95 """Enumeration describing parts of a version number that can be present."""
96 Unknown = 0 #: Undocumented
97 Epoch = 1 #: Epoch is present. (e.g. E in ``E:1.2.3`` or ``vE!1.2.3``)
98 Major = 2 #: Major number is present. (e.g. X in ``vX.0.0``).
99 Year = 2 #: Year is present. (e.g. X in ``XXXX.10``).
100 Minor = 4 #: Minor number is present. (e.g. Y in ``v0.Y.0``).
101 Month = 4 #: Month is present. (e.g. X in ``2024.YY``).
102 Week = 4 #: Week is present. (e.g. X in ``2024.YY``).
103 Micro = 8 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
104 Patch = 8 #: Patch number is present. (e.g. Z in ``v0.0.Z``).
105 Day = 8 #: Day is present. (e.g. X in ``2024.10.ZZ``).
106 Level = 16 #: Release level is present.
107 Dev = 32 #: Development part is present.
108 Build = 64 #: Build number is present. (e.g. bbbb in ``v0.0.0.bbbb``)
109 Post = 128 #: Post-release number is present.
110 Prefix = 256 #: Prefix is present.
111 Postfix = 512 #: Postfix is present.
112 Hash = 1024 #: Hash is present.
113# AHead = 256
116@export
117class ReleaseLevel(Enum):
118 """Enumeration describing the version's maturity level."""
119 Final = 0 #:
120 ReleaseCandidate = -10 #:
121 Development = -20 #:
122 Gamma = -30 #:
123 Beta = -40 #:
124 Alpha = -50 #:
126 def __eq__(self, other: Any) -> bool:
127 """
128 Compare two release levels if the level is equal to the second operand.
130 :param other: Operand to compare against.
131 :returns: ``True``, if release level is equal the second operand's release level.
132 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
133 """
134 if isinstance(other, str): 134 ↛ 135line 134 didn't jump to line 135 because the condition on line 134 was never true
135 other = ReleaseLevel(other)
137 if not isinstance(other, ReleaseLevel): 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true
138 ex = TypeError("Second operand is not supported by == operator.")
139 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
140 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.")
141 raise ex
143 return self is other
145 def __ne__(self, other: Any) -> bool:
146 """
147 Compare two release levels if the level is unequal to the second operand.
149 :param other: Operand to compare against.
150 :returns: ``True``, if release level is unequal the second operand's release level.
151 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
152 """
153 if isinstance(other, str):
154 other = ReleaseLevel(other)
156 if not isinstance(other, ReleaseLevel):
157 ex = TypeError("Second operand is not supported by != operator.")
158 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
159 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.")
160 raise ex
162 return self is not other
164 def __lt__(self, other: Any) -> bool:
165 """
166 Compare two release levels if the level is less than the second operand.
168 :param other: Operand to compare against.
169 :returns: ``True``, if release level is less than the second operand.
170 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
171 """
172 if isinstance(other, str): 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true
173 other = ReleaseLevel(other)
175 if not isinstance(other, ReleaseLevel): 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 ex = TypeError("Second operand is not supported by < operator.")
177 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
178 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.")
179 raise ex
181 return self.value < other.value
183 def __le__(self, other: Any) -> bool:
184 """
185 Compare two release levels if the level is less than or equal the second operand.
187 :param other: Operand to compare against.
188 :returns: ``True``, if release level is less than or equal the second operand.
189 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
190 """
191 if isinstance(other, str):
192 other = ReleaseLevel(other)
194 if not isinstance(other, ReleaseLevel):
195 ex = TypeError("Second operand is not supported by <=>= operator.")
196 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
197 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.")
198 raise ex
200 return self.value <= other.value
202 def __gt__(self, other: Any) -> bool:
203 """
204 Compare two release levels if the level is greater than the second operand.
206 :param other: Operand to compare against.
207 :returns: ``True``, if release level is greater than the second operand.
208 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
209 """
210 if isinstance(other, str): 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true
211 other = ReleaseLevel(other)
213 if not isinstance(other, ReleaseLevel): 213 ↛ 214line 213 didn't jump to line 214 because the condition on line 213 was never true
214 ex = TypeError("Second operand is not supported by > operator.")
215 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
216 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.")
217 raise ex
219 return self.value > other.value
221 def __ge__(self, other: Any) -> bool:
222 """
223 Compare two release levels if the level is greater than or equal the second operand.
225 :param other: Operand to compare against.
226 :returns: ``True``, if release level is greater than or equal the second operand.
227 :raises TypeError: If parameter ``other`` is not of type :class:`ReleaseLevel` or string.
228 """
229 if isinstance(other, str):
230 other = ReleaseLevel(other)
232 if not isinstance(other, ReleaseLevel):
233 ex = TypeError("Second operand is not supported by >= operator.")
234 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
235 ex.add_note(f"Supported types for second operand: {getTypeName(self)} or 'str'.")
236 raise ex
238 return self.value >= other.value
240 def __hash__(self) -> int:
241 """
242 Compute a hash for this release level, so it can be used as a key in a dictionary or an element of a set.
244 The hash is derived from the release level's value, so two release levels compare and hash alike.
246 :returns: Hash of the release level's value.
247 """
248 return hash(self.value)
250 def __str__(self) -> str:
251 """
252 Returns the release level's string equivalent.
254 :returns: The string equivalent of the release level.
255 :raises ToolingException: If the release level is unknown, so it has no string equivalent.
256 """
257 if self is ReleaseLevel.Final:
258 return "final"
259 elif self is ReleaseLevel.ReleaseCandidate:
260 return "rc"
261 elif self is ReleaseLevel.Development: 261 ↛ 262line 261 didn't jump to line 262 because the condition on line 261 was never true
262 return "dev"
263 elif self is ReleaseLevel.Beta: 263 ↛ 264line 263 didn't jump to line 264 because the condition on line 263 was never true
264 return "beta"
265 elif self is ReleaseLevel.Alpha: 265 ↛ 268line 265 didn't jump to line 268 because the condition on line 265 was always true
266 return "alpha"
268 raise ToolingException(f"Unknown ReleaseLevel '{self.name}'.")
271@export
272class Flags(Flag):
273 """State enumeration, if a (tagged) version is build from a clean or dirty working directory."""
274 NoVCS = 0 #: No Version Control System VCS
275 Clean = 1 #: A versioned build was created from a *clean* working directory.
276 Dirty = 2 #: A versioned build was created from a *dirty* working directory.
278 CVS = 16 #: Concurrent Versions System (CVS)
279 SVN = 32 #: Subversion (SVN)
280 Git = 64 #: Git
281 Hg = 128 #: Mercurial (Hg)
284@export
285def WordSizeValidator(
286 bits: Nullable[int] = None,
287 majorBits: Nullable[int] = None,
288 minorBits: Nullable[int] = None,
289 microBits: Nullable[int] = None,
290 buildBits: Nullable[int] = None
291):
292 """
293 A factory function to return a validator for Version instances for a positive integer range based on word-sizes in bits.
295 :param bits: Optional, number of bits to encode any positive version number part.
296 :param majorBits: Optional, number of bits to encode a positive major number in a version.
297 :param minorBits: Optional, number of bits to encode a positive minor number in a version.
298 :param microBits: Optional, number of bits to encode a positive micro number in a version.
299 :param buildBits: Optional, number of bits to encode a positive build number in a version.
300 :returns: A validation function for Version instances.
301 """
302 majorMax = minorMax = microMax = buildMax = -1
303 if bits is not None:
304 majorMax = minorMax = microMax = buildMax = 2**bits - 1
306 if majorBits is not None:
307 majorMax = 2**majorBits - 1
309 if minorBits is not None:
310 minorMax = 2**minorBits - 1
312 if microBits is not None:
313 microMax = 2 ** microBits - 1
315 if buildBits is not None: 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true
316 buildMax = 2**buildBits - 1
318 def validator(version: SemanticVersion) -> bool:
319 """
320 Validator function, which checks each version part against the maximum its word size allows.
322 :param version: The version to validate.
323 :returns: ``True``, if every part fits into its word size.
324 :raises ValueError: If a part exceeds the maximum value of its word size.
325 """
326 if Parts.Major in version._parts and version._major > majorMax:
327 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
329 if Parts.Minor in version._parts and version._minor > minorMax:
330 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
332 if Parts.Micro in version._parts and version._micro > microMax:
333 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
335 if Parts.Build in version._parts and version._build > buildMax: 335 ↛ 336line 335 didn't jump to line 336 because the condition on line 335 was never true
336 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
338 return True
340 return validator
343@export
344def MaxValueValidator(
345 max: Nullable[int] = None,
346 majorMax: Nullable[int] = None,
347 minorMax: Nullable[int] = None,
348 microMax: Nullable[int] = None,
349 buildMax: Nullable[int] = None
350):
351 """
352 A factory function to return a validator for Version instances checking for a positive integer range [0..max].
354 :param max: Optional, the upper bound for any positive version number part.
355 :param majorMax: Optional, the upper bound for the positive major number.
356 :param minorMax: Optional, the upper bound for the positive minor number.
357 :param microMax: Optional, the upper bound for the positive micro number.
358 :param buildMax: Optional, the upper bound for the positive build number.
359 :returns: A validation function for Version instances.
360 """
361 if max is not None: 361 ↛ 364line 361 didn't jump to line 364 because the condition on line 361 was always true
362 majorMax = minorMax = microMax = buildMax = max
364 def validator(version: SemanticVersion) -> bool:
365 """
366 Validator function, which checks each version part against its maximum value.
368 :param version: The version to validate.
369 :returns: ``True``, if every part is within its maximum.
370 :raises ValueError: If a part exceeds its maximum value.
371 """
372 if Parts.Major in version._parts and version._major > majorMax:
373 raise ValueError(f"Field 'Version.Major' > {majorMax}.")
375 if Parts.Minor in version._parts and version._minor > minorMax:
376 raise ValueError(f"Field 'Version.Minor' > {minorMax}.")
378 if Parts.Micro in version._parts and version._micro > microMax:
379 raise ValueError(f"Field 'Version.Micro' > {microMax}.")
381 if Parts.Build in version._parts and version._build > buildMax: 381 ↛ 382line 381 didn't jump to line 382 because the condition on line 381 was never true
382 raise ValueError(f"Field 'Version.Build' > {buildMax}.")
384 return True
386 return validator
389@export
390class Version(metaclass=ExtendedType, slots=True):
391 """Base-class for a version representation."""
393 __hash: Nullable[int] #: once computed hash of the object
395 #: Separator between an epoch and the rest of the version number. Debian writes ``2:1.2.3``; PEP 440 writes
396 #: ``2!1.2.3``, so :class:`PythonVersion` overrides this.
397 _EPOCH_SEPARATOR: ClassVar[str] = ":"
399 _parts: Parts #: Integer flag enumeration of present parts in a version number.
400 _prefix: str #: Prefix string
401 _epoch: int #: Epoch, which outranks every other part of the version number.
402 _major: int #: Major number part of the version number.
403 _minor: int #: Minor number part of the version number.
404 _micro: int #: Micro number part of the version number.
405 _releaseLevel: ReleaseLevel #: Release level (alpha, beta, rc, final, ...).
406 _releaseNumber: int #: Release number (Python calls this a serial).
407 _post: int #: Post-release version number part.
408 _dev: int #: Development number
409 _build: int #: Build number part of the version number.
410 _postfix: str #: Postfix string
411 _hash: str #: Hash from version control system.
412 _flags: Flags #: State if the version in a working directory is clean or dirty compared to a tagged version.
414 def __init__(
415 self,
416 major: int,
417 minor: Nullable[int] = None,
418 micro: Nullable[int] = None,
419 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
420 number: Nullable[int] = None,
421 post: Nullable[int] = None,
422 dev: Nullable[int] = None,
423 *,
424 epoch: Nullable[int] = None,
425 build: Nullable[int] = None,
426 postfix: Nullable[str] = None,
427 prefix: Nullable[str] = None,
428 hash: Nullable[str] = None,
429 flags: Flags = Flags.NoVCS
430 ) -> None:
431 """
432 Initializes a version number representation.
434 :param major: Major number part of the version number.
435 :param minor: Optional, minor number part of the version number.
436 :param micro: Optional, micro (patch) number part of the version number.
437 :param level: Optional, release level (alpha, beta, release candidate, final, ...) of the version number.
438 :param number: Optional, release number part (in combination with release level) of the version number.
439 :param post: Optional, post number part of the version number.
440 :param dev: Optional, development number part of the version number.
441 :param epoch: Optional, the version number's epoch, which outranks every other part.
442 :param build: Optional, build number part of the version number.
443 :param postfix: Optional, the version number's postfix.
444 :param prefix: Optional, the version number's prefix.
445 :param hash: Optional, postfix string.
446 :param flags: Optional, the version number's flags.
447 :raises TypeError: If parameter 'major' is not of type integer.
448 :raises ValueError: If parameter 'major' is a negative number.
449 :raises TypeError: If parameter 'minor' is not of type integer.
450 :raises ValueError: If parameter 'minor' is a negative number.
451 :raises TypeError: If parameter 'micro' is not of type integer.
452 :raises ValueError: If parameter 'micro' is a negative number.
453 :raises TypeError: If parameter 'epoch' is not of type integer.
454 :raises ValueError: If parameter 'epoch' is a negative number.
455 :raises TypeError: If parameter 'build' is not of type integer.
456 :raises ValueError: If parameter 'build' is a negative number.
457 :raises TypeError: If parameter 'prefix' is not of type string.
458 :raises TypeError: If parameter 'postfix' is not of type string.
459 """
460 self.__hash = None
462 if not isinstance(major, int):
463 ex = TypeError("Parameter 'major' is not of type 'int'.")
464 ex.add_note(f"Got type '{getFullyQualifiedName(major)}'.")
465 raise ex
466 elif major < 0:
467 raise ValueError("Parameter 'major' is negative.")
469 self._parts = Parts.Major
470 self._major = major
472 if epoch is not None:
473 if not isinstance(epoch, int):
474 ex = TypeError("Parameter 'epoch' is not of type 'int'.")
475 ex.add_note(f"Got type '{getFullyQualifiedName(epoch)}'.")
476 raise ex
477 elif epoch < 0:
478 raise ValueError("Parameter 'epoch' is negative.")
480 self._parts |= Parts.Epoch
481 self._epoch = epoch
482 else:
483 self._epoch = 0
485 if minor is not None:
486 if not isinstance(minor, int):
487 ex = TypeError("Parameter 'minor' is not of type 'int'.")
488 ex.add_note(f"Got type '{getFullyQualifiedName(minor)}'.")
489 raise ex
490 elif minor < 0:
491 raise ValueError("Parameter 'minor' is negative.")
493 self._parts |= Parts.Minor
494 self._minor = minor
495 else:
496 self._minor = 0
498 if micro is not None:
499 if not isinstance(micro, int):
500 ex = TypeError("Parameter 'micro' is not of type 'int'.")
501 ex.add_note(f"Got type '{getFullyQualifiedName(micro)}'.")
502 raise ex
503 elif micro < 0:
504 raise ValueError("Parameter 'micro' is negative.")
506 self._parts |= Parts.Micro
507 self._micro = micro
508 else:
509 self._micro = 0
511 if level is None:
512 raise ValueError("Parameter 'level' is None.")
513 elif not isinstance(level, ReleaseLevel):
514 ex = TypeError("Parameter 'level' is not of type 'ReleaseLevel'.")
515 ex.add_note(f"Got type '{getFullyQualifiedName(level)}'.")
516 raise ex
517 elif level is ReleaseLevel.Final:
518 if number is not None:
519 raise ValueError("Parameter 'number' must be None, if parameter 'level' is 'Final'.")
521 self._parts |= Parts.Level
522 self._releaseLevel = level
523 self._releaseNumber = 0
524 else:
525 self._parts |= Parts.Level
526 self._releaseLevel = level
528 if number is not None:
529 if not isinstance(number, int):
530 ex = TypeError("Parameter 'number' is not of type 'int'.")
531 ex.add_note(f"Got type '{getFullyQualifiedName(number)}'.")
532 raise ex
533 elif number < 0:
534 raise ValueError("Parameter 'number' is negative.")
536 self._releaseNumber = number
537 else:
538 self._releaseNumber = 0
540 if dev is not None:
541 if not isinstance(dev, int):
542 ex = TypeError("Parameter 'dev' is not of type 'int'.")
543 ex.add_note(f"Got type '{getFullyQualifiedName(dev)}'.")
544 raise ex
545 elif dev < 0:
546 raise ValueError("Parameter 'dev' is negative.")
548 self._parts |= Parts.Dev
549 self._dev = dev
550 else:
551 self._dev = 0
553 if post is not None:
554 if not isinstance(post, int):
555 ex = TypeError("Parameter 'post' is not of type 'int'.")
556 ex.add_note(f"Got type '{getFullyQualifiedName(post)}'.")
557 raise ex
558 elif post < 0:
559 raise ValueError("Parameter 'post' is negative.")
561 self._parts |= Parts.Post
562 self._post = post
563 else:
564 self._post = 0
566 if build is not None:
567 if not isinstance(build, int):
568 ex = TypeError("Parameter 'build' is not of type 'int'.")
569 ex.add_note(f"Got type '{getFullyQualifiedName(build)}'.")
570 raise ex
571 elif build < 0:
572 raise ValueError("Parameter 'build' is negative.")
574 self._build = build
575 self._parts |= Parts.Build
576 else:
577 self._build = 0
579 if postfix is not None:
580 if not isinstance(postfix, str):
581 ex = TypeError("Parameter 'postfix' is not of type 'str'.")
582 ex.add_note(f"Got type '{getFullyQualifiedName(postfix)}'.")
583 raise ex
585 self._parts |= Parts.Postfix
586 self._postfix = postfix
587 else:
588 self._postfix = ""
590 if prefix is not None:
591 if not isinstance(prefix, str):
592 ex = TypeError("Parameter 'prefix' is not of type 'str'.")
593 ex.add_note(f"Got type '{getFullyQualifiedName(prefix)}'.")
594 raise ex
596 self._parts |= Parts.Prefix
597 self._prefix = prefix
598 else:
599 self._prefix = ""
601 if hash is not None:
602 if not isinstance(hash, str):
603 ex = TypeError("Parameter 'hash' is not of type 'str'.")
604 ex.add_note(f"Got type '{getFullyQualifiedName(hash)}'.")
605 raise ex
607 self._parts |= Parts.Hash
608 self._hash = hash
609 else:
610 self._hash = ""
612 if flags is None:
613 raise ValueError("Parameter 'flags' is None.")
614 elif not isinstance(flags, Flags):
615 ex = TypeError("Parameter 'flags' is not of type 'Flags'.")
616 ex.add_note(f"Got type '{getFullyQualifiedName(flags)}'.")
617 raise ex
619 self._flags = flags
621 @classmethod
622 @abstractmethod
623 def Parse(cls, versionString: Nullable[str], validator: Nullable[Predicate[SemanticVersion]] = None) -> Version:
624 """
625 Parse a version string and return a Version instance.
627 :param versionString: The version string to parse.
628 :param validator: Optional, validator rejecting a parsed version, e.g. by word size or maximum value.
629 :returns: The parsed version number.
630 """
632 @readonly
633 def Parts(self) -> Parts:
634 """
635 Read-only property to access the used parts of this version number.
637 :returns: A flag enumeration of used version number parts.
638 """
639 return self._parts
641 @readonly
642 def Prefix(self) -> str:
643 """
644 Read-only property to access the version number's prefix.
646 :returns: The prefix of the version number.
647 """
648 return self._prefix
650 @readonly
651 def Epoch(self) -> int:
652 """
653 Read-only property to access the epoch (:attr:`_epoch`).
655 An epoch outranks every other part, so a version carrying one is newer than any version with a lower epoch
656 however high the rest of its numbers are. It exists so a project can *lower* its version number - change
657 scheme, or recover from a bad release - without every later version comparing as older. A version without one
658 has epoch ``0``, which is what keeps it comparable with a version that has one.
660 :returns: The epoch, or ``0`` if the version carries none.
661 """
662 return self._epoch
664 @readonly
665 def Major(self) -> int:
666 """
667 Read-only property to access the major number.
669 :returns: The major number.
670 """
671 return self._major
673 @readonly
674 def Minor(self) -> int:
675 """
676 Read-only property to access the minor number.
678 :returns: The minor number.
679 """
680 return self._minor
682 @readonly
683 def Micro(self) -> int:
684 """
685 Read-only property to access the micro number.
687 :returns: The micro number.
688 """
689 return self._micro
691 @readonly
692 def ReleaseLevel(self) -> ReleaseLevel:
693 """
694 Read-only property to access the release level.
696 :returns: The release level.
697 """
698 return self._releaseLevel
700 @readonly
701 def ReleaseNumber(self) -> int:
702 """
703 Read-only property to access the release number.
705 :returns: The release number.
706 """
707 return self._releaseNumber
709 @readonly
710 def Post(self) -> int:
711 """
712 Read-only property to access the post number.
714 :returns: The post number.
715 """
716 return self._post
718 @readonly
719 def Dev(self) -> int:
720 """
721 Read-only property to access the development number.
723 :returns: The development number.
724 """
725 return self._dev
727 @readonly
728 def Build(self) -> int:
729 """
730 Read-only property to access the build number.
732 :returns: The build number.
733 """
734 return self._build
736 @readonly
737 def Postfix(self) -> str:
738 """
739 Read-only property to access the version number's postfix.
741 :returns: The postfix of the version number.
742 """
743 return self._postfix
745 @readonly
746 def Hash(self) -> str:
747 """
748 Read-only property to access the version number's hash.
750 :returns: The hash.
751 """
752 return self._hash
754 @readonly
755 def Flags(self) -> Flags:
756 """
757 Read-only property to access the version number's flags.
759 :returns: The flags of the version number.
760 """
761 return self._flags
763 def _equal(self, left: Version, right: Version) -> Nullable[bool]:
764 """
765 Private helper method to compute the equality of two :class:`Version` instances.
767 :param left: Left operand.
768 :param right: Right operand.
769 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
770 """
771 return (
772 (left._epoch == right._epoch) and
773 (left._major == right._major) and
774 (left._minor == right._minor) and
775 (left._micro == right._micro) and
776 (left._releaseLevel == right._releaseLevel) and
777 (left._releaseNumber == right._releaseNumber) and
778 ((Parts.Post in left._parts) == (Parts.Post in right._parts)) and
779 (left._post == right._post) and
780 ((Parts.Dev in left._parts) == (Parts.Dev in right._parts)) and
781 (left._dev == right._dev) and
782 (left._build == right._build) and
783 (left._postfix == right._postfix)
784 )
786 def _compare(self, left: Version, right: Version) -> Nullable[bool]:
787 """
788 Private helper method to compute the comparison of two :class:`Version` instances.
790 :param left: Left operand.
791 :param right: Right operand.
792 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
793 False if ``left`` is greater than ``right``. |br|
794 Otherwise it's None (both operands are equal).
795 """
796 if left._epoch < right._epoch:
797 return True
798 elif left._epoch > right._epoch:
799 return False
801 if left._major < right._major:
802 return True
803 elif left._major > right._major:
804 return False
806 if left._minor < right._minor:
807 return True
808 elif left._minor > right._minor:
809 return False
811 if left._micro < right._micro:
812 return True
813 elif left._micro > right._micro:
814 return False
816 if left._releaseLevel < right._releaseLevel:
817 return True
818 elif left._releaseLevel > right._releaseLevel: 818 ↛ 819line 818 didn't jump to line 819 because the condition on line 818 was never true
819 return False
821 if left._releaseNumber < right._releaseNumber:
822 return True
823 elif left._releaseNumber > right._releaseNumber: 823 ↛ 824line 823 didn't jump to line 824 because the condition on line 823 was never true
824 return False
826 leftPost = Parts.Post in left._parts
827 rightPost = Parts.Post in right._parts
828 if leftPost != rightPost:
829 return rightPost
830 elif left._post < right._post: 830 ↛ 831line 830 didn't jump to line 831 because the condition on line 830 was never true
831 return True
832 elif left._post > right._post: 832 ↛ 833line 832 didn't jump to line 833 because the condition on line 832 was never true
833 return False
835 leftDev = Parts.Dev in left._parts
836 rightDev = Parts.Dev in right._parts
837 if leftDev != rightDev:
838 return leftDev
839 elif left._dev < right._dev:
840 return True
841 elif left._dev > right._dev:
842 return False
844 if left._build < right._build: 844 ↛ 845line 844 didn't jump to line 845 because the condition on line 844 was never true
845 return True
846 elif left._build > right._build: 846 ↛ 847line 846 didn't jump to line 847 because the condition on line 846 was never true
847 return False
849 return None
851 def _minimum(self, actual: Version, expected: Version) -> Nullable[bool]:
852 """
853 Check if a version fulfills a minimum requirement.
855 How exact the comparison is depends on how detailed the expected version is: a minor number in the expectation
856 requires an exact major number, and a micro number requires an exact minor number.
858 :param actual: The version to check.
859 :param expected: The minimum version, whose parts decide how exact the comparison is.
860 :returns: ``True``, if the actual version fulfills the expectation.
861 """
862 exactMajor = Parts.Minor in expected._parts
863 exactMinor = Parts.Micro in expected._parts
865 if exactMajor and actual._major != expected._major: 865 ↛ 866line 865 didn't jump to line 866 because the condition on line 865 was never true
866 return False
867 elif not exactMajor and actual._major < expected._major:
868 return False
870 if exactMinor and actual._minor != expected._minor: 870 ↛ 871line 870 didn't jump to line 871 because the condition on line 870 was never true
871 return False
872 elif not exactMinor and actual._minor < expected._minor:
873 return False
875 if Parts.Micro in expected._parts:
876 return actual._micro >= expected._micro
878 return True
880 def _format(self, formatSpec: str) -> str:
881 """
882 Return a string representation of this version number according to the format specification.
884 .. topic:: Format Specifiers
886 * ``%p`` - prefix
887 * ``%M`` - major number
888 * ``%m`` - minor number
889 * ``%u`` - micro number
890 * ``%b`` - build number
892 :param formatSpec: The format specification.
893 :returns: Formatted version number.
894 """
895 if formatSpec == "":
896 return self.__str__()
898 result = formatSpec
899 result = result.replace("%p", str(self._prefix))
900 result = result.replace("%M", str(self._major))
901 result = result.replace("%m", str(self._minor))
902 result = result.replace("%u", str(self._micro))
903 result = result.replace("%b", str(self._build))
904 result = result.replace("%r", str(self._releaseLevel)[0])
905 result = result.replace("%R", str(self._releaseLevel))
906 result = result.replace("%n", str(self._releaseNumber))
907 result = result.replace("%d", str(self._dev))
908 result = result.replace("%P", str(self._postfix))
910 return result
912 @mustoverride
913 def __eq__(self, other: Any) -> bool:
914 """
915 Compare two version numbers for equality.
917 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
918 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
919 number is assumed (all other parts are zero).
921 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
922 number.
924 :param other: Operand to compare against.
925 :returns: ``True``, if both version numbers are equal.
926 :raises ValueError: If parameter ``other`` is None.
927 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer.
928 """
929 if other is None:
930 raise ValueError("Second operand is None.")
931 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
932 pass
933 elif isinstance(other, str):
934 other = self.__class__.Parse(other)
935 elif isinstance(other, int):
936 other = self.__class__(major=other)
937 else:
938 ex = TypeError("Second operand is not supported by == operator.")
939 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
940 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, str, int")
941 raise ex
943 return self._equal(self, other)
945 @mustoverride
946 def __ne__(self, other: Any) -> bool:
947 """
948 Compare two version numbers for inequality.
950 The second operand should be an instance of :class:`Version`, but ``str`` and ``int`` are accepted, too. |br|
951 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
952 number is assumed (all other parts are zero).
954 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
955 number.
957 :param other: Operand to compare against.
958 :returns: ``True``, if both version numbers are not equal.
959 :raises ValueError: If parameter ``other`` is None.
960 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, string or integer.
961 """
962 if other is None:
963 raise ValueError("Second operand is None.")
964 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
965 pass
966 elif isinstance(other, str):
967 other = self.__class__.Parse(other)
968 elif isinstance(other, int):
969 other = self.__class__(major=other)
970 else:
971 ex = TypeError("Second operand is not supported by == operator.")
972 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
973 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, str, int")
974 raise ex
976 return not self._equal(self, other)
978 @mustoverride
979 def __lt__(self, other: Any) -> bool:
980 """
981 Compare two version numbers if the version is less than the second operand.
983 The second operand should be an instance of :class:`Version`, but :class:`VersionRange`, :class:`VersionSet`,
984 ``str`` and ``int`` are accepted, too. |br|
985 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
986 number is assumed (all other parts are zero).
988 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
989 number.
991 :param other: Operand to compare against.
992 :returns: ``True``, if version is less than the second operand.
993 :raises ValueError: If parameter ``other`` is None.
994 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
995 :class:`VersionSet`, string or integer.
996 """
997 if other is None:
998 raise ValueError("Second operand is None.")
999 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1000 pass
1001 elif isinstance(other, VersionRange):
1002 other = other._lowerBound
1003 elif isinstance(other, VersionSet):
1004 other = other._items[0]
1005 elif isinstance(other, str):
1006 other = self.__class__.Parse(other)
1007 elif isinstance(other, int):
1008 other = self.__class__(major=other)
1009 else:
1010 ex = TypeError("Second operand is not supported by < operator.")
1011 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1012 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int")
1013 raise ex
1015 return self._compare(self, other) is True
1017 @mustoverride
1018 def __le__(self, other: Any) -> bool:
1019 """
1020 Compare two version numbers if the version is less than or equal the second operand.
1022 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1023 ``str`` and ``int`` are accepted, too. |br|
1024 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1025 number is assumed (all other parts are zero).
1027 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1028 number.
1030 :param other: Operand to compare against.
1031 :returns: ``True``, if version is less than or equal the second operand.
1032 :raises ValueError: If parameter ``other`` is None.
1033 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1034 :class:`VersionSet`, string or integer.
1035 """
1036 equalValue = True
1037 if other is None:
1038 raise ValueError("Second operand is None.")
1039 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1040 pass
1041 elif isinstance(other, VersionRange):
1042 equalValue = RangeBoundHandling.LowerBoundExclusive not in other._boundHandling
1043 other = other._lowerBound
1044 elif isinstance(other, VersionSet):
1045 other = other._items[0]
1046 elif isinstance(other, str):
1047 other = self.__class__.Parse(other)
1048 elif isinstance(other, int):
1049 other = self.__class__(major=other)
1050 else:
1051 ex = TypeError("Second operand is not supported by <= operator.")
1052 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1053 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int")
1054 raise ex
1056 result = self._compare(self, other)
1057 return result if result is not None else equalValue
1059 @mustoverride
1060 def __gt__(self, other: Any) -> bool:
1061 """
1062 Compare two version numbers if the version is greater than the second operand.
1064 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1065 ``str`` and ``int`` are accepted, too. |br|
1066 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1067 number is assumed (all other parts are zero).
1069 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1070 number.
1072 :param other: Operand to compare against.
1073 :returns: ``True``, if version is greater than the second operand.
1074 :raises ValueError: If parameter ``other`` is None.
1075 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1076 :class:`VersionSet`, string or integer.
1077 """
1078 if other is None:
1079 raise ValueError("Second operand is None.")
1080 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1081 pass
1082 elif isinstance(other, VersionRange):
1083 other = other._upperBound
1084 elif isinstance(other, VersionSet):
1085 other = other._items[-1]
1086 elif isinstance(other, str):
1087 other = self.__class__.Parse(other)
1088 elif isinstance(other, int):
1089 other = self.__class__(major=other)
1090 else:
1091 ex = TypeError("Second operand is not supported by > operator.")
1092 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1093 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int")
1094 raise ex
1096 return self._compare(self, other) is False
1098 @mustoverride
1099 def __ge__(self, other: Any) -> bool:
1100 """
1101 Compare two version numbers if the version is greater than or equal the second operand.
1103 The second operand should be an instance of :class:`Version`, :class:`VersionRange`, :class:`VersionSet`, but
1104 ``str`` and ``int`` are accepted, too. |br|
1105 In case of ``str``, it's tried to parse the string as a version number. In case of ``int``, a single major
1106 number is assumed (all other parts are zero).
1108 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1109 number.
1111 :param other: Operand to compare against.
1112 :returns: ``True``, if version is greater than or equal the second operand.
1113 :raises ValueError: If parameter ``other`` is None.
1114 :raises TypeError: If parameter ``other`` is not of type :class:`Version`, :class:`VersionRange`,
1115 :class:`VersionSet`, string or integer.
1116 """
1117 equalValue = True
1118 if other is None:
1119 raise ValueError("Second operand is None.")
1120 elif ((sC := self.__class__) is (oC := other.__class__) or issubclass(sC, oC) or issubclass(oC, sC)):
1121 pass
1122 elif isinstance(other, VersionRange):
1123 equalValue = RangeBoundHandling.UpperBoundExclusive not in other._boundHandling
1124 other = other._upperBound
1125 elif isinstance(other, VersionSet):
1126 other = other._items[-1]
1127 elif isinstance(other, str):
1128 other = self.__class__.Parse(other)
1129 elif isinstance(other, int):
1130 other = self.__class__(major=other)
1131 else:
1132 ex = TypeError("Second operand is not supported by >= operator.")
1133 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1134 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, VersionRange, VersionSet, str, int")
1135 raise ex
1137 result = self._compare(self, other)
1138 return not result if result is not None else equalValue
1140 def __rshift__(self, other: Nullable[Union[Version, str, int]]) -> bool:
1141 """
1142 Return the minimum of this version and a second operand.
1144 :param other: Second operand, a version, a version string or a major version number.
1145 :returns: ``True``, if this version is the minimum of both operands.
1146 :raises ValueError: If the second operand is ``None``.
1147 :raises TypeError: If the second operand is not a version, a string or an integer.
1148 """
1149 if other is None:
1150 raise ValueError("Second operand is None.")
1151 elif isinstance(other, self.__class__):
1152 pass
1153 elif isinstance(other, str):
1154 other = self.__class__.Parse(other)
1155 elif isinstance(other, int):
1156 other = self.__class__(major=other)
1157 else:
1158 ex = TypeError("Second operand is not supported by >> operator.")
1159 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
1160 ex.add_note(f"Supported types for second operand: {getTypeName(self)}, str, int")
1161 raise ex
1163 return self._minimum(self, other)
1165 def __hash__(self) -> int:
1166 """
1167 Compute a hash for this version number and cache it.
1169 All parts of the version are part of the hash, so two versions differing in a postfix or a build number don't
1170 collide.
1172 :returns: Hash of this version number.
1173 """
1174 if self.__hash is None:
1175 self.__hash = hash((
1176 self._prefix,
1177 self._epoch,
1178 self._major,
1179 self._minor,
1180 self._micro,
1181 self._releaseLevel,
1182 self._releaseNumber,
1183 self._post,
1184 self._dev,
1185 self._build,
1186 self._postfix,
1187 self._hash,
1188 self._flags
1189 ))
1190 return self.__hash
1193@export
1194class SemanticVersion(Version):
1195 """Representation of a semantic version number like ``3.7.12``."""
1197 _PATTERN: ClassVar[Pattern] = re_compile(
1198 r"^"
1199 r"(?P<prefix>rev|REV|[vViIrR])?"
1200 r"(?:(?P<epoch>\d+):)?"
1201 r"(?P<major>\d+)"
1202 r"(?:\.(?P<minor>\d+))?"
1203 r"(?:\.(?P<micro>\d+))?"
1204 r"(?:"
1205 r"(?:\.(?P<build>\d+))"
1206 r"|"
1207 r"(?:[\.\-]?(?P<release>dev|final))"
1208 r"|"
1209 r"(?:(?P<delim1>[\.\-]?)(?P<level>alpha|beta|gamma|preview|pre|a|b|c|rc|pl)(?P<number>\d*))"
1210 r")?"
1211 r"(?:(?P<delim2>[\.\-]post)(?P<post>\d+))?"
1212 r"(?:(?P<delim3>[\.\-]dev)(?P<dev>\d+))?"
1213 r"(?:(?P<delim4>[\.\-\+])(?P<postfix>\w+))?"
1214 r"$"
1215 ) #: Regular expression to parse a semantic version from a string.
1217 #: Spellings of a release level, e.g. ``pre`` and ``preview`` for a release candidate.
1218 _RELEASE_LEVEL_SPELLINGS: ClassVar[dict[ReleaseLevel, tuple[str, ...]]] = {
1219 ReleaseLevel.Alpha: ("a", "alpha"),
1220 ReleaseLevel.Beta: ("b", "beta"),
1221 ReleaseLevel.Gamma: ("c", "gamma"),
1222 ReleaseLevel.ReleaseCandidate: ("rc", "pre", "preview")
1223 }
1225 _releaseLevelSpelling: str #: Spelling of the release level as parsed, or ``""`` for the class' own spelling.
1226# QUESTION: was this how many commits a version is ahead of the last tagged version?
1227# ahead: int = 0
1229 def __init_subclass__(cls, **kwargs: Any) -> None:
1230 """
1231 Rebuild the pattern when a derived class spells the epoch separator differently.
1233 A compiled pattern still carries the expression it was built from, so the base class' one is taken apart and
1234 put back together with this class' separator. Only the epoch's separator is substituted, and only when the
1235 class states no pattern of its own - a class replacing the whole expression means it.
1237 :param kwargs: Keyword arguments passed on to the base implementation.
1238 """
1239 super().__init_subclass__(**kwargs)
1241 if "_PATTERN" in cls.__dict__ or cls._EPOCH_SEPARATOR == SemanticVersion._EPOCH_SEPARATOR:
1242 return
1244 cls._PATTERN = re_compile(SemanticVersion._PATTERN.pattern.replace(
1245 f"(?P<epoch>\\d+){re_escape(SemanticVersion._EPOCH_SEPARATOR)}",
1246 f"(?P<epoch>\\d+){re_escape(cls._EPOCH_SEPARATOR)}"
1247 ))
1249 def __init__(
1250 self,
1251 major: int,
1252 minor: Nullable[int] = None,
1253 micro: Nullable[int] = None,
1254 level: Nullable[ReleaseLevel] = ReleaseLevel.Final,
1255 number: Nullable[int] = None,
1256 post: Nullable[int] = None,
1257 dev: Nullable[int] = None,
1258 *,
1259 epoch: Nullable[int] = None,
1260 build: Nullable[int] = None,
1261 postfix: Nullable[str] = None,
1262 prefix: Nullable[str] = None,
1263 hash: Nullable[str] = None,
1264 flags: Flags = Flags.NoVCS,
1265 spelling: Nullable[str] = None
1266 ) -> None:
1267 """
1268 Initializes a semantic version number representation.
1270 :param major: Major number part of the version number.
1271 :param minor: Optional, minor number part of the version number.
1272 :param micro: Optional, micro (patch) number part of the version number.
1273 :param level: Optional, release level of the version number (alpha, beta, release candidate, final, ...).
1274 :param number: Optional, number within the release level, e.g. ``2`` in ``rc2``.
1275 :param post: Optional, post number part of the version number.
1276 :param dev: Optional, development number part of the version number.
1277 :param epoch: Optional, the version number's epoch, which outranks every other part.
1278 :param build: Optional, build number part of the version number.
1279 :param postfix: Optional, the version number's postfix.
1280 :param prefix: Optional, the version number's prefix.
1281 :param hash: Optional, hash of the version control system's commit this version was built from.
1282 :param flags: Optional, the version number's flags.
1283 :param spelling: Optional, the release level's spelling, e.g. ``pre`` for a release candidate.
1284 :raises TypeError: If parameter 'major' is not of type integer.
1285 :raises ValueError: If parameter 'major' is a negative number.
1286 :raises TypeError: If parameter 'minor' is not of type integer.
1287 :raises ValueError: If parameter 'minor' is a negative number.
1288 :raises TypeError: If parameter 'micro' is not of type integer.
1289 :raises ValueError: If parameter 'micro' is a negative number.
1290 :raises TypeError: If parameter 'post' is not of type integer.
1291 :raises ValueError: If parameter 'post' is a negative number.
1292 :raises TypeError: If parameter 'dev' is not of type integer.
1293 :raises ValueError: If parameter 'dev' is a negative number.
1294 :raises TypeError: If parameter 'epoch' is not of type integer.
1295 :raises ValueError: If parameter 'epoch' is a negative number.
1296 :raises TypeError: If parameter 'build' is not of type integer.
1297 :raises ValueError: If parameter 'build' is a negative number.
1298 :raises TypeError: If parameter 'prefix' is not of type string.
1299 :raises TypeError: If parameter 'postfix' is not of type string.
1300 :raises TypeError: If parameter 'spelling' is not of type string.
1301 :raises ValueError: If parameter 'spelling' isn't a spelling of the release level.
1302 """
1303 super().__init__(major, minor, micro, level, number, post, dev, epoch=epoch, build=build, postfix=postfix,
1304 prefix=prefix, hash=hash, flags=flags)
1306 if spelling is None:
1307 self._releaseLevelSpelling = ""
1308 elif not isinstance(spelling, str):
1309 ex = TypeError("Parameter 'spelling' is not of type 'str'.")
1310 ex.add_note(f"Got type '{getFullyQualifiedName(spelling)}'.")
1311 raise ex
1312 elif spelling not in (spellings := self._RELEASE_LEVEL_SPELLINGS.get(self._releaseLevel, ())):
1313 ex = ValueError(f"Parameter 'spelling' is '{spelling}', which isn't a spelling of the release level.")
1314 if len(spellings) == 0:
1315 ex.add_note(f"Release level '{self._releaseLevel}' has no spelling.")
1316 else:
1317 ex.add_note(f"Release level '{self._releaseLevel}' is spelled: {', '.join(spellings)}")
1318 raise ex
1319 else:
1320 self._releaseLevelSpelling = spelling
1322 @classmethod
1323 def Parse(cls, versionString: Nullable[str], validator: Nullable[Predicate[SemanticVersion]] = None) -> SemanticVersion:
1324 """
1325 Parse a version string and return a :class:`SemanticVersion` instance.
1327 Allowed prefix characters:
1329 * ``v|V`` - version, public version, public release
1330 * ``i|I`` - internal version, internal release
1331 * ``r|R`` - release, revision
1332 * ``rev|REV`` - revision
1334 :param versionString: The version string to parse.
1335 :param validator: Optional, a validation function.
1336 :returns: An object representing a semantic version.
1337 :raises TypeError: When parameter ``versionString`` is not a string.
1338 :raises ValueError: When parameter ``versionString`` is None or empty.
1339 :raises ValueError: When parameter ``versionString`` isn't a semantic version number. |br|
1340 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g. ``v1.2.3``.
1341 :raises ValueError: When the epoch is malformed. |br|
1342 An epoch is a number followed by the separator and comes after any prefix,
1343 e.g. ``2:1.2.3`` or ``v2!1.2.3``.
1344 :raises VersionValidatorError: When the parsed version is rejected by ``validator``.
1345 """
1346 if versionString is None:
1347 raise ValueError("Parameter 'versionString' is None.")
1348 elif not isinstance(versionString, str):
1349 ex = TypeError("Parameter 'versionString' is not of type 'str'.")
1350 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1351 raise ex
1352 elif (versionString := versionString.strip()) == "":
1353 raise ValueError("Parameter 'versionString' is empty.")
1355 if (match := cls._PATTERN.match(versionString)) is None:
1356 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1357 ex.add_note("It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v1.2.3'.")
1358 raise ex
1360 def toInt(value: Nullable[str]) -> Nullable[int]:
1361 """
1362 Nested function converting an optional part of a version string to an integer.
1364 :param value: The matched part, or ``None`` if the pattern didn't match it.
1365 :returns: The part as an integer, or ``None`` if it wasn't present.
1366 :raises ValueError: If the part isn't a number.
1367 """
1368 if value is None or value == "":
1369 return None
1371 try:
1372 return int(value)
1373 except ValueError as ex: # pragma: no cover
1374 raise ValueError(f"Invalid part '{value}' in version number '{versionString}'.") from ex
1376 prefix = match["prefix"]
1378 release = match["release"]
1379 if release is not None:
1380 if release == "dev": 1380 ↛ 1382line 1380 didn't jump to line 1382 because the condition on line 1380 was always true
1381 releaseLevel = ReleaseLevel.Development
1382 elif release == "final":
1383 releaseLevel = ReleaseLevel.Final
1384 else: # pragma: no cover
1385 raise ValueError(f"Unknown release level '{release}' in version number '{versionString}'.")
1386 else:
1387 level = match["level"]
1388 if level is not None:
1389 level = level.lower()
1390 if level == "a" or level == "alpha":
1391 releaseLevel = ReleaseLevel.Alpha
1392 elif level == "b" or level == "beta":
1393 releaseLevel = ReleaseLevel.Beta
1394 elif level == "c" or level == "gamma":
1395 releaseLevel = ReleaseLevel.Gamma
1396 elif level == "rc" or level == "pre" or level == "preview":
1397 releaseLevel = ReleaseLevel.ReleaseCandidate
1398 else: # pragma: no cover
1399 raise ValueError(f"Unknown release level '{level}' in version number '{versionString}'.")
1400 else:
1401 releaseLevel = ReleaseLevel.Final
1403 version = cls(
1404 major=toInt(match["major"]),
1405 minor=toInt(match["minor"]),
1406 micro=toInt(match["micro"]),
1407 level=releaseLevel,
1408 number=toInt(match["number"]),
1409 post=toInt(match["post"]),
1410 dev=toInt(match["dev"]),
1411 epoch=toInt(match["epoch"]),
1412 build=toInt(match["build"]),
1413 postfix=match["postfix"],
1414 prefix=prefix if prefix != "" else None,
1415 # hash=match["hash"],
1416 flags=Flags.Clean,
1417 spelling=match["level"]
1418 )
1420 if validator is not None and not validator(version):
1421 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version)
1423 return version
1425 @readonly
1426 def Patch(self) -> int:
1427 """
1428 Read-only property to access the patch number.
1430 The patch number is identical to the micro number.
1432 :returns: The patch number.
1433 """
1434 return self._micro
1436 @readonly
1437 def ReleaseLevelSpelling(self) -> str:
1438 """
1439 Read-only property to access the release level's spelling.
1441 :returns: The spelling as parsed, e.g. ``pre`` for a release candidate, or ``""`` for the class' own spelling.
1442 """
1443 return self._releaseLevelSpelling
1445 def _equal(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]:
1446 """
1447 Private helper method to compute the equality of two :class:`SemanticVersion` instances.
1449 :param left: Left operand.
1450 :param right: Right operand.
1451 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1452 """
1453 return super()._equal(left, right)
1455 def _compare(self, left: SemanticVersion, right: SemanticVersion) -> Nullable[bool]:
1456 """
1457 Private helper method to compute the comparison of two :class:`SemanticVersion` instances.
1459 :param left: Left operand.
1460 :param right: Right operand.
1461 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1462 False if ``left`` is greater than ``right``. |br|
1463 Otherwise it's None (both operands are equal).
1464 """
1465 return super()._compare(left, right)
1467 def __eq__(self, other: Any) -> bool:
1468 """
1469 Compare two version numbers for equality.
1471 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1472 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1473 number is assumed (all other parts are zero).
1475 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1476 number.
1478 :param other: Operand to compare against.
1479 :returns: ``True``, if both version numbers are equal.
1480 :raises ValueError: If parameter ``other`` is None.
1481 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1482 """
1483 return super().__eq__(other)
1485 def __ne__(self, other: Any) -> bool:
1486 """
1487 Compare two version numbers for inequality.
1489 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1490 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1491 number is assumed (all other parts are zero).
1493 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1494 number.
1496 :param other: Operand to compare against.
1497 :returns: ``True``, if both version numbers are not equal.
1498 :raises ValueError: If parameter ``other`` is None.
1499 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1500 """
1501 return super().__ne__(other)
1503 def __lt__(self, other: Any) -> bool:
1504 """
1505 Compare two version numbers if the version is less than the second operand.
1507 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1508 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1509 number is assumed (all other parts are zero).
1511 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1512 number.
1514 :param other: Operand to compare against.
1515 :returns: ``True``, if version is less than the second operand.
1516 :raises ValueError: If parameter ``other`` is None.
1517 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1518 """
1519 return super().__lt__(other)
1521 def __le__(self, other: Any) -> bool:
1522 """
1523 Compare two version numbers if the version is less than or equal the second operand.
1525 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1526 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1527 number is assumed (all other parts are zero).
1529 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1530 number.
1532 :param other: Operand to compare against.
1533 :returns: ``True``, if version is less than or equal the second operand.
1534 :raises ValueError: If parameter ``other`` is None.
1535 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1536 """
1537 return super().__le__(other)
1539 def __gt__(self, other: Any) -> bool:
1540 """
1541 Compare two version numbers if the version is greater than the second operand.
1543 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1544 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1545 number is assumed (all other parts are zero).
1547 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1548 number.
1550 :param other: Operand to compare against.
1551 :returns: ``True``, if version is greater than the second operand.
1552 :raises ValueError: If parameter ``other`` is None.
1553 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1554 """
1555 return super().__gt__(other)
1557 def __ge__(self, other: Any) -> bool:
1558 """
1559 Compare two version numbers if the version is greater than or equal the second operand.
1561 The second operand should be an instance of :class:`SemanticVersion`, but ``str`` and ``int`` are accepted, too. |br|
1562 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
1563 number is assumed (all other parts are zero).
1565 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
1566 number.
1568 :param other: Operand to compare against.
1569 :returns: ``True``, if version is greater than or equal the second operand.
1570 :raises ValueError: If parameter ``other`` is None.
1571 :raises TypeError: If parameter ``other`` is not of type :class:`SemanticVersion`, string or integer.
1572 """
1573 return super().__ge__(other)
1575 def __rshift__(self, other: Nullable[Union[SemanticVersion, str, int]]) -> bool:
1576 """
1577 Return the minimum of this semantic version and a second operand.
1579 :param other: Second operand, a version, a version string or a major version number.
1580 :returns: ``True``, if this version is the minimum of both operands.
1581 :raises ValueError: If the second operand is ``None``.
1582 :raises TypeError: If the second operand is not a version, a string or an integer.
1583 """
1584 return super().__rshift__(other)
1586 def __hash__(self) -> int:
1587 """
1588 Compute a hash for this version number.
1590 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
1591 version unhashable.
1593 :returns: Hash of this version number.
1594 """
1595 return super().__hash__()
1597 def __format__(self, formatSpec: str) -> str:
1598 """
1599 Return a string representation of this version number according to the format specification.
1601 :param formatSpec: The format specification, using ``%``-placeholders for the version's parts.
1602 :returns: Formatted version number.
1603 :raises ValueError: If the format specification contains an unknown placeholder.
1604 """
1605 result = self._format(formatSpec)
1607 if (pos := result.find("%")) != -1 and result[pos + 1] != "%": # pragma: no cover
1608 raise ValueError(f"Unknown format specifier '%{result[pos + 1]}' in '{formatSpec}'.")
1610 return result.replace("%%", "%")
1612 def __repr__(self) -> str:
1613 """
1614 Return a normalized string representation of this version number.
1616 .. note::
1618 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
1619 :meth:`__str__` to render a version number including its prefix.
1621 :returns: Raw version number representation without a prefix.
1622 """
1623 epoch = f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else ""
1625 return f"{epoch}{self._major}.{self._minor}.{self._micro}"
1627 def __str__(self) -> str:
1628 """
1629 Return a string representation of this version number.
1631 :returns: Version number representation.
1632 """
1633 result = self._prefix if Parts.Prefix in self._parts else ""
1634 result += f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else ""
1635 result += f"{self._major}" # major is always present
1636 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1637 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1638 result += f".{self._build}" if Parts.Build in self._parts else ""
1639 if self._releaseLevel is ReleaseLevel.Development:
1640 result += "-dev"
1641 elif self._releaseLevel is ReleaseLevel.Alpha:
1642 result += f".{self._releaseLevelSpelling or 'alpha'}{self._releaseNumber}"
1643 elif self._releaseLevel is ReleaseLevel.Beta:
1644 result += f".{self._releaseLevelSpelling or 'beta'}{self._releaseNumber}"
1645 elif self._releaseLevel is ReleaseLevel.Gamma: 1645 ↛ 1646line 1645 didn't jump to line 1646 because the condition on line 1645 was never true
1646 result += f".{self._releaseLevelSpelling or 'gamma'}{self._releaseNumber}"
1647 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1648 result += f".{self._releaseLevelSpelling or 'rc'}{self._releaseNumber}"
1649 result += f".post{self._post}" if Parts.Post in self._parts else ""
1650 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1651 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1653 return result
1656@export
1657class PythonVersion(SemanticVersion):
1658 """
1659 Represents a Python version.
1660 """
1662 #: :pep:`440` writes an epoch ``v2!1.2.3``, where Debian and the default write ``2:1.2.3``.
1663 _EPOCH_SEPARATOR: ClassVar[str] = "!"
1665 @classmethod
1666 def Parse(
1667 cls,
1668 versionString: Nullable[str],
1669 validator: Nullable[Predicate[SemanticVersion]] = None,
1670 *,
1671 normalize: bool = False
1672 ) -> PythonVersion:
1673 """
1674 Parse a version string and return a :class:`PythonVersion` instance.
1676 The version keeps the spelling it was parsed from, e.g. ``1.0-pre1`` or ``1.0-dev``, and compares equal to its
1677 :pep:`440` normalized form ``1.0rc1`` or ``1.0.dev0``. With ``normalize``, the version is normalized as by
1678 :meth:`Normalize`.
1680 :param versionString: The version string to parse.
1681 :param validator: Optional, a validation function.
1682 :param normalize: Optional, normalize the parsed version according to :pep:`440`.
1683 :returns: An object representing a Python version.
1684 :raises TypeError: When parameter ``versionString`` is not a string.
1685 :raises ValueError: When parameter ``versionString`` is None or empty.
1686 :raises ValueError: When parameter ``versionString`` isn't a version number.
1687 :raises VersionValidatorError: When the parsed version is rejected by ``validator``.
1688 """
1689 version = super().Parse(versionString)
1690 if normalize:
1691 version = version.Normalize()
1693 if validator is not None and not validator(version):
1694 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version)
1696 return version
1698 @classmethod
1699 def FromSysVersionInfo(cls) -> PythonVersion:
1700 """
1701 Create a Python version from :data:`sys.version_info`.
1703 :returns: A PythonVersion instance of the current Python interpreter's version.
1704 :raises ToolingException: If the interpreter reports a release level this class doesn't know.
1705 """
1706 if version_info.releaselevel == "final":
1707 rl = ReleaseLevel.Final
1708 number = None
1709 else: # pragma: no cover
1710 number = version_info.serial
1712 if version_info.releaselevel == "alpha":
1713 rl = ReleaseLevel.Alpha
1714 elif version_info.releaselevel == "beta":
1715 rl = ReleaseLevel.Beta
1716 elif version_info.releaselevel == "candidate":
1717 rl = ReleaseLevel.ReleaseCandidate
1718 else: # pragma: no cover
1719 raise ToolingException(f"Unsupported release level '{version_info.releaselevel}'.")
1721 return cls(version_info.major, version_info.minor, version_info.micro, level=rl, number=number)
1723 def Normalize(self) -> PythonVersion:
1724 """
1725 Return this version in :pep:`440`'s normalized form.
1727 The release candidate's spellings ``c``, ``pre`` and ``preview`` become ``rc``, a development release ``-dev``
1728 becomes ``.dev0``, and the prefix is dropped: ``v1.0-pre1`` becomes ``1.0rc1``.
1730 :returns: A new version in normalized form.
1731 """
1732 level = self._releaseLevel
1733 number = self._releaseNumber
1734 dev = self._dev if Parts.Dev in self._parts else None
1735 if level is ReleaseLevel.Development:
1736 level = ReleaseLevel.Final
1737 dev = 0
1738 elif level is ReleaseLevel.Gamma:
1739 level = ReleaseLevel.ReleaseCandidate
1741 return self.__class__(
1742 major=self._major,
1743 minor=self._minor if Parts.Minor in self._parts else None,
1744 micro=self._micro if Parts.Micro in self._parts else None,
1745 level=level,
1746 number=number if level is not ReleaseLevel.Final else None,
1747 post=self._post if Parts.Post in self._parts else None,
1748 dev=dev,
1749 epoch=self._epoch if Parts.Epoch in self._parts else None,
1750 build=self._build if Parts.Build in self._parts else None,
1751 postfix=self._postfix if Parts.Postfix in self._parts else None,
1752 hash=self._hash if Parts.Hash in self._parts else None,
1753 flags=self._flags
1754 )
1756 def _key(self, version: Version) -> tuple[Any, ...]:
1757 """
1758 Private helper method to compute a version's :pep:`440` sort key.
1760 A release candidate's spellings are one release level, ``-dev`` is ``.dev0``, and a development release of a
1761 final release sorts before its pre-releases: ``1.0.dev0 < 1.0a1 < 1.0``.
1763 :param version: The version to compute the key for.
1764 :returns: Tuple of the version's parts in comparison order.
1765 """
1766 level = version._releaseLevel
1767 number = version._releaseNumber
1768 hasDev = Parts.Dev in version._parts or level is ReleaseLevel.Development
1769 if level is ReleaseLevel.Development:
1770 level = ReleaseLevel.Final
1771 number = 0
1772 elif level is ReleaseLevel.Gamma:
1773 level = ReleaseLevel.ReleaseCandidate
1775 post = version._post if Parts.Post in version._parts else -1
1776 if level is ReleaseLevel.Final and hasDev and post == -1:
1777 release = (ReleaseLevel.Alpha.value - 1, 0)
1778 else:
1779 release = (level.value, number)
1781 return (
1782 version._epoch,
1783 version._major,
1784 version._minor,
1785 version._micro,
1786 release,
1787 post,
1788 (0, version._dev) if hasDev else (1, 0),
1789 version._build
1790 )
1792 def _equal(self, left: Version, right: Version) -> Nullable[bool]:
1793 """
1794 Private helper method to compute the equality of two versions according to :pep:`440`.
1796 :param left: Left operand.
1797 :param right: Right operand.
1798 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1799 """
1800 return self._key(left) == self._key(right) and left._postfix == right._postfix
1802 def _compare(self, left: Version, right: Version) -> Nullable[bool]:
1803 """
1804 Private helper method to compare two versions according to :pep:`440`.
1806 :param left: Left operand.
1807 :param right: Right operand.
1808 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
1809 False if ``left`` is greater than ``right``. |br|
1810 Otherwise it's None (both operands are equal).
1811 """
1812 leftKey = self._key(left)
1813 rightKey = self._key(right)
1814 if leftKey < rightKey:
1815 return True
1816 elif leftKey > rightKey:
1817 return False
1819 return None
1821 def __hash__(self) -> int:
1822 """
1823 Compute a hash for this version number.
1825 Versions comparing equal have equal hashes, so the hash is computed from what :meth:`_equal` compares.
1827 :returns: Hash of this version number.
1828 """
1829 return hash((self._key(self), self._postfix))
1831 def __str__(self) -> str:
1832 """
1833 Return a string representation of this version number.
1835 :returns: Version number representation.
1836 """
1837 result = self._prefix if Parts.Prefix in self._parts else ""
1838 result += f"{self._epoch}{self._EPOCH_SEPARATOR}" if Parts.Epoch in self._parts else ""
1839 result += f"{self._major}" # major is always present
1840 result += f".{self._minor}" if Parts.Minor in self._parts else ""
1841 result += f".{self._micro}" if Parts.Micro in self._parts else ""
1842 result += f".{self._build}" if Parts.Build in self._parts else ""
1843 if self._releaseLevel is ReleaseLevel.Development:
1844 result += "-dev"
1845 elif self._releaseLevel is ReleaseLevel.Alpha:
1846 result += f"{self._releaseLevelSpelling or 'a'}{self._releaseNumber}"
1847 elif self._releaseLevel is ReleaseLevel.Beta:
1848 result += f"{self._releaseLevelSpelling or 'b'}{self._releaseNumber}"
1849 elif self._releaseLevel is ReleaseLevel.Gamma:
1850 result += f"{self._releaseLevelSpelling or 'c'}{self._releaseNumber}"
1851 elif self._releaseLevel is ReleaseLevel.ReleaseCandidate:
1852 result += f"{self._releaseLevelSpelling or 'rc'}{self._releaseNumber}"
1853 result += f".post{self._post}" if Parts.Post in self._parts else ""
1854 result += f".dev{self._dev}" if Parts.Dev in self._parts else ""
1855 result += f"+{self._postfix}" if Parts.Postfix in self._parts else ""
1857 return result
1860@export
1861class CalendarVersion(Version):
1862 """Representation of a calendar version number like ``2021.10``."""
1864 _PARTCOUNT: ClassVar[int] = 3 #: Number of numeric parts a version number of this class can carry.
1866 _PATTERN: ClassVar[Pattern] = re_compile(
1867 r"^"
1868 r"(?P<prefix>rev|REV|[vViIrR])?"
1869 r"(?P<major>\d+)"
1870 r"(?:\.(?P<minor>\d+))?"
1871 r"(?:\.(?P<micro>\d+))?"
1872 r"$"
1873 ) #: Regular expression to parse a calendar version from a string.
1875 def __init__(
1876 self,
1877 major: int,
1878 minor: Nullable[int] = None,
1879 micro: Nullable[int] = None,
1880 build: Nullable[int] = None,
1881 flags: Flags = Flags.Clean,
1882 prefix: Nullable[str] = None,
1883 postfix: Nullable[str] = None
1884 ) -> None:
1885 """
1886 Initializes a calendar version number representation.
1888 :param major: Major number part of the version number.
1889 :param minor: Optional, minor number part of the version number.
1890 :param micro: Optional, micro (patch) number part of the version number.
1891 :param build: Optional, build number part of the version number.
1892 :param flags: Optional, the version number's flags.
1893 :param prefix: Optional, the version number's prefix.
1894 :param postfix: Optional, the version number's postfix.
1895 :raises TypeError: If parameter 'major' is not of type integer.
1896 :raises ValueError: If parameter 'major' is a negative number.
1897 :raises TypeError: If parameter 'minor' is not of type integer.
1898 :raises ValueError: If parameter 'minor' is a negative number.
1899 :raises TypeError: If parameter 'micro' is not of type integer.
1900 :raises ValueError: If parameter 'micro' is a negative number.
1901 :raises TypeError: If parameter 'build' is not of type integer.
1902 :raises ValueError: If parameter 'build' is a negative number.
1903 :raises TypeError: If parameter 'prefix' is not of type string.
1904 :raises TypeError: If parameter 'postfix' is not of type string.
1905 """
1906 super().__init__(major, minor, micro, build=build, postfix=postfix, prefix=prefix, flags=flags)
1908 @classmethod
1909 def Parse(cls, versionString: Nullable[str], validator: Nullable[Predicate[CalendarVersion]] = None) -> CalendarVersion:
1910 """
1911 Parse a version string and return a :class:`CalendarVersion` instance.
1913 Allowed prefix characters:
1915 * ``v|V`` - version, public version, public release
1916 * ``i|I`` - internal version, internal release
1917 * ``r|R`` - release, revision
1918 * ``rev|REV`` - revision
1920 A version number carries up to :attr:`_PARTCOUNT` numeric parts. :class:`YearMonthVersion`,
1921 :class:`YearWeekVersion` and :class:`YearReleaseVersion` describe two parts, so a third part is rejected for
1922 them.
1924 :param versionString: The version string to parse.
1925 :param validator: Optional, a validation function.
1926 :returns: An object representing a calendar version.
1927 :raises TypeError: If parameter ``versionString`` is not a string.
1928 :raises ValueError: If parameter ``versionString`` is None or empty.
1929 :raises ValueError: If parameter ``versionString`` isn't a calendar version number. |br|
1930 It may carry one of the prefixes ``v``, ``i``, ``r`` or ``rev``, e.g.
1931 ``v2024.04``.
1932 :raises ValueError: If parameter ``versionString`` has more parts than the class describes. |br|
1933 Use :class:`CalendarVersion` or :class:`YearMonthDayVersion` to parse a
1934 three-part calendar version number.
1935 :raises VersionValidatorError: If the parsed version is rejected by ``validator``.
1936 """
1937 if versionString is None:
1938 raise ValueError("Parameter 'versionString' is None.")
1939 elif not isinstance(versionString, str):
1940 ex = TypeError("Parameter 'versionString' is not of type 'str'.")
1941 ex.add_note(f"Got type '{getFullyQualifiedName(versionString)}'.")
1942 raise ex
1943 elif (versionString := versionString.strip()) == "":
1944 raise ValueError("Parameter 'versionString' is empty.")
1946 if (match := cls._PATTERN.match(versionString)) is None:
1947 ex = ValueError(f"Syntax error in parameter 'versionString': '{versionString}'")
1948 ex.add_note(f"A calendar version number is made of up to {cls._PARTCOUNT} numeric parts, e.g. '2024.04'.")
1949 ex.add_note("It may carry one of the prefixes 'v', 'i', 'r' or 'rev', e.g. 'v2024.04'.")
1950 raise ex
1952 prefix = match["prefix"]
1953 minor = match["minor"]
1954 micro = match["micro"]
1956 if micro is not None and cls._PARTCOUNT < 3:
1957 ex = ValueError(f"Version number '{versionString}' has 3 parts, but '{cls.__name__}' describes {cls._PARTCOUNT}.")
1958 ex.add_note("Use 'CalendarVersion' or 'YearMonthDayVersion' to parse a 3-part calendar version number.")
1959 raise ex
1961 numbers = [int(match["major"])]
1962 if minor is not None:
1963 numbers.append(int(minor))
1964 if micro is not None:
1965 numbers.append(int(micro))
1967 version = cls(*numbers, flags=Flags.Clean, prefix=prefix if prefix != "" else None)
1969 if validator is not None and not validator(version):
1970 raise VersionValidatorError(f"Failed to validate version string '{versionString}'.", version=version)
1972 return version
1974 @readonly
1975 def Year(self) -> int:
1976 """
1977 Read-only property to access the year part.
1979 :returns: The year part.
1980 """
1981 return self._major
1983 def _equal(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]:
1984 """
1985 Private helper method to compute the equality of two :class:`CalendarVersion` instances.
1987 :param left: Left parameter.
1988 :param right: Right parameter.
1989 :returns: ``True``, if ``left`` is equal to ``right``, otherwise it's ``False``.
1990 """
1991 return (left._major == right._major) and (left._minor == right._minor) and (left._micro == right._micro)
1993 def _compare(self, left: CalendarVersion, right: CalendarVersion) -> Nullable[bool]:
1994 """
1995 Private helper method to compute the comparison of two :class:`CalendarVersion` instances.
1997 :param left: Left parameter.
1998 :param right: Right parameter.
1999 :returns: ``True``, if ``left`` is smaller than ``right``. |br|
2000 False if ``left`` is greater than ``right``. |br|
2001 Otherwise it's None (both parameters are equal).
2002 """
2003 if left._major < right._major:
2004 return True
2005 elif left._major > right._major:
2006 return False
2008 if left._minor < right._minor:
2009 return True
2010 elif left._minor > right._minor:
2011 return False
2013 if left._micro < right._micro: 2013 ↛ 2014line 2013 didn't jump to line 2014 because the condition on line 2013 was never true
2014 return True
2015 elif left._micro > right._micro: 2015 ↛ 2016line 2015 didn't jump to line 2016 because the condition on line 2015 was never true
2016 return False
2018 return None
2020 def __eq__(self, other: Any) -> bool:
2021 """
2022 Compare two version numbers for equality.
2024 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
2025 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
2026 number is assumed (all other parts are zero).
2028 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
2029 number.
2031 :param other: Parameter to compare against.
2032 :returns: ``True``, if both version numbers are equal.
2033 :raises ValueError: If parameter ``other`` is None.
2034 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
2035 """
2036 return super().__eq__(other)
2038 def __ne__(self, other: Any) -> bool:
2039 """
2040 Compare two version numbers for inequality.
2042 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
2043 In case of ``str``, it's tried to parse the string as a calendar version number. In case of ``int``, a single major
2044 number is assumed (all other parts are zero).
2046 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
2047 number.
2049 :param other: Parameter to compare against.
2050 :returns: ``True``, if both version numbers are not equal.
2051 :raises ValueError: If parameter ``other`` is None.
2052 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
2053 """
2054 return super().__ne__(other)
2056 def __lt__(self, other: Any) -> bool:
2057 """
2058 Compare two version numbers if the version is less than the second operand.
2060 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
2061 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
2062 number is assumed (all other parts are zero).
2064 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
2065 number.
2067 :param other: Parameter to compare against.
2068 :returns: ``True``, if version is less than the second operand.
2069 :raises ValueError: If parameter ``other`` is None.
2070 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
2071 """
2072 return super().__lt__(other)
2074 def __le__(self, other: Any) -> bool:
2075 """
2076 Compare two version numbers if the version is less than or equal the second operand.
2078 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
2079 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
2080 number is assumed (all other parts are zero).
2082 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
2083 number.
2085 :param other: Parameter to compare against.
2086 :returns: ``True``, if version is less than or equal the second operand.
2087 :raises ValueError: If parameter ``other`` is None.
2088 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
2089 """
2090 return super().__le__(other)
2092 def __gt__(self, other: Any) -> bool:
2093 """
2094 Compare two version numbers if the version is greater than the second operand.
2096 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
2097 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
2098 number is assumed (all other parts are zero).
2100 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
2101 number.
2103 :param other: Parameter to compare against.
2104 :returns: ``True``, if version is greater than the second operand.
2105 :raises ValueError: If parameter ``other`` is None.
2106 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
2107 """
2108 return super().__gt__(other)
2110 def __ge__(self, other: Any) -> bool:
2111 """
2112 Compare two version numbers if the version is greater than or equal the second operand.
2114 The second operand should be an instance of :class:`CalendarVersion`, but ``str`` and ``int`` are accepted, too. |br|
2115 In case of ``str``, it's tried to parse the string as a semantic version number. In case of ``int``, a single major
2116 number is assumed (all other parts are zero).
2118 ``float`` is not supported, due to rounding issues when converting the fractional part of the float to a minor
2119 number.
2121 :param other: Parameter to compare against.
2122 :returns: ``True``, if version is greater than or equal the second operand.
2123 :raises ValueError: If parameter ``other`` is None.
2124 :raises TypeError: If parameter ``other`` is not of type :class:`CalendarVersion`, string or integer.
2125 """
2126 return super().__ge__(other)
2128 def __hash__(self) -> int:
2129 """
2130 Compute a hash for this version number.
2132 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2133 version unhashable.
2135 :returns: Hash of this version number.
2136 """
2137 return super().__hash__()
2139 def __format__(self, formatSpec: str) -> str:
2140 """
2141 Return a string representation of this version number according to the format specification.
2143 .. topic:: Format Specifiers
2145 * ``%M`` - major number (year)
2146 * ``%m`` - minor number (month/week)
2147 * ``%u`` - micro number (day)
2149 :param formatSpec: The format specification.
2150 :returns: Formatted version number.
2151 """
2152 if formatSpec == "":
2153 return self.__str__()
2155 result = formatSpec
2156 # result = result.replace("%P", str(self._prefix))
2157 result = result.replace("%M", str(self._major))
2158 result = result.replace("%m", str(self._minor))
2159 result = result.replace("%u", str(self._micro))
2160 # result = result.replace("%p", str(self._pre))
2162 return result.replace("%%", "%")
2164 def __repr__(self) -> str:
2165 """
2166 Return a normalized string representation of this version number.
2168 .. note::
2170 A prefix doesn't contribute to the version number's value, therefore it's not part of the normalized form. Use
2171 :meth:`__str__` to render a version number including its prefix.
2173 :returns: Raw version number representation without a prefix.
2174 """
2175 result = f"{self._major}.{self._minor}"
2176 result += f".{self._micro}" if Parts.Micro in self._parts else ""
2178 return result
2180 def __str__(self) -> str:
2181 """
2182 Return a string representation of this version number with only the present parts.
2184 :returns: Version number representation including a prefix.
2185 """
2186 result = self._prefix if Parts.Prefix in self._parts else ""
2187 result += f"{self._major}"
2188 result += f".{self._minor}" if Parts.Minor in self._parts else ""
2189 result += f".{self._micro}" if Parts.Micro in self._parts else ""
2191 return result
2194@export
2195class YearMonthVersion(CalendarVersion):
2196 """Representation of a calendar version number made of year and month like ``2021.10``."""
2198 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and month.
2200 def __init__(
2201 self,
2202 year: int,
2203 month: Nullable[int] = None,
2204 build: Nullable[int] = None,
2205 flags: Flags = Flags.Clean,
2206 prefix: Nullable[str] = None,
2207 postfix: Nullable[str] = None
2208 ) -> None:
2209 """
2210 Initializes a year-month version number representation.
2212 :param year: Year part of the version number.
2213 :param month: Optional, month part of the version number.
2214 :param build: Optional, build number part of the version number.
2215 :param flags: Optional, the version number's flags.
2216 :param prefix: Optional, the version number's prefix.
2217 :param postfix: Optional, the version number's postfix.
2218 :raises TypeError: If parameter 'major' is not of type integer.
2219 :raises ValueError: If parameter 'major' is a negative number.
2220 :raises TypeError: If parameter 'minor' is not of type integer.
2221 :raises ValueError: If parameter 'minor' is a negative number.
2222 :raises TypeError: If parameter 'micro' is not of type integer.
2223 :raises ValueError: If parameter 'micro' is a negative number.
2224 :raises TypeError: If parameter 'build' is not of type integer.
2225 :raises ValueError: If parameter 'build' is a negative number.
2226 :raises TypeError: If parameter 'prefix' is not of type string.
2227 :raises TypeError: If parameter 'postfix' is not of type string.
2228 """
2229 super().__init__(year, month, None, build, flags, prefix, postfix)
2231 @readonly
2232 def Month(self) -> int:
2233 """
2234 Read-only property to access the month part.
2236 :returns: The month part.
2237 """
2238 return self._minor
2240 def __hash__(self) -> int:
2241 """
2242 Compute a hash for this version number.
2244 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2245 version unhashable.
2247 :returns: Hash of this version number.
2248 """
2249 return super().__hash__()
2252@export
2253class YearWeekVersion(CalendarVersion):
2254 """Representation of a calendar version number made of year and week like ``2021.47``."""
2256 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and week.
2258 def __init__(
2259 self,
2260 year: int,
2261 week: Nullable[int] = None,
2262 build: Nullable[int] = None,
2263 flags: Flags = Flags.Clean,
2264 prefix: Nullable[str] = None,
2265 postfix: Nullable[str] = None
2266 ) -> None:
2267 """
2268 Initializes a year-week version number representation.
2270 :param year: Year part of the version number.
2271 :param week: Optional, week part of the version number.
2272 :param build: Optional, build number part of the version number.
2273 :param flags: Optional, the version number's flags.
2274 :param prefix: Optional, the version number's prefix.
2275 :param postfix: Optional, the version number's postfix.
2276 :raises TypeError: If parameter 'major' is not of type integer.
2277 :raises ValueError: If parameter 'major' is a negative number.
2278 :raises TypeError: If parameter 'minor' is not of type integer.
2279 :raises ValueError: If parameter 'minor' is a negative number.
2280 :raises TypeError: If parameter 'micro' is not of type integer.
2281 :raises ValueError: If parameter 'micro' is a negative number.
2282 :raises TypeError: If parameter 'build' is not of type integer.
2283 :raises ValueError: If parameter 'build' is a negative number.
2284 :raises TypeError: If parameter 'prefix' is not of type string.
2285 :raises TypeError: If parameter 'postfix' is not of type string.
2286 """
2287 super().__init__(year, week, None, build, flags, prefix, postfix)
2289 @readonly
2290 def Week(self) -> int:
2291 """
2292 Read-only property to access the week part.
2294 :returns: The week part.
2295 """
2296 return self._minor
2298 def __hash__(self) -> int:
2299 """
2300 Compute a hash for this version number.
2302 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2303 version unhashable.
2305 :returns: Hash of this version number.
2306 """
2307 return super().__hash__()
2310@export
2311class YearReleaseVersion(CalendarVersion):
2312 """Representation of a calendar version number made of year and release per year like ``2021.2``."""
2314 _PARTCOUNT: ClassVar[int] = 2 #: A version number of this class carries year and release.
2316 def __init__(
2317 self,
2318 year: int,
2319 release: Nullable[int] = None,
2320 build: Nullable[int] = None,
2321 flags: Flags = Flags.Clean,
2322 prefix: Nullable[str] = None,
2323 postfix: Nullable[str] = None
2324 ) -> None:
2325 """
2326 Initializes a year-release version number representation.
2328 :param year: Year part of the version number.
2329 :param release: Optional, release number of the version number.
2330 :param build: Optional, build number part of the version number.
2331 :param flags: Optional, the version number's flags.
2332 :param prefix: Optional, the version number's prefix.
2333 :param postfix: Optional, the version number's postfix.
2334 :raises TypeError: If parameter 'major' is not of type integer.
2335 :raises ValueError: If parameter 'major' is a negative number.
2336 :raises TypeError: If parameter 'minor' is not of type integer.
2337 :raises ValueError: If parameter 'minor' is a negative number.
2338 :raises TypeError: If parameter 'micro' is not of type integer.
2339 :raises ValueError: If parameter 'micro' is a negative number.
2340 :raises TypeError: If parameter 'build' is not of type integer.
2341 :raises ValueError: If parameter 'build' is a negative number.
2342 :raises TypeError: If parameter 'prefix' is not of type string.
2343 :raises TypeError: If parameter 'postfix' is not of type string.
2344 """
2345 super().__init__(year, release, None, build, flags, prefix, postfix)
2347 @readonly
2348 def Release(self) -> int:
2349 """
2350 Read-only property to access the release number.
2352 :returns: The release number.
2353 """
2354 return self._minor
2356 def __hash__(self) -> int:
2357 """
2358 Compute a hash for this version number.
2360 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2361 version unhashable.
2363 :returns: Hash of this version number.
2364 """
2365 return super().__hash__()
2368@export
2369class YearMonthDayVersion(CalendarVersion):
2370 """Representation of a calendar version number made of year, month and day like ``2021.10.15``."""
2372 def __init__(
2373 self,
2374 year: int,
2375 month: Nullable[int] = None,
2376 day: Nullable[int] = None,
2377 build: Nullable[int] = None,
2378 flags: Flags = Flags.Clean,
2379 prefix: Nullable[str] = None,
2380 postfix: Nullable[str] = None
2381 ) -> None:
2382 """
2383 Initializes a year-month-day version number representation.
2385 :param year: Year part of the version number.
2386 :param month: Optional, month part of the version number.
2387 :param day: Optional, day part of the version number.
2388 :param build: Optional, build number part of the version number.
2389 :param flags: Optional, the version number's flags.
2390 :param prefix: Optional, the version number's prefix.
2391 :param postfix: Optional, the version number's postfix.
2392 :raises TypeError: If parameter 'major' is not of type integer.
2393 :raises ValueError: If parameter 'major' is a negative number.
2394 :raises TypeError: If parameter 'minor' is not of type integer.
2395 :raises ValueError: If parameter 'minor' is a negative number.
2396 :raises TypeError: If parameter 'micro' is not of type integer.
2397 :raises ValueError: If parameter 'micro' is a negative number.
2398 :raises TypeError: If parameter 'build' is not of type integer.
2399 :raises ValueError: If parameter 'build' is a negative number.
2400 :raises TypeError: If parameter 'prefix' is not of type string.
2401 :raises TypeError: If parameter 'postfix' is not of type string.
2402 """
2403 super().__init__(year, month, day, build, flags, prefix, postfix)
2405 @readonly
2406 def Month(self) -> int:
2407 """
2408 Read-only property to access the month part.
2410 :returns: The month part.
2411 """
2412 return self._minor
2414 @readonly
2415 def Day(self) -> int:
2416 """
2417 Read-only property to access the day part.
2419 :returns: The day part.
2420 """
2421 return self._micro
2423 def __hash__(self) -> int:
2424 """
2425 Compute a hash for this version number.
2427 The derived class re-implements :meth:`__eq__`, so Python would otherwise drop the inherited hash and make the
2428 version unhashable.
2430 :returns: Hash of this version number.
2431 """
2432 return super().__hash__()
2435V = TypeVar("V", bound=Version)
2437@export
2438class RangeBoundHandling(Flag):
2439 """
2440 A flag defining how to handle bounds in a range.
2442 If a bound is inclusive, the bound's value is within the range. If a bound is exclusive, the bound's value is the
2443 first value outside the range. Inclusive and exclusive behavior can be mixed for lower and upper bounds.
2444 """
2445 BothBoundsInclusive = 0 #: Lower and upper bound are inclusive.
2446 LowerBoundInclusive = 0 #: Lower bound is inclusive.
2447 UpperBoundInclusive = 0 #: Upper bound is inclusive.
2448 LowerBoundExclusive = 1 #: Lower bound is exclusive.
2449 UpperBoundExclusive = 2 #: Upper bound is exclusive.
2450 BothBoundsExclusive = 3 #: Lower and upper bound are exclusive.
2453@export
2454class VersionRange(Generic[V], metaclass=ExtendedType, slots=True):
2455 """
2456 Representation of a version range described by a lower bound and upper bound version.
2458 This version range works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2460 A bound may be **unbound**, written as ``None``, meaning the range is open in that direction. That is how a
2461 dependency range like Maven's ``[1.0,)`` - *1.0 and everything after it* - is expressed, and it is what makes a
2462 single comparison such as ``>=1.0`` a range at all. A range unbound at both ends contains every version.
2464 Whether the bound itself belongs to the range is :class:`RangeBoundHandling`'s business, not the bound's, so
2465 each example names the comparison it stands for:
2467 .. code-block:: python
2469 VersionRange(SemanticVersion.Parse("1.0.0"), None) # >=1.0.0 ⟶ 1.0.0 and everything above it
2470 VersionRange(None, SemanticVersion.Parse("2.0.0")) # <=2.0.0 ⟶ everything up to 2.0.0
2471 VersionRange(None, None) # every version
2472 """
2473 _lowerBound: Nullable[V] #: Lower bound of the version range, or ``None`` if it is unbound.
2474 _upperBound: Nullable[V] #: Upper bound of the version range, or ``None`` if it is unbound.
2475 _boundHandling: RangeBoundHandling #: Strategy deciding whether the bounds are part of the range.
2477 def __init__(
2478 self,
2479 lowerBound: Nullable[V],
2480 upperBound: Nullable[V],
2481 boundHandling: RangeBoundHandling = RangeBoundHandling.BothBoundsInclusive
2482 ) -> None:
2483 """
2484 Initializes a version range described by a lower and upper bound.
2486 Either bound may be ``None``, which leaves the range open in that direction. The checks that relate the two
2487 bounds - that they are compatible types, and that the lower one isn't above the upper one - can only be made
2488 when both are present, so they are skipped for an open bound rather than failing on it.
2490 :param lowerBound: Lowest version (inclusive), or ``None`` to leave the range open downwards.
2491 :param upperBound: Highest version (inclusive), or ``None`` to leave the range open upwards.
2492 :param boundHandling: Optional, strategy deciding whether the bounds are part of the range.
2493 :raises TypeError: If parameter ``lowerBound`` is neither ``None`` nor of type :class:`Version`.
2494 :raises TypeError: If parameter ``upperBound`` is neither ``None`` nor of type :class:`Version`.
2495 :raises TypeError: If parameter ``lowerBound`` and ``upperBound`` are unrelated types.
2496 :raises ValueError: If parameter ``lowerBound`` isn't less than or equal to ``upperBound``.
2497 """
2498 if lowerBound is not None and not isinstance(lowerBound, Version):
2499 ex = TypeError("Parameter 'lowerBound' is not of type 'Version'.")
2500 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}'.")
2501 raise ex
2503 if upperBound is not None and not isinstance(upperBound, Version):
2504 ex = TypeError("Parameter 'upperBound' is not of type 'Version'.")
2505 ex.add_note(f"Got type '{getFullyQualifiedName(upperBound)}'.")
2506 raise ex
2508 if lowerBound is not None and upperBound is not None:
2509 if not self._AreCompatible(lowerBound, upperBound):
2510 ex = TypeError("Parameters 'lowerBound' and 'upperBound' are not compatible with each other.")
2511 ex.add_note(f"Got type '{getFullyQualifiedName(lowerBound)}' for lowerBound and "
2512 f"type '{getFullyQualifiedName(upperBound)}' for upperBound.")
2513 raise ex
2515 if not (lowerBound <= upperBound):
2516 ex = ValueError("Parameter 'lowerBound' isn't less than parameter 'upperBound'.")
2517 ex.add_note(f"Got '{lowerBound}' for lowerBound and '{upperBound}' for upperBound.")
2518 raise ex
2520 self._lowerBound = lowerBound
2521 self._upperBound = upperBound
2522 self._boundHandling = boundHandling
2524 @property
2525 def LowerBound(self) -> Nullable[V]:
2526 """
2527 Property to access the range's lower bound.
2529 Assigning ``None`` leaves the bound unbound, opening the range in that direction.
2531 :returns: Lower bound of the version range, or ``None`` if it is unbound.
2532 :raises TypeError: If an assigned value is neither ``None`` nor of type :class:`Version`.
2533 :raises TypeError: If an assigned value's type is unrelated to the range's upper bound.
2534 :raises ValueError: If an assigned value is above the range's upper bound. |br|
2535 The bounds are only related when both are present.
2536 """
2537 return self._lowerBound
2539 @LowerBound.setter
2540 def LowerBound(self, value: Nullable[V]) -> None:
2541 if value is not None and not isinstance(value, Version): 2541 ↛ 2542line 2541 didn't jump to line 2542 because the condition on line 2541 was never true
2542 ex = TypeError("Parameter 'value' is not of type 'Version'.")
2543 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2544 raise ex
2546 if value is not None and self._upperBound is not None:
2547 if not self._AreCompatible(value, self._upperBound):
2548 ex = TypeError("Parameter 'value' is not compatible with the range's upper bound.")
2549 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2550 ex.add_note(f"The upper bound is of type '{getFullyQualifiedName(self._upperBound)}'.")
2551 raise ex
2553 if not (value <= self._upperBound):
2554 ex = ValueError("Parameter 'value' isn't less than or equal to the range's upper bound.")
2555 ex.add_note(f"Got '{value}' for the lower bound; the upper bound is '{self._upperBound}'.")
2556 raise ex
2558 self._lowerBound = value
2560 @property
2561 def UpperBound(self) -> Nullable[V]:
2562 """
2563 Property to access the range's upper bound.
2565 Assigning ``None`` leaves the bound unbound, opening the range in that direction.
2567 :returns: Upper bound of the version range, or ``None`` if it is unbound.
2568 :raises TypeError: If an assigned value is neither ``None`` nor of type :class:`Version`.
2569 :raises TypeError: If an assigned value's type is unrelated to the range's lower bound.
2570 :raises ValueError: If an assigned value is below the range's lower bound. |br|
2571 The bounds are only related when both are present.
2572 """
2573 return self._upperBound
2575 @UpperBound.setter
2576 def UpperBound(self, value: Nullable[V]) -> None:
2577 if value is not None and not isinstance(value, Version): 2577 ↛ 2578line 2577 didn't jump to line 2578 because the condition on line 2577 was never true
2578 ex = TypeError("Parameter 'value' is not of type 'Version'.")
2579 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2580 raise ex
2582 if value is not None and self._lowerBound is not None:
2583 if not self._AreCompatible(value, self._lowerBound):
2584 ex = TypeError("Parameter 'value' is not compatible with the range's lower bound.")
2585 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2586 ex.add_note(f"The lower bound is of type '{getFullyQualifiedName(self._lowerBound)}'.")
2587 raise ex
2589 if not (self._lowerBound <= value):
2590 ex = ValueError("Parameter 'value' isn't greater than or equal to the range's lower bound.")
2591 ex.add_note(f"Got '{value}' for the upper bound; the lower bound is '{self._lowerBound}'.")
2592 raise ex
2594 self._upperBound = value
2596 @property
2597 def BoundHandling(self) -> RangeBoundHandling:
2598 """
2599 Property to access the range's bound handling strategy.
2601 :returns: The range's bound handling strategy.
2602 :raises TypeError: If an assigned value is not of type :class:`RangeBoundHandling`.
2603 """
2604 return self._boundHandling
2606 @BoundHandling.setter
2607 def BoundHandling(self, value: RangeBoundHandling) -> None:
2608 if not isinstance(value, RangeBoundHandling):
2609 ex = TypeError("Parameter 'value' is not of type 'RangeBoundHandling'.")
2610 ex.add_note(f"Got type '{getFullyQualifiedName(value)}'.")
2611 raise ex
2613 self._boundHandling = value
2615 @staticmethod
2616 def _AreCompatible(left: Version, right: Version) -> bool:
2617 """
2618 Check whether two versions' types can be related to each other.
2620 Two versions relate when they are of the same class, or when one's class derives from the other's. So a
2621 :class:`SemanticVersion` relates to a :class:`PythonVersion`, which derives from it, and not to a
2622 :class:`CalendarVersion`, which is a sibling under :class:`Version`.
2624 This is the single rule every part of a range applies: to its two bounds against each other, to a version
2625 held against them, and to a bound assigned after construction.
2627 :param left: The first version.
2628 :param right: The second version.
2629 :returns: ``True``, if the two versions' types are related.
2630 """
2631 leftType = left.__class__
2632 rightType = right.__class__
2634 return leftType is rightType or issubclass(leftType, rightType) or issubclass(rightType, leftType)
2636 def _CheckCompatibility(self, other: Version) -> None:
2637 """
2638 Check that a version can be related to this range's bounds.
2640 The rule is the one :meth:`__init__` applies *between* the two bounds: the same class, or one deriving from
2641 the other. So a range bounded by :class:`SemanticVersion` accepts a :class:`PythonVersion`, because that
2642 derives from it, and refuses a :class:`CalendarVersion`, which is a sibling. A range whose bounds are both
2643 unbound carries no type to check against, so it accepts any version.
2645 :param other: The version to check against this range's bounds.
2646 :raises TypeError: If the version's type is unrelated to this range's bounds.
2647 """
2648 reference = self._lowerBound if self._lowerBound is not None else self._upperBound
2649 if reference is None:
2650 return
2652 if not self._AreCompatible(other, reference):
2653 ex = TypeError("Parameter 'other' is not compatible with version range.")
2654 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2655 ex.add_note(f"This range is bounded by type '{getFullyQualifiedName(reference)}'.")
2656 raise ex
2658 def __and__(self, other: Any) -> VersionRange[V]:
2659 """
2660 Compute the intersection of two version ranges.
2662 Each bound of the result comes from whichever range constrains it more tightly, and is inclusive only if it
2663 is inclusive in *that* range. Where both ranges name the same bound value, it is inclusive only when **both**
2664 include it - the intersection cannot admit a version one of its operands excludes.
2666 :param other: Second version range to intersect with.
2667 :returns: Intersected version range.
2668 :raises TypeError: If parameter 'other' is not of type :class:`VersionRange`.
2669 :raises TypeError: If the two ranges' bounds are of unrelated types.
2670 :raises ValueError: If intersection is empty.
2671 """
2672 if not isinstance(other, VersionRange): 2672 ↛ 2673line 2672 didn't jump to line 2673 because the condition on line 2672 was never true
2673 ex = TypeError("Parameter 'other' is not of type 'VersionRange'.")
2674 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2675 raise ex
2677 if self._lowerBound is not None and other._lowerBound is not None:
2678 if not self._AreCompatible(self._lowerBound, other._lowerBound): 2678 ↛ 2679line 2678 didn't jump to line 2679 because the condition on line 2678 was never true
2679 ex = TypeError("Parameter 'other's LowerBound and this range's 'LowerBound' are not compatible "
2680 "with each other.")
2681 ex.add_note(f"Got type '{getFullyQualifiedName(other._lowerBound)}' for other.LowerBound and "
2682 f"type '{getFullyQualifiedName(self._lowerBound)}' for self.LowerBound.")
2683 raise ex
2685 ownLowerExclusive = RangeBoundHandling.LowerBoundExclusive in self._boundHandling
2686 otherLowerExclusive = RangeBoundHandling.LowerBoundExclusive in other._boundHandling
2687 ownUpperExclusive = RangeBoundHandling.UpperBoundExclusive in self._boundHandling
2688 otherUpperExclusive = RangeBoundHandling.UpperBoundExclusive in other._boundHandling
2690 # An unbound lower end is the lowest of all, so the other range's bound wins; likewise the highest upper one.
2691 # Each bound keeps the handling of the range it came from; a shared value keeps the stricter of the two.
2692 if self._lowerBound is None:
2693 lBound = other._lowerBound
2694 lowerExclusive = otherLowerExclusive
2695 elif other._lowerBound is None:
2696 lBound = self._lowerBound
2697 lowerExclusive = ownLowerExclusive
2698 elif self._lowerBound > other._lowerBound:
2699 lBound = self._lowerBound
2700 lowerExclusive = ownLowerExclusive
2701 elif other._lowerBound > self._lowerBound:
2702 lBound = other._lowerBound
2703 lowerExclusive = otherLowerExclusive
2704 else:
2705 lBound = self._lowerBound
2706 lowerExclusive = ownLowerExclusive or otherLowerExclusive
2708 if self._upperBound is None:
2709 uBound = other._upperBound
2710 upperExclusive = otherUpperExclusive
2711 elif other._upperBound is None: 2711 ↛ 2712line 2711 didn't jump to line 2712 because the condition on line 2711 was never true
2712 uBound = self._upperBound
2713 upperExclusive = ownUpperExclusive
2714 elif self._upperBound < other._upperBound:
2715 uBound = self._upperBound
2716 upperExclusive = ownUpperExclusive
2717 elif other._upperBound < self._upperBound:
2718 uBound = other._upperBound
2719 upperExclusive = otherUpperExclusive
2720 else:
2721 uBound = self._upperBound
2722 upperExclusive = ownUpperExclusive or otherUpperExclusive
2724 if lBound is not None and uBound is not None and not (lBound <= uBound):
2725 ex = ValueError("The intersection of both version ranges is empty.")
2726 ex.add_note(f"Got value '{lBound}' for the highest lower bound.")
2727 ex.add_note(f"The lowest upper bound is '{uBound}'.")
2728 raise ex
2730 boundHandling = RangeBoundHandling.BothBoundsInclusive
2731 if lowerExclusive:
2732 boundHandling |= RangeBoundHandling.LowerBoundExclusive
2734 if upperExclusive:
2735 boundHandling |= RangeBoundHandling.UpperBoundExclusive
2737 return self.__class__(lBound, uBound, boundHandling)
2739 def __lt__(self, other: Any) -> bool:
2740 """
2741 Compare a version range and a version numbers if the version range is less than the second operand (version).
2743 :param other: Operand to compare against.
2744 :returns: ``True``, if version range is less than the second operand (version).
2745 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2746 """
2747 # TODO: support VersionRange < VersionRange too
2748 # TODO: support str, int, ... like Version ?
2749 if not isinstance(other, Version):
2750 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2751 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2752 raise ex
2754 self._CheckCompatibility(other)
2756 if self._upperBound is None:
2757 return False
2759 return self._upperBound < other
2761 def __le__(self, other: Any) -> bool:
2762 """
2763 Compare a version range and a version numbers if the version range is less than or equal the second operand (version).
2765 :param other: Operand to compare against.
2766 :returns: ``True``, if version range is less than or equal the second operand (version).
2767 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2768 """
2769 # TODO: support VersionRange < VersionRange too
2770 # TODO: support str, int, ... like Version ?
2771 if not isinstance(other, Version): 2771 ↛ 2772line 2771 didn't jump to line 2772 because the condition on line 2771 was never true
2772 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2773 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2774 raise ex
2776 self._CheckCompatibility(other)
2778 if self._upperBound is None:
2779 return False
2781 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2782 return self._upperBound < other
2784 return self._upperBound <= other
2786 def __gt__(self, other: Any) -> bool:
2787 """
2788 Compare a version range and a version numbers if the version range is greater than the second operand (version).
2790 :param other: Operand to compare against.
2791 :returns: ``True``, if version range is greater than the second operand (version).
2792 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2793 """
2794 # TODO: support VersionRange < VersionRange too
2795 # TODO: support str, int, ... like Version ?
2796 if not isinstance(other, Version): 2796 ↛ 2797line 2796 didn't jump to line 2797 because the condition on line 2796 was never true
2797 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2798 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2799 raise ex
2801 self._CheckCompatibility(other)
2803 if self._lowerBound is None:
2804 return False
2806 return self._lowerBound > other
2808 def __ge__(self, other: Any) -> bool:
2809 """
2810 Compare a version range and a version numbers if the version range is greater than or equal the second operand (version).
2812 :param other: Operand to compare against.
2813 :returns: ``True``, if version range is greater than or equal the second operand (version).
2814 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
2815 """
2816 # TODO: support VersionRange < VersionRange too
2817 # TODO: support str, int, ... like Version ?
2818 if not isinstance(other, Version): 2818 ↛ 2819line 2818 didn't jump to line 2819 because the condition on line 2818 was never true
2819 ex = TypeError("Parameter 'other' is not of type 'Version'.")
2820 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
2821 raise ex
2823 self._CheckCompatibility(other)
2825 if self._lowerBound is None:
2826 return False
2828 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling: 2828 ↛ 2829line 2828 didn't jump to line 2829 because the condition on line 2828 was never true
2829 return self._lowerBound > other
2831 return self._lowerBound >= other
2833 def __contains__(self, version: Version) -> bool:
2834 """
2835 Check if the version is in the version range.
2837 :param version: Version to check.
2838 :returns: ``True``, if version is in range.
2839 :raises TypeError: If parameter ``version`` is not of type :class:`Version`.
2840 :raises TypeError: If parameter ``version``'s type is unrelated to this range's bounds. |br|
2841 The rule is the one :meth:`__init__` applies between the bounds.
2842 """
2843 if not isinstance(version, Version): 2843 ↛ 2844line 2843 didn't jump to line 2844 because the condition on line 2843 was never true
2844 ex = TypeError("Parameter 'item' is not of type 'Version'.")
2845 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
2846 raise ex
2848 self._CheckCompatibility(version)
2850 # An unbound end excludes nothing, so its half of the comparison is simply not made.
2851 if self._lowerBound is not None:
2852 if RangeBoundHandling.LowerBoundExclusive in self._boundHandling:
2853 if not (self._lowerBound < version):
2854 return False
2855 elif not (self._lowerBound <= version):
2856 return False
2858 if self._upperBound is not None:
2859 if RangeBoundHandling.UpperBoundExclusive in self._boundHandling:
2860 if not (version < self._upperBound):
2861 return False
2862 elif not (version <= self._upperBound):
2863 return False
2865 return True
2868@export
2869class VersionSet(Generic[V], metaclass=ExtendedType, slots=True):
2870 """
2871 Representation of an ordered set of versions.
2873 This version set works with :class:`SemanticVersion` and :class:`CalendarVersion` and its derived classes.
2874 """
2875 _items: list[V] #: An ordered list of set members.
2877 def __init__(self, versions: Union[Version, Iterable[V]]) -> None:
2878 """
2879 Initializes a version set either by a single version or an iterable of versions.
2881 :param versions: A single version or an iterable of versions.
2882 :raises ValueError: If parameter ``versions`` is None`.
2883 :raises TypeError: In case of a single version, if parameter ``version`` is not of type :class:`Version`.
2884 :raises TypeError: In case of an iterable, if parameter ``versions`` containes elements, which are not of type :class:`Version`.
2885 :raises TypeError: If parameter ``versions`` is neither a single version nor an iterable thereof.
2886 """
2887 if versions is None:
2888 raise ValueError("Parameter 'versions' is None.")
2890 if isinstance(versions, Version):
2891 self._items = [versions]
2892 elif isinstance(versions, abc_Iterable): 2892 ↛ 2910line 2892 didn't jump to line 2910 because the condition on line 2892 was always true
2893 iterator = iter(versions)
2894 try:
2895 firstVersion = next(iterator)
2896 except StopIteration:
2897 self._items = []
2898 return
2900 if not isinstance(firstVersion, Version): 2900 ↛ 2901line 2900 didn't jump to line 2901 because the condition on line 2900 was never true
2901 raise TypeError("First element in parameter 'versions' is not of type Version.")
2903 baseType = firstVersion.__class__
2904 for version in iterator:
2905 if not isinstance(version, baseType):
2906 raise TypeError(f"Element from parameter 'versions' is not of type {baseType.__name__}")
2908 self._items = list(sorted(versions))
2909 else:
2910 raise TypeError("Parameter 'versions' is not an Iterable.")
2912 def __and__(self, other: VersionSet[V]) -> VersionSet[V]:
2913 """
2914 Compute intersection of two version sets.
2916 :param other: Second set of versions.
2917 :returns: Intersection of two version sets.
2918 """
2919 selfIterator = self.__iter__()
2920 otherIterator = other.__iter__()
2922 result = []
2923 try:
2924 selfValue = next(selfIterator)
2925 otherValue = next(otherIterator)
2927 while True:
2928 if selfValue < otherValue:
2929 selfValue = next(selfIterator)
2930 elif otherValue < selfValue:
2931 otherValue = next(otherIterator)
2932 else:
2933 result.append(selfValue)
2934 selfValue = next(selfIterator)
2935 otherValue = next(otherIterator)
2937 except StopIteration:
2938 pass
2940 return VersionSet(result)
2942 def __or__(self, other: VersionSet[V]) -> VersionSet[V]:
2943 """
2944 Compute union of two version sets.
2946 :param other: Second set of versions.
2947 :returns: Union of two version sets.
2948 """
2949 selfIterator = self.__iter__()
2950 otherIterator = other.__iter__()
2952 result = []
2953 try:
2954 selfValue = next(selfIterator)
2955 except StopIteration:
2956 for otherValue in otherIterator:
2957 result.append(otherValue)
2959 try:
2960 otherValue = next(otherIterator)
2961 except StopIteration:
2962 for selfValue in selfIterator:
2963 result.append(selfValue)
2965 while True:
2966 if selfValue < otherValue:
2967 result.append(selfValue)
2968 try:
2969 selfValue = next(selfIterator)
2970 except StopIteration:
2971 result.append(otherValue)
2972 for otherValue in otherIterator: 2972 ↛ 2973line 2972 didn't jump to line 2973 because the loop on line 2972 never started
2973 result.append(otherValue)
2975 break
2976 elif otherValue < selfValue:
2977 result.append(otherValue)
2978 try:
2979 otherValue = next(otherIterator)
2980 except StopIteration:
2981 result.append(selfValue)
2982 for selfValue in selfIterator:
2983 result.append(selfValue)
2985 break
2986 else:
2987 result.append(selfValue)
2988 try:
2989 selfValue = next(selfIterator)
2990 except StopIteration:
2991 for otherValue in otherIterator: 2991 ↛ 2992line 2991 didn't jump to line 2992 because the loop on line 2991 never started
2992 result.append(otherValue)
2994 break
2996 try:
2997 otherValue = next(otherIterator)
2998 except StopIteration:
2999 for selfValue in selfIterator:
3000 result.append(selfValue)
3002 break
3004 return VersionSet(result)
3006 def __lt__(self, other: Any) -> bool:
3007 """
3008 Compare a version set and a version numbers if the version set is less than the second operand (version).
3010 :param other: Operand to compare against.
3011 :returns: ``True``, if version set is less than the second operand (version).
3012 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
3013 """
3014 # TODO: support VersionRange < VersionRange too
3015 # TODO: support str, int, ... like Version ?
3016 if not isinstance(other, Version):
3017 ex = TypeError("Parameter 'other' is not of type 'Version'.")
3018 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
3019 raise ex
3021 return self._items[-1] < other
3023 def __le__(self, other: Any) -> bool:
3024 """
3025 Compare a version set and a version numbers if the version set is less than or equal the second operand (version).
3027 :param other: Operand to compare against.
3028 :returns: ``True``, if version set is less than or equal the second operand (version).
3029 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
3030 """
3031 # TODO: support VersionRange < VersionRange too
3032 # TODO: support str, int, ... like Version ?
3033 if not isinstance(other, Version): 3033 ↛ 3034line 3033 didn't jump to line 3034 because the condition on line 3033 was never true
3034 ex = TypeError("Parameter 'other' is not of type 'Version'.")
3035 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
3036 raise ex
3038 return self._items[-1] <= other
3040 def __gt__(self, other: Any) -> bool:
3041 """
3042 Compare a version set and a version numbers if the version set is greater than the second operand (version).
3044 :param other: Operand to compare against.
3045 :returns: ``True``, if version set is greater than the second operand (version).
3046 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
3047 """
3048 # TODO: support VersionRange < VersionRange too
3049 # TODO: support str, int, ... like Version ?
3050 if not isinstance(other, Version): 3050 ↛ 3051line 3050 didn't jump to line 3051 because the condition on line 3050 was never true
3051 ex = TypeError("Parameter 'other' is not of type 'Version'.")
3052 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
3053 raise ex
3055 return self._items[0] > other
3057 def __ge__(self, other: Any) -> bool:
3058 """
3059 Compare a version set and a version numbers if the version set is greater than or equal the second operand (version).
3061 :param other: Operand to compare against.
3062 :returns: ``True``, if version set is greater than or equal the second operand (version).
3063 :raises TypeError: If parameter ``other`` is not of type :class:`Version`.
3064 """
3065 # TODO: support VersionRange < VersionRange too
3066 # TODO: support str, int, ... like Version ?
3067 if not isinstance(other, Version): 3067 ↛ 3068line 3067 didn't jump to line 3068 because the condition on line 3067 was never true
3068 ex = TypeError("Parameter 'other' is not of type 'Version'.")
3069 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
3070 raise ex
3072 return self._items[0] >= other
3074 def __contains__(self, version: V) -> bool:
3075 """
3076 Checks if the version a member of the set.
3078 :param version: The version to check.
3079 :returns: ``True``, if the version is a member of the set.
3080 """
3081 return version in self._items
3083 def __len__(self) -> int:
3084 """
3085 Returns the number of members in the set.
3087 :returns: Number of set members.
3088 """
3089 return len(self._items)
3091 def __iter__(self) -> Iterator[V]:
3092 """
3093 Returns an iterator to iterate all versions of this set from lowest to highest.
3095 :returns: Iterator to iterate versions.
3096 """
3097 return self._items.__iter__()
3099 def __getitem__(self, index: int) -> V:
3100 """
3101 Access to a version of a set by index.
3103 :param index: The index of the version to access.
3104 :returns: The indexed version.
3106 .. hint::
3108 Versions are ordered from lowest to highest version number.
3109 """
3110 return self._items[index]
3113#: The :class:`Version` class under a name no constraint shadows with a property of its own.
3114_VersionType = Version
3117@export
3118class VersionComparison(Enum):
3119 """
3120 The comparison one constraint of a :class:`VersionExpression` applies to a version.
3122 The six ordering comparisons mean the same thing in every packaging ecosystem, even where the spelling differs -
3123 Debian writes ``<<`` for :attr:`LessThan` and npm writes ``=`` for :attr:`Equal`. Each member's value is its
3124 *canonical* spelling, which is what a constraint renders as; a dialect maps its own spellings onto these members
3125 while parsing.
3126 """
3128 Equal = "==" #: The version has to be equal to the constraint's version.
3129 Unequal = "!=" #: The version must not be the constraint's version.
3130 LessThan = "<" #: The version has to be lower than the constraint's version.
3131 LessThanOrEqual = "<=" #: The version must not be higher than the constraint's version.
3132 GreaterThan = ">" #: The version has to be higher than the constraint's version.
3133 GreaterThanOrEqual = ">=" #: The version must not be lower than the constraint's version.
3134 CompatibleRelease = "~=" #: The version has to be compatible with the constraint's version (:pep:`440`).
3135 Caret = "^" #: The version must not change the constraint's leftmost non-zero part (npm).
3136 Tilde = "~" #: The version must not change the constraint's minor part (npm).
3138 def __str__(self) -> str:
3139 """
3140 Return the operator in its canonical spelling.
3142 :returns: The comparison's operator.
3143 """
3144 return self.value
3147@export
3148class VersionConstraint(Generic[V], metaclass=ExtendedType, slots=True):
3149 """
3150 One comparison of a :class:`VersionExpression`.
3152 A constraint is written e.g. as ``>=1.2.0``. It is a container of the versions satisfying it, so membership is asked
3153 with ``in``:
3155 .. code-block:: python
3157 constraint = VersionConstraint(VersionComparison.GreaterThanOrEqual, SemanticVersion.Parse("1.2.0")) # >=1.2.0
3158 SemanticVersion.Parse("1.5.0") in constraint # True
3160 .. seealso::
3162 :class:`CompatibleVersionConstraint`
3163 |rarr| The constraint implementing :attr:`~VersionComparison.CompatibleRelease`.
3164 """
3166 _comparison: VersionComparison #: The comparison this constraint applies.
3167 _version: V #: The version the compared version is held against.
3169 def __init__(self, comparison: VersionComparison, version: V) -> None:
3170 """
3171 Initialize a constraint from a comparison and the version it compares against.
3173 :param comparison: The comparison to apply.
3174 :param version: The version to compare against.
3175 :raises TypeError: If parameter 'comparison' is not of type :class:`VersionComparison`.
3176 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3177 :raises ValueError: If parameter 'comparison' is a shorthand like
3178 :attr:`~VersionComparison.CompatibleRelease`. |br|
3179 Use the matching :class:`RangeVersionConstraint`, which derives an upper bound from the
3180 version.
3181 """
3182 if not isinstance(comparison, VersionComparison):
3183 ex = TypeError("Parameter 'comparison' is not of type 'VersionComparison'.")
3184 ex.add_note(f"Got type '{getFullyQualifiedName(comparison)}'.")
3185 raise ex
3187 if not isinstance(version, Version):
3188 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3189 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3190 raise ex
3192 if comparison in _SHORTHAND_COMPARISONS and not isinstance(self, RangeVersionConstraint):
3193 ex = ValueError(f"Comparison '{comparison.name}' is not a plain comparison.")
3194 ex.add_note("Use the matching 'RangeVersionConstraint', which derives an upper bound from the version.")
3195 raise ex
3197 self._comparison = comparison
3198 self._version = version
3200 @readonly
3201 def Comparison(self) -> VersionComparison:
3202 """
3203 Read-only property to access the comparison this constraint applies (:attr:`_comparison`).
3205 :returns: The comparison.
3206 """
3207 return self._comparison
3209 @readonly
3210 def Version(self) -> V:
3211 """
3212 Read-only property to access the version this constraint compares against (:attr:`_version`).
3214 :returns: The version.
3215 """
3216 return self._version
3218 def __contains__(self, version: V) -> bool:
3219 """
3220 Check if a version satisfies this constraint.
3222 :param version: The version to check.
3223 :returns: ``True``, if the version satisfies the constraint.
3224 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3225 :raises ValueError: If this constraint carries a comparison a plain constraint cannot apply. |br|
3226 A shorthand is implemented by a :class:`RangeVersionConstraint`.
3227 """
3228 if not isinstance(version, Version): 3228 ↛ 3229line 3228 didn't jump to line 3229 because the condition on line 3228 was never true
3229 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3230 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3231 raise ex
3233 if self._comparison is VersionComparison.Equal:
3234 return version == self._version
3235 elif self._comparison is VersionComparison.Unequal:
3236 return version != self._version
3237 elif self._comparison is VersionComparison.LessThan:
3238 return version < self._version
3239 elif self._comparison is VersionComparison.LessThanOrEqual:
3240 return version <= self._version
3241 elif self._comparison is VersionComparison.GreaterThan:
3242 return version > self._version
3243 elif self._comparison is VersionComparison.GreaterThanOrEqual:
3244 return version >= self._version
3246 ex = ValueError(f"Comparison '{self._comparison.name}' cannot be applied by a plain constraint.")
3247 ex.add_note("A shorthand is implemented by a 'RangeVersionConstraint'.")
3248 raise ex
3250 def ToVersionRange(self) -> VersionRange[_VersionType]:
3251 """
3252 Convert this constraint into the :class:`VersionRange` it describes.
3254 Five of the six comparisons are intervals once a bound may be unbound:
3256 * ``>=1.2.0`` is ``[1.2.0, )``,
3257 * ``>1.2.0`` is ``(1.2.0, )``,
3258 * ``<=2.0.0`` is ``( , 2.0.0]``,
3259 * ``<2.0.0`` is ``( , 2.0.0)``,
3260 * ``==1.2.0`` is ``[1.2.0, 1.2.0]``, a range of exactly one version.
3262 :attr:`~VersionComparison.Unequal` is the exception and has no range: the complement of a single version is
3263 not an interval but a *union* of two, one below it and one above. That is why an expression keeps both
3264 representations rather than being replaced by a range.
3266 :returns: The range of versions satisfying this constraint.
3267 :raises ValueError: If this constraint is an :attr:`~VersionComparison.Unequal`, which no single range
3268 describes. |br|
3269 The complement of a version is a union of two intervals.
3270 """
3271 if self._comparison is VersionComparison.Equal:
3272 return VersionRange(self._version, self._version)
3273 elif self._comparison is VersionComparison.GreaterThanOrEqual:
3274 return VersionRange(self._version, None)
3275 elif self._comparison is VersionComparison.GreaterThan:
3276 return VersionRange(self._version, None, RangeBoundHandling.LowerBoundExclusive)
3277 elif self._comparison is VersionComparison.LessThanOrEqual:
3278 return VersionRange(None, self._version)
3279 elif self._comparison is VersionComparison.LessThan:
3280 return VersionRange(None, self._version, RangeBoundHandling.UpperBoundExclusive)
3282 ex = ValueError(f"Comparison '{self._comparison.name}' describes no single version range.")
3283 ex.add_note("The complement of a version is a union of two intervals, which a 'VersionRange' cannot hold.")
3284 raise ex
3286 def __str__(self) -> str:
3287 """
3288 Return the constraint in its canonical spelling.
3290 :returns: The operator followed by the version.
3291 """
3292 return f"{self._comparison}{self._version}"
3295#: The comparisons a plain :class:`VersionConstraint` cannot express, because each derives an upper bound.
3296_SHORTHAND_COMPARISONS = (
3297 VersionComparison.CompatibleRelease,
3298 VersionComparison.Caret,
3299 VersionComparison.Tilde,
3300)
3303@export
3304class RangeVersionConstraint(VersionConstraint[V]):
3305 """
3306 Base-class of the shorthand constraints meaning *at least this version, and below a derived bound*.
3308 Every packaging ecosystem has one of these, spells it differently, and derives its upper bound by a **different**
3309 rule:
3311 * :pep:`440` writes ``~=``,
3312 * npm writes ``^`` and ``~``,
3313 * RubyGems writes ``~>``.
3315 The rule is the only thing that differs, so it is what a derived class supplies in :meth:`_DeriveUpperBound`.
3317 .. seealso::
3319 :class:`CompatibleVersionConstraint`
3320 |rarr| :pep:`440`'s ``~=``.
3321 :class:`CaretVersionConstraint`
3322 |rarr| npm's ``^``.
3323 :class:`TildeVersionConstraint`
3324 |rarr| npm's ``~``.
3325 """
3327 _upperBound: SemanticVersion #: The first version outside the constraint, derived from the written version.
3329 def __init__(self, comparison: VersionComparison, version: V) -> None:
3330 """
3331 Initialize a shorthand constraint and derive its upper bound.
3333 :param comparison: The shorthand comparison this constraint applies.
3334 :param version: The version the shorthand is written with.
3335 :raises TypeError: If parameter 'version' is not of type :class:`SemanticVersion`. |br|
3336 An upper bound is derived from the version's parts, which only a semantic version has.
3337 :raises ValueError: If the version has too few parts for this shorthand.
3338 """
3339 if not isinstance(version, SemanticVersion):
3340 ex = TypeError("Parameter 'version' is not of type 'SemanticVersion'.")
3341 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3342 raise ex
3344 super().__init__(comparison, version)
3346 self._upperBound = self._DeriveUpperBound(version)
3348 @abstractmethod
3349 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion: # type: ignore[empty-body]
3350 """
3351 Derive the first version outside this constraint from the version it is written with.
3353 The bound has to be built in the written version's **epoch**. A bound left in epoch 0 outranks nothing, so
3354 the constraint would match no version at all - not even the one it was written with.
3356 :param version: The version the shorthand is written with, always a semantic version.
3357 :returns: The exclusive upper bound, in the same epoch as ``version``.
3358 :raises ValueError: If the version has too few parts for this shorthand.
3359 """
3361 @readonly
3362 def UpperBound(self) -> SemanticVersion:
3363 """
3364 Read-only property to access the first version outside this constraint (:attr:`_upperBound`).
3366 :returns: The exclusive upper bound derived from the written version.
3367 """
3368 return self._upperBound
3370 def __contains__(self, version: V) -> bool:
3371 """
3372 Check if a version is within this constraint.
3374 :param version: The version to check.
3375 :returns: ``True``, if the version is at least the written one and below the derived upper bound.
3376 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3377 """
3378 if not isinstance(version, Version): 3378 ↛ 3379line 3378 didn't jump to line 3379 because the condition on line 3378 was never true
3379 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3380 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3381 raise ex
3383 return self._version <= version < self._upperBound
3385 def ToVersionRange(self) -> VersionRange[_VersionType]:
3386 """
3387 Convert this constraint into the :class:`VersionRange` it describes.
3389 A shorthand constraint knows both of its bounds - the written version is the inclusive lower one, the derived
3390 bound the exclusive upper one - so the conversion is exact and loses nothing.
3392 :returns: The range of versions satisfying this constraint.
3393 """
3394 return VersionRange(
3395 self._version,
3396 self._upperBound,
3397 RangeBoundHandling.LowerBoundInclusive | RangeBoundHandling.UpperBoundExclusive
3398 )
3401@export
3402class CompatibleVersionConstraint(RangeVersionConstraint[V]):
3403 """
3404 The *compatible release* constraint, written ``~=`` by :pep:`440`.
3406 The last part written may move, the one to its left may not: ``~=1.2.3`` is ``<1.3``, ``~=1.2`` is ``<2`` and
3407 ``~=1.2.3.4`` is ``<1.2.4``. The written version therefore needs at least two parts - ``~=1`` would say nothing
3408 that ``>=1`` doesn't, and :pep:`440` rejects it for that reason.
3410 .. code-block:: python
3412 constraint = CompatibleVersionConstraint(PythonVersion.Parse("1.2.3"))
3413 PythonVersion.Parse("1.2.9") in constraint # True
3414 PythonVersion.Parse("1.3.0") in constraint # False
3415 """
3417 def __init__(self, version: V) -> None:
3418 """
3419 Initialize a compatible-release constraint from the version it is written with.
3421 :param version: The version to be compatible with, with at least two parts.
3422 """
3423 super().__init__(VersionComparison.CompatibleRelease, version)
3425 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion:
3426 """
3427 Drop the last part written and increment the one that becomes the last.
3429 :param version: The version the shorthand is written with.
3430 :returns: The exclusive upper bound.
3431 :raises ValueError: If the version has fewer than two parts.
3432 """
3433 versionType = version.__class__
3434 epoch = version.Epoch if Parts.Epoch in version._parts else None
3435 if Parts.Build in version._parts:
3436 return versionType(version.Major, version.Minor, version.Patch + 1, epoch=epoch)
3437 elif Parts.Micro in version._parts:
3438 return versionType(version.Major, version.Minor + 1, epoch=epoch)
3439 elif Parts.Minor in version._parts:
3440 return versionType(version.Major + 1, epoch=epoch)
3442 ex = ValueError(f"Version '{version}' has too few parts for a compatible release.")
3443 ex.add_note("'~=1' would mean the same as '>=1'; write at least a major and a minor part.")
3444 raise ex
3447@export
3448class CaretVersionConstraint(RangeVersionConstraint[V]):
3449 """
3450 npm's ``^``: the version must not change the **leftmost non-zero** part of the one written.
3452 ``^1.2.3`` is ``<2.0.0``, but ``^0.2.3`` is ``<0.3.0`` and ``^0.0.3`` is ``<0.0.4`` - below 1.0.0 npm treats
3453 each part as breaking, which is what makes this different from :class:`TildeVersionConstraint`. A part that was
3454 not written cannot be the pivot, so ``^0`` is ``<1.0.0`` while ``^0.0`` is ``<0.1.0``.
3456 .. note::
3458 npm excludes pre-releases of the upper bound by writing it ``<2.0.0-0``. That distinction is not modelled
3459 here; a pre-release of the bound is compared by :class:`SemanticVersion`'s own ordering.
3460 """
3462 def __init__(self, version: V) -> None:
3463 """
3464 Initialize a caret constraint from the version it is written with.
3466 :param version: The version to be compatible with.
3467 """
3468 super().__init__(VersionComparison.Caret, version)
3470 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion:
3471 """
3472 Increment the leftmost non-zero part that was actually written.
3474 :param version: The version the shorthand is written with.
3475 :returns: The exclusive upper bound.
3476 """
3477 versionType = version.__class__
3478 epoch = version.Epoch if Parts.Epoch in version._parts else None
3479 if version.Major != 0 or Parts.Minor not in version._parts:
3480 return versionType(version.Major + 1, epoch=epoch)
3481 elif version.Minor != 0 or Parts.Micro not in version._parts:
3482 return versionType(0, version.Minor + 1, epoch=epoch)
3484 return versionType(0, 0, version.Patch + 1, epoch=epoch)
3487@export
3488class TildeVersionConstraint(RangeVersionConstraint[V]):
3489 """
3490 npm's ``~``: the version must not change the minor part of the one written.
3492 ``~1.2.3`` and ``~1.2`` are both ``<1.3.0``. Only when no minor part was written does the major one become the
3493 pivot, so ``~1`` is ``<2.0.0``.
3495 This is **not** :pep:`440`'s ``~=``: the two agree on ``~1.2.3`` and disagree on ``~1.2``, which npm reads as
3496 ``<1.3.0`` and :pep:`440` as ``<2``.
3497 """
3499 def __init__(self, version: V) -> None:
3500 """
3501 Initialize a tilde constraint from the version it is written with.
3503 :param version: The version to be compatible with.
3504 """
3505 super().__init__(VersionComparison.Tilde, version)
3507 def _DeriveUpperBound(self, version: SemanticVersion) -> SemanticVersion:
3508 """
3509 Increment the minor part, or the major one when no minor part was written.
3511 :param version: The version the shorthand is written with.
3512 :returns: The exclusive upper bound.
3513 """
3514 versionType = version.__class__
3515 epoch = version.Epoch if Parts.Epoch in version._parts else None
3516 if Parts.Minor in version._parts:
3517 return versionType(version.Major, version.Minor + 1, epoch=epoch)
3519 return versionType(version.Major + 1, epoch=epoch)
3522@export
3523def _BuildConstraintPattern(
3524 operators: dict[str, VersionComparison],
3525 separators: str,
3526 versionType: type[Version]
3527) -> Pattern[str]:
3528 """
3529 Build the pattern matching one constraint of a :class:`VersionExpression` dialect.
3531 The operators are alternated longest-first, so ``>=`` wins over ``>`` and ``~=`` over any single character. The
3532 operator and its version are matched *together*, with whitespace allowed between them, which is what lets
3533 whitespace separate constraints without splitting ``>= 1.2.0`` in half.
3535 A version may hold none of the characters the dialect's operators are built from, and none of its separators, so
3536 those are excluded from it. Without that the optional operator group would let ``>=1.2.0 <2.0.0`` read its second
3537 constraint as the *version* ``<2.0.0``.
3539 The **epoch separator is the exception** and stays allowed: :pep:`440` writes an epoch ``1!1.0``, and ``!`` is
3540 also the first character of ``!=``. Excluding it would cut ``>=1!1.0`` short at the epoch. The operator
3541 alternation is tried before the version at every position, so ``!=`` is still read as an operator wherever a
3542 constraint can begin.
3544 This is a function rather than a method, so :class:`VersionExpression` can call it in its own class body. A
3545 dialect derived from it is served by :meth:`VersionExpression.__init_subclass__`.
3547 :param operators: The dialect's operator spellings.
3548 :param separators: The dialect's constraint separators.
3549 :param versionType: The dialect's version class, which names the epoch separator to keep.
3550 :returns: The pattern, with the operator as group 1 and the version as group 2.
3551 """
3552 alternation = "|".join(re_escape(operator) for operator in sorted(operators, key=len, reverse=True))
3553 excluded = (set("".join(operators)) | set(separators)) - set(versionType._EPOCH_SEPARATOR)
3555 # WORKAROUND: Python <3.12
3556 # Reusing the f-string's own quote character inside its expression needs PEP 701, so the escaped character
3557 # class is built into a variable first. On 3.11 the inlined form is a 'SyntaxError: f-string: unmatched (',
3558 # raised at import, which takes the whole package with it.
3559 # Replace by:
3560 # return re_compile(rf"({alternation})?\s*([^\s{re_escape("".join(sorted(excluded)))}]+)")
3561 excludedCharacters = re_escape("".join(sorted(excluded)))
3563 return re_compile(rf"({alternation})?\s*([^\s{excludedCharacters}]+)")
3566@export
3567class VersionExpression(Generic[V], metaclass=ExtendedType, slots=True):
3568 """
3569 A conjunction of :class:`VersionConstraint`\\ s.
3571 An expression is written e.g. as ``>=1.2.0,<2.0.0``. Every constraint has to be satisfied, which is what separating
3572 them means in every packaging ecosystem that has the notion. An expression with **no** constraints matches every
3573 version, so *no version restriction* can be represented rather than special-cased by its callers.
3575 .. code-block:: python
3577 expression = VersionExpression.Parse(">=1.2.0,<2.0.0")
3578 SemanticVersion.Parse("1.5.0") in expression # True
3579 SemanticVersion.Parse("2.0.0") in expression # False
3581 SemanticVersion.Parse("4.2.0") in VersionExpression.Parse("") # True - no constraints matches anything
3583 This class is the **ecosystem-neutral** dialect: the six ordering comparisons in their canonical spelling,
3584 separated by commas or whitespace. An ecosystem that spells its operators differently, or adds a shorthand,
3585 derives from it and overrides :attr:`_OPERATORS`, :attr:`_SEPARATORS`, :attr:`_VERSION_TYPE` or
3586 :attr:`_SHORTHANDS`. A dialect is data, not behavior.
3588 .. seealso::
3590 :class:`PythonVersionExpression`
3591 |rarr| The dialect :pep:`440` defines, which a Python requirement file writes.
3592 """
3594 #: Operator spellings this dialect accepts, mapped onto the comparison they mean.
3595 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3596 "==": VersionComparison.Equal,
3597 "!=": VersionComparison.Unequal,
3598 "<=": VersionComparison.LessThanOrEqual,
3599 ">=": VersionComparison.GreaterThanOrEqual,
3600 "<": VersionComparison.LessThan,
3601 ">": VersionComparison.GreaterThan,
3602 }
3604 #: Characters separating one constraint from the next. Whitespace always separates as well.
3605 _SEPARATORS: ClassVar[str] = ","
3607 #: The :class:`Version` class this dialect parses its versions as, unless a caller names another.
3608 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion
3610 #: The class each shorthand comparison is built as. A comparison absent here is a plain
3611 #: :class:`VersionConstraint`; the neutral dialect has no shorthand at all.
3612 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {}
3614 #: This dialect's compiled constraint pattern, built from the three tables above. A derived dialect gets its own
3615 #: in :meth:`__init_subclass__`.
3616 _CONSTRAINT_PATTERN: ClassVar[Pattern[str]] = _BuildConstraintPattern(_OPERATORS, _SEPARATORS, _VERSION_TYPE)
3618 _constraints: tuple[VersionConstraint[V], ...] #: The constraints a version has to satisfy, all of them.
3620 def __init__(self, constraints: Iterable[VersionConstraint[V]] = ()) -> None:
3621 """
3622 Initialize an expression from its constraints.
3624 :param constraints: Optional, the constraints a version has to satisfy. None of them means *any version*.
3625 :raises ValueError: If parameter 'constraints' is None.
3626 :raises TypeError: If parameter 'constraints' is not iterable.
3627 :raises TypeError: If parameter 'constraints' contains an item that is not a :class:`VersionConstraint`.
3628 """
3629 if constraints is None:
3630 raise ValueError("Parameter 'constraints' is None.")
3631 elif not isinstance(constraints, abc_Iterable):
3632 ex = TypeError("Parameter 'constraints' is not iterable.")
3633 ex.add_note(f"Got type '{getFullyQualifiedName(constraints)}'.")
3634 raise ex
3636 items = tuple(constraints)
3637 for constraint in items:
3638 if not isinstance(constraint, VersionConstraint): 3638 ↛ 3639line 3638 didn't jump to line 3639 because the condition on line 3638 was never true
3639 ex = TypeError("Parameter 'constraints' contains an item that is not of type 'VersionConstraint'.")
3640 ex.add_note(f"Got type '{getFullyQualifiedName(constraint)}'.")
3641 raise ex
3643 self._constraints = items
3645 def __init_subclass__(cls, **kwargs: Any) -> None:
3646 """
3647 Compile the constraint pattern of a newly defined dialect.
3649 A dialect is data: its operators, its separators and its version type are class variables, so its pattern is
3650 settled once the class body has been read and is built here instead of on every :meth:`Parse`.
3652 :param kwargs: Keyword arguments passed on to the base implementation.
3653 """
3654 super().__init_subclass__(**kwargs)
3656 cls._CONSTRAINT_PATTERN = _BuildConstraintPattern(cls._OPERATORS, cls._SEPARATORS, cls._VERSION_TYPE)
3658 @classmethod
3659 def Parse(cls, expression: Nullable[str], versionType: Nullable[type[Version]] = None) -> Self:
3660 """
3661 Parse an expression into its constraints.
3663 An expression is written e.g. as ``>=1.2.0,<2.0.0``. A constraint without an operator is an equality, so ``1.2.0``
3664 and ``==1.2.0`` are the same statement. An empty expression yields an expression with no constraints, which every
3665 version satisfies - that is how *no version restriction* is written.
3667 The expression is *scanned* rather than split, so a dialect separating constraints by whitespace does not
3668 break a constraint that has whitespace after its operator.
3670 :param expression: The expression to parse, or ``None`` for *any version*.
3671 :param versionType: Optional, the :class:`Version` class the versions are parsed as. Defaults to the
3672 dialect's :attr:`_VERSION_TYPE`.
3673 :returns: The parsed expression.
3674 :raises TypeError: If parameter 'expression' is not a string.
3675 :raises ValueError: If the expression holds input this dialect doesn't accept. |br|
3676 The note names the operators this dialect accepts.
3677 :raises ValueError: If a version in the expression can't be parsed. |br|
3678 The note names the operators this dialect accepts, because an operator another
3679 ecosystem spells differently is read as part of the version.
3680 """
3681 if expression is None:
3682 return cls()
3683 elif not isinstance(expression, str):
3684 ex = TypeError("Parameter 'expression' is not of type 'str'.")
3685 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.")
3686 raise ex
3687 elif (expression := expression.strip()) == "":
3688 return cls()
3690 versionType = cls._VERSION_TYPE if versionType is None else versionType
3691 skippable = cls._SEPARATORS + " \t"
3692 # 'V' is unbound here - the version class comes from the dialect or the parameter, not from the type variable.
3693 constraints: list[VersionConstraint[Any]] = []
3694 position = 0
3696 for match in cls._CONSTRAINT_PATTERN.finditer(expression):
3697 if (skipped := expression[position:match.start()].strip(skippable)) != "":
3698 ex = ValueError(f"Expression '{expression}' has unexpected input at '{skipped}'.")
3699 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.")
3700 raise ex
3702 operator = match.group(1)
3703 comparison = VersionComparison.Equal if operator is None else cls._OPERATORS[operator]
3704 try:
3705 version = versionType.Parse(match.group(2))
3706 except ValueError as cause:
3707 # An operator this dialect doesn't know is not recognized as one, so it lands in the version instead.
3708 ex = ValueError(f"Expression '{expression}' has unexpected input at '{match.group(0).strip()}'.")
3709 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.")
3710 raise ex from cause
3712 if (shorthand := cls._SHORTHANDS.get(comparison, None)) is not None:
3713 constraints.append(shorthand(version))
3714 else:
3715 constraints.append(VersionConstraint(comparison, version))
3716 position = match.end()
3718 if (skipped := expression[position:].strip(skippable)) != "": 3718 ↛ 3719line 3718 didn't jump to line 3719 because the condition on line 3718 was never true
3719 ex = ValueError(f"Expression '{expression}' has unexpected input at '{skipped}'.")
3720 ex.add_note(f"This dialect accepts the operators {', '.join(sorted(cls._OPERATORS))}.")
3721 raise ex
3723 return cls(constraints)
3725 @readonly
3726 def Constraints(self) -> tuple[VersionConstraint[V], ...]:
3727 """
3728 Read-only property to access the constraints a version has to satisfy (:attr:`_constraints`).
3730 :returns: The constraints, empty if the expression matches every version.
3731 """
3732 return self._constraints
3734 @readonly
3735 def MatchesAnyVersion(self) -> bool:
3736 """
3737 Read-only property to return whether this expression constrains nothing.
3739 :returns: ``True``, if the expression has no constraints and every version satisfies it.
3740 """
3741 return len(self._constraints) == 0
3743 def __contains__(self, version: V) -> bool:
3744 """
3745 Check if a version satisfies every constraint of this expression.
3747 :param version: The version to check.
3748 :returns: ``True``, if the version satisfies all constraints. An expression without constraints is
3749 satisfied by every version.
3750 :raises TypeError: If parameter 'version' is not of type :class:`Version`.
3751 """
3752 if not isinstance(version, Version):
3753 ex = TypeError("Parameter 'version' is not of type 'Version'.")
3754 ex.add_note(f"Got type '{getFullyQualifiedName(version)}'.")
3755 raise ex
3757 return all(version in constraint for constraint in self._constraints)
3759 def ToVersionRange(self) -> VersionRange[Version]:
3760 """
3761 Convert this expression into the single :class:`VersionRange` it describes.
3763 An expression is a conjunction, so its range is the **intersection** of its constraints' ranges, which
3764 :meth:`VersionRange.__and__` computes. An expression with no constraints is the range unbound at both ends,
3765 since both match every version.
3767 Not every expression has a range. One containing an :attr:`~VersionComparison.Unequal` does not, because the
3768 complement of a version is a union of two intervals - ``>=1.0,!=1.3,<2.0`` is an ordinary requirement with
3769 no single range. That is why :class:`VersionExpression` is not replaced by :class:`VersionRange`: a range is
3770 one interval, an expression is any conjunction, and the second is strictly more expressive.
3772 :returns: The range of versions satisfying every constraint.
3773 :raises ValueError: If a constraint describes no range, which an :attr:`~VersionComparison.Unequal` never
3774 does.
3775 :raises ValueError: If the constraints have no version in common.
3776 """
3777 versionRange: VersionRange[_VersionType] = VersionRange(None, None)
3778 for constraint in self._constraints:
3779 versionRange = versionRange & constraint.ToVersionRange()
3781 return versionRange
3783 def __len__(self) -> int:
3784 """
3785 Return the number of constraints in this expression.
3787 :returns: Number of constraints.
3788 """
3789 return len(self._constraints)
3791 def __iter__(self) -> Iterator[VersionConstraint[V]]:
3792 """
3793 Iterate the constraints of this expression, in the order they were written.
3795 :returns: An iterator over the constraints.
3796 """
3797 return iter(self._constraints)
3799 def __str__(self) -> str:
3800 """
3801 Return the expression in this dialect's spelling.
3803 A :class:`VersionConstraint` renders itself canonically, which is not what every dialect writes - Debian
3804 spells :attr:`~VersionComparison.Equal` ``=`` and :attr:`~VersionComparison.LessThan` ``<<``. The dialect's
3805 own operator table answers what it writes; where a dialect has several spellings for one comparison, the
3806 first one wins.
3808 :returns: The constraints, joined by this dialect's separator, or an empty string if it constrains nothing.
3809 """
3810 separator: str = self._SEPARATORS[0] if len(self._SEPARATORS) > 0 else " "
3811 spelled: list[str] = []
3813 for constraint in self._constraints:
3814 for spelling, comparison in self._OPERATORS.items(): 3814 ↛ 3819line 3814 didn't jump to line 3819 because the loop on line 3814 didn't complete
3815 if comparison is constraint.Comparison:
3816 spelled.append(f"{spelling}{constraint.Version}")
3817 break
3818 else:
3819 spelled.append(str(constraint))
3821 return separator.join(spelled)
3824@export
3825class PythonVersionExpression(VersionExpression[V]):
3826 """
3827 A version expression in the dialect :pep:`440` defines, which is what a Python requirement file writes.
3829 It adds the compatible release operator ``~=`` to the six ordering comparisons and parses its versions as
3830 :class:`PythonVersion`:
3832 .. code-block:: python
3834 expression = PythonVersionExpression.Parse("~=1.2.3")
3835 PythonVersion.Parse("1.2.9") in expression # True
3836 PythonVersion.Parse("1.3.0") in expression # False
3838 .. seealso::
3840 :class:`CompatibleVersionConstraint`
3841 |rarr| What ``~=`` is parsed into, and how its upper bound is derived.
3842 """
3844 #: The neutral dialect's operators, plus the compatible release operator :pep:`440` defines.
3845 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3846 **VersionExpression._OPERATORS,
3847 "~=": VersionComparison.CompatibleRelease,
3848 }
3850 #: :pep:`440` versions, so an epoch, a release candidate or a post-release parses.
3851 _VERSION_TYPE: ClassVar[type[Version]] = PythonVersion
3853 #: ``~=`` derives an upper bound, so it is not a plain comparison.
3854 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {
3855 VersionComparison.CompatibleRelease: CompatibleVersionConstraint,
3856 }
3859@export
3860class NPMVersionExpression(VersionExpression[V]):
3861 """
3862 A version expression in npm's dialect, which is what a ``package.json`` dependency writes.
3864 npm differs from :pep:`440` in three ways that matter to a parser:
3866 * constraints are separated by **whitespace**, and a comma is a syntax error;
3867 * equality is written ``=``, never ``==``;
3868 * there is **no** ``!=`` - npm cannot exclude a single version this way.
3870 It adds ``^`` and ``~``, which are not :pep:`440`'s ``~=``:
3872 .. code-block:: python
3874 expression = NPMVersionExpression.Parse("^1.2.3")
3875 SemanticVersion.Parse("1.9.0") in expression # True
3876 SemanticVersion.Parse("2.0.0") in expression # False
3878 .. note::
3880 The shorthands ``1.2.x``, ``*``, the hyphen range ``1.2.3 - 2.3.4`` and the alternative ``||`` are **not**
3881 parsed. The first three are further rewriting rules; ``||`` is a disjunction, which this class cannot hold
3882 because every constraint of an expression has to be satisfied.
3884 .. seealso::
3886 :class:`CaretVersionConstraint` |br|
3887 :class:`TildeVersionConstraint`
3888 """
3890 #: npm's operators. No ``==`` and no ``!=``; ``^`` and ``~`` are npm's own shorthands.
3891 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3892 "<=": VersionComparison.LessThanOrEqual,
3893 ">=": VersionComparison.GreaterThanOrEqual,
3894 "<": VersionComparison.LessThan,
3895 ">": VersionComparison.GreaterThan,
3896 "=": VersionComparison.Equal,
3897 "^": VersionComparison.Caret,
3898 "~": VersionComparison.Tilde,
3899 }
3901 #: npm separates constraints by whitespace alone; a comma is a syntax error there.
3902 _SEPARATORS: ClassVar[str] = ""
3904 #: npm is strict semantic versioning.
3905 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion
3907 #: ``^`` and ``~`` each derive an upper bound, by different rules.
3908 _SHORTHANDS: ClassVar[dict[VersionComparison, type]] = {
3909 VersionComparison.Caret: CaretVersionConstraint,
3910 VersionComparison.Tilde: TildeVersionConstraint,
3911 }
3914@export
3915class DebianVersionExpression(VersionExpression[V]):
3916 """
3917 A version expression in Debian's dialect, as a ``debian/control`` dependency writes it inside its parentheses.
3919 Debian spells the strict comparisons ``<<`` and ``>>``, equality ``=``, and has **no** ``!=``. The obsolete
3920 spellings ``<`` and ``>`` are deliberately **not** accepted: ``dpkg`` still takes them but warns, because they
3921 historically meant ``<=`` and ``>=`` - reading them silently as the strict operators would invert their meaning.
3923 .. code-block:: python
3925 expression = DebianVersionExpression.Parse(">> 1.2.3")
3926 SemanticVersion.Parse("1.3.0") in expression # True
3928 .. note::
3930 A Debian dependency states **one** constraint per package mention - ``pkg (>= 1.0), pkg (<< 2.0)`` - so an
3931 expression here usually holds a single constraint. The comma is Debian's *dependency* separator, not a
3932 constraint separator.
3934 .. attention::
3936 Debian version strings are ``epoch:upstream-revision``. :class:`SemanticVersion` reads the revision as a
3937 postfix, but an **epoch** (``2:1.2.3-1``) does not parse, and ``1.2.3-1`` renders back as ``1.2.3+1``.
3938 Matching Debian versions faithfully needs a ``DebianVersion`` class, which pyTooling does not have.
3939 """
3941 #: Debian's operators. ``<<`` and ``>>`` are the strict ones; there is no ``!=``.
3942 _OPERATORS: ClassVar[dict[str, VersionComparison]] = {
3943 "<<": VersionComparison.LessThan,
3944 ">>": VersionComparison.GreaterThan,
3945 "<=": VersionComparison.LessThanOrEqual,
3946 ">=": VersionComparison.GreaterThanOrEqual,
3947 "=": VersionComparison.Equal,
3948 }
3950 #: Debian's comma separates dependencies rather than constraints, but accepting it costs nothing.
3951 _SEPARATORS: ClassVar[str] = ","
3953 #: The closest pyTooling has to a Debian version - see the class doc-string's caveat.
3954 _VERSION_TYPE: ClassVar[type[Version]] = SemanticVersion